mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into search_delay
This commit is contained in:
commit
5a88a1fc41
17 changed files with 883 additions and 301 deletions
|
|
@ -205,9 +205,6 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
|
|
||||||
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
|
|
||||||
|
|
||||||
if (failedPlugins.Any())
|
if (failedPlugins.Any())
|
||||||
{
|
{
|
||||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource
|
||||||
return DefaultLanguageCode;
|
return DefaultLanguageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
|
private void AddPluginLanguageDirectories()
|
||||||
{
|
{
|
||||||
foreach (var plugin in plugins)
|
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||||
{
|
{
|
||||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||||
var dir = Path.GetDirectoryName(location);
|
var dir = Path.GetDirectoryName(location);
|
||||||
|
|
@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource
|
||||||
_oldResources.Clear();
|
_oldResources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize language. Will change app language and plugin language based on settings.
|
||||||
|
/// </summary>
|
||||||
|
public async Task InitializeLanguageAsync()
|
||||||
|
{
|
||||||
|
// Get actual language
|
||||||
|
var languageCode = _settings.Language;
|
||||||
|
if (languageCode == Constant.SystemLanguageCode)
|
||||||
|
{
|
||||||
|
languageCode = SystemLanguageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get language by language code and change language
|
||||||
|
var language = GetLanguageByLanguageCode(languageCode);
|
||||||
|
|
||||||
|
// Add plugin language directories first so that we can load language files from plugins
|
||||||
|
AddPluginLanguageDirectories();
|
||||||
|
|
||||||
|
// Change language
|
||||||
|
await ChangeLanguageAsync(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change language during runtime. Will change app language and plugin language & save settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="languageCode"></param>
|
||||||
public void ChangeLanguage(string languageCode)
|
public void ChangeLanguage(string languageCode)
|
||||||
{
|
{
|
||||||
languageCode = languageCode.NonNull();
|
languageCode = languageCode.NonNull();
|
||||||
|
|
@ -110,7 +136,12 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
// Get language by language code and change language
|
// Get language by language code and change language
|
||||||
var language = GetLanguageByLanguageCode(languageCode);
|
var language = GetLanguageByLanguageCode(languageCode);
|
||||||
ChangeLanguage(language, isSystem);
|
|
||||||
|
// Change language
|
||||||
|
_ = ChangeLanguageAsync(language);
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Language GetLanguageByLanguageCode(string languageCode)
|
private Language GetLanguageByLanguageCode(string languageCode)
|
||||||
|
|
@ -128,26 +159,22 @@ namespace Flow.Launcher.Core.Resource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ChangeLanguage(Language language, bool isSystem)
|
private async Task ChangeLanguageAsync(Language language)
|
||||||
{
|
{
|
||||||
language = language.NonNull();
|
// Remove old language files and load language
|
||||||
|
|
||||||
RemoveOldLanguageFiles();
|
RemoveOldLanguageFiles();
|
||||||
if (language != AvailableLanguages.English)
|
if (language != AvailableLanguages.English)
|
||||||
{
|
{
|
||||||
LoadLanguage(language);
|
LoadLanguage(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Culture of main thread
|
// Culture of main thread
|
||||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||||
|
|
||||||
// Raise event after culture is set
|
// Raise event for plugins after culture is set
|
||||||
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
await Task.Run(UpdatePluginMetadataTranslations);
|
||||||
_ = Task.Run(() =>
|
|
||||||
{
|
|
||||||
UpdatePluginMetadataTranslations();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Xml;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Markup;
|
using System.Windows.Markup;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Effects;
|
using System.Windows.Media.Effects;
|
||||||
|
|
@ -16,7 +17,6 @@ using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using TextBox = System.Windows.Controls.TextBox;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Resource
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
|
|
@ -56,20 +56,23 @@ namespace Flow.Launcher.Core.Resource
|
||||||
MakeSureThemeDirectoriesExist();
|
MakeSureThemeDirectoriesExist();
|
||||||
|
|
||||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||||
_oldResource = dicts.First(d =>
|
_oldResource = dicts.FirstOrDefault(d =>
|
||||||
{
|
{
|
||||||
if (d.Source == null)
|
if (d.Source == null) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
var p = d.Source.AbsolutePath;
|
var p = d.Source.AbsolutePath;
|
||||||
var dir = Path.GetDirectoryName(p).NonNull();
|
return p.Contains(Folder) && Path.GetExtension(p) == Extension;
|
||||||
var info = new DirectoryInfo(dir);
|
|
||||||
var f = info.Name;
|
|
||||||
var e = Path.GetExtension(p);
|
|
||||||
var found = f == Folder && e == Extension;
|
|
||||||
return found;
|
|
||||||
});
|
});
|
||||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
|
||||||
|
if (_oldResource != null)
|
||||||
|
{
|
||||||
|
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Error("Current theme resource not found. Initializing with default theme.");
|
||||||
|
_oldTheme = Constant.DefaultTheme;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -98,13 +101,152 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate)
|
private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate)
|
||||||
{
|
{
|
||||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
// Add new resources
|
||||||
|
if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate))
|
||||||
|
{
|
||||||
|
Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old resources
|
||||||
|
if (_oldResource != null && _oldResource != dictionaryToUpdate &&
|
||||||
|
Application.Current.Resources.MergedDictionaries.Contains(_oldResource))
|
||||||
|
{
|
||||||
|
Application.Current.Resources.MergedDictionaries.Remove(_oldResource);
|
||||||
|
}
|
||||||
|
|
||||||
dicts.Remove(_oldResource);
|
|
||||||
dicts.Add(dictionaryToUpdate);
|
|
||||||
_oldResource = dictionaryToUpdate;
|
_oldResource = dictionaryToUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates only the font settings and refreshes the UI.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateFonts()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Load a ResourceDictionary for the specified theme.
|
||||||
|
var themeName = GetCurrentTheme();
|
||||||
|
var dict = GetThemeResourceDictionary(themeName);
|
||||||
|
|
||||||
|
// Apply font settings to the theme resource.
|
||||||
|
ApplyFontSettings(dict);
|
||||||
|
UpdateResourceDictionary(dict);
|
||||||
|
|
||||||
|
// Must apply blur and drop shadow effects
|
||||||
|
_ = RefreshFrameAsync();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception("Error occurred while updating theme fonts", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads and applies font settings to the theme resource.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyFontSettings(ResourceDictionary dict)
|
||||||
|
{
|
||||||
|
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
|
||||||
|
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
|
||||||
|
{
|
||||||
|
var fontFamily = new FontFamily(_settings.QueryBoxFont);
|
||||||
|
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
|
||||||
|
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||||
|
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||||
|
|
||||||
|
SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true);
|
||||||
|
SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||||
|
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
||||||
|
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||||
|
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
||||||
|
{
|
||||||
|
var fontFamily = new FontFamily(_settings.ResultFont);
|
||||||
|
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle);
|
||||||
|
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight);
|
||||||
|
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch);
|
||||||
|
|
||||||
|
SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||||
|
SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||||
|
SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||||
|
SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||||
|
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||||
|
{
|
||||||
|
var fontFamily = new FontFamily(_settings.ResultSubFont);
|
||||||
|
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle);
|
||||||
|
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight);
|
||||||
|
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch);
|
||||||
|
|
||||||
|
SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||||
|
SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies font properties to a Style.
|
||||||
|
/// </summary>
|
||||||
|
private static void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox)
|
||||||
|
{
|
||||||
|
// Remove existing font-related setters
|
||||||
|
if (isTextBox)
|
||||||
|
{
|
||||||
|
// First, find the setters to remove and store them in a list
|
||||||
|
var settersToRemove = style.Setters
|
||||||
|
.OfType<Setter>()
|
||||||
|
.Where(setter =>
|
||||||
|
setter.Property == Control.FontFamilyProperty ||
|
||||||
|
setter.Property == Control.FontStyleProperty ||
|
||||||
|
setter.Property == Control.FontWeightProperty ||
|
||||||
|
setter.Property == Control.FontStretchProperty)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Remove each found setter one by one
|
||||||
|
foreach (var setter in settersToRemove)
|
||||||
|
{
|
||||||
|
style.Setters.Remove(setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add New font setter
|
||||||
|
style.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||||
|
style.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||||
|
style.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||||
|
style.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||||
|
|
||||||
|
// Set caret brush (retain existing logic)
|
||||||
|
var caretBrushPropertyValue = style.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
|
||||||
|
var foregroundPropertyValue = style.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
|
||||||
|
.Select(x => x.Value).FirstOrDefault();
|
||||||
|
if (!caretBrushPropertyValue && foregroundPropertyValue != null)
|
||||||
|
style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var settersToRemove = style.Setters
|
||||||
|
.OfType<Setter>()
|
||||||
|
.Where(setter =>
|
||||||
|
setter.Property == TextBlock.FontFamilyProperty ||
|
||||||
|
setter.Property == TextBlock.FontStyleProperty ||
|
||||||
|
setter.Property == TextBlock.FontWeightProperty ||
|
||||||
|
setter.Property == TextBlock.FontStretchProperty)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var setter in settersToRemove)
|
||||||
|
{
|
||||||
|
style.Setters.Remove(setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily));
|
||||||
|
style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle));
|
||||||
|
style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight));
|
||||||
|
style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ResourceDictionary GetThemeResourceDictionary(string theme)
|
private ResourceDictionary GetThemeResourceDictionary(string theme)
|
||||||
{
|
{
|
||||||
var uri = GetThemePath(theme);
|
var uri = GetThemePath(theme);
|
||||||
|
|
@ -128,22 +270,22 @@ namespace Flow.Launcher.Core.Resource
|
||||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||||
|
|
||||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
|
queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
|
queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||||
|
|
||||||
var caretBrushPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
|
var caretBrushPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
|
||||||
var foregroundPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
|
var foregroundPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
|
||||||
.Select(x => x.Value).FirstOrDefault();
|
.Select(x => x.Value).FirstOrDefault();
|
||||||
if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling
|
if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling
|
||||||
queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue));
|
queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
|
||||||
|
|
||||||
// Query suggestion box's font style is aligned with query box
|
// Query suggestion box's font style is aligned with query box
|
||||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
|
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
|
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||||
|
|
@ -180,7 +322,7 @@ namespace Flow.Launcher.Core.Resource
|
||||||
/* Ignore Theme Window Width and use setting */
|
/* Ignore Theme Window Width and use setting */
|
||||||
var windowStyle = dict["WindowStyle"] as Style;
|
var windowStyle = dict["WindowStyle"] as Style;
|
||||||
var width = _settings.WindowSize;
|
var width = _settings.WindowSize;
|
||||||
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
|
windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width));
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,11 +407,12 @@ namespace Flow.Launcher.Core.Resource
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
|
throw new DirectoryNotFoundException($"Theme path can't be found <{path}>");
|
||||||
|
|
||||||
// reload all resources even if the theme itself hasn't changed in order to pickup changes
|
// Retrieve theme resource – always use the resource with font settings applied.
|
||||||
// to things like fonts
|
var resourceDict = GetResourceDictionary(theme);
|
||||||
UpdateResourceDictionary(GetResourceDictionary(theme));
|
|
||||||
|
UpdateResourceDictionary(resourceDict);
|
||||||
|
|
||||||
_settings.Theme = theme;
|
_settings.Theme = theme;
|
||||||
|
|
||||||
|
|
@ -280,10 +423,11 @@ namespace Flow.Launcher.Core.Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
BlurEnabled = IsBlurTheme();
|
BlurEnabled = IsBlurTheme();
|
||||||
//if (_settings.UseDropShadowEffect)
|
|
||||||
// AddDropShadowEffectToCurrentTheme();
|
// Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues
|
||||||
//Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
|
_ = RefreshFrameAsync();
|
||||||
_ = SetBlurForWindowAsync();
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
catch (DirectoryNotFoundException)
|
catch (DirectoryNotFoundException)
|
||||||
{
|
{
|
||||||
|
|
@ -305,7 +449,6 @@ namespace Flow.Launcher.Core.Resource
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -481,17 +624,14 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
private void SetBlurForWindow(string theme, BackdropTypes backdropType)
|
private void SetBlurForWindow(string theme, BackdropTypes backdropType)
|
||||||
{
|
{
|
||||||
var dict = GetThemeResourceDictionary(theme);
|
var dict = GetResourceDictionary(theme);
|
||||||
if (dict == null)
|
if (dict == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
|
var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
|
||||||
if (windowBorderStyle == null)
|
if (windowBorderStyle == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
Window mainWindow = Application.Current.MainWindow;
|
var mainWindow = Application.Current.MainWindow;
|
||||||
if (mainWindow == null)
|
if (mainWindow == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
// Check if the theme supports blur
|
// Check if the theme supports blur
|
||||||
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
|
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
||||||
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
|
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
|
||||||
|
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
|
||||||
|
|
||||||
public static string PythonPath;
|
public static string PythonPath;
|
||||||
public static string NodePath;
|
public static string NodePath;
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,16 @@ GetMonitorInfo
|
||||||
MONITORINFOEXW
|
MONITORINFOEXW
|
||||||
|
|
||||||
WM_ENTERSIZEMOVE
|
WM_ENTERSIZEMOVE
|
||||||
WM_EXITSIZEMOVE
|
WM_EXITSIZEMOVE
|
||||||
|
|
||||||
|
GetKeyboardLayout
|
||||||
|
GetWindowThreadProcessId
|
||||||
|
ActivateKeyboardLayout
|
||||||
|
GetKeyboardLayoutList
|
||||||
|
PostMessage
|
||||||
|
WM_INPUTLANGCHANGEREQUEST
|
||||||
|
INPUTLANGCHANGE_FORWARD
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD1
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD2
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD3
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD4
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Microsoft.Win32;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
using Windows.Win32.Graphics.Dwm;
|
using Windows.Win32.Graphics.Dwm;
|
||||||
|
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||||
using Windows.Win32.UI.WindowsAndMessaging;
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Point = System.Windows.Point;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
|
|
@ -63,7 +67,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="window"></param>
|
/// <param name="window"></param>
|
||||||
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
||||||
|
|
@ -317,5 +321,172 @@ namespace Flow.Launcher.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Keyboard Layout
|
||||||
|
|
||||||
|
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
|
||||||
|
private const string EnglishLanguageTag = "en";
|
||||||
|
|
||||||
|
private static readonly string[] ImeLanguageTags =
|
||||||
|
{
|
||||||
|
"zh", // Chinese
|
||||||
|
"ja", // Japanese
|
||||||
|
"ko", // Korean
|
||||||
|
};
|
||||||
|
|
||||||
|
private const uint KeyboardLayoutLoWord = 0xFFFF;
|
||||||
|
|
||||||
|
// Store the previous keyboard layout
|
||||||
|
private static HKL _previousLayout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switches the keyboard layout to English if available.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="backupPrevious">If true, the current keyboard layout will be stored for later restoration.</param>
|
||||||
|
/// <exception cref="Win32Exception">Thrown when there's an error getting the window thread process ID.</exception>
|
||||||
|
public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious)
|
||||||
|
{
|
||||||
|
// Find an installed English layout
|
||||||
|
var enHKL = FindEnglishKeyboardLayout();
|
||||||
|
|
||||||
|
// No installed English layout found
|
||||||
|
if (enHKL == HKL.Null) return;
|
||||||
|
|
||||||
|
// Get the current foreground window
|
||||||
|
var hwnd = PInvoke.GetForegroundWindow();
|
||||||
|
if (hwnd == HWND.Null) return;
|
||||||
|
|
||||||
|
// Get the current foreground window thread ID
|
||||||
|
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
|
||||||
|
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||||
|
|
||||||
|
// If the current layout has an IME mode, disable it without switching to another layout.
|
||||||
|
// This is needed because for languages with IME mode, Flow Launcher just temporarily disables
|
||||||
|
// the IME mode instead of switching to another layout.
|
||||||
|
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
|
||||||
|
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
|
||||||
|
foreach (var langTag in ImeLanguageTags)
|
||||||
|
{
|
||||||
|
if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup current keyboard layout
|
||||||
|
if (backupPrevious) _previousLayout = currentLayout;
|
||||||
|
|
||||||
|
// Switch to English layout
|
||||||
|
PInvoke.ActivateKeyboardLayout(enHKL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the previously backed-up keyboard layout.
|
||||||
|
/// If it wasn't backed up or has already been restored, this method does nothing.
|
||||||
|
/// </summary>
|
||||||
|
public static void RestorePreviousKeyboardLayout()
|
||||||
|
{
|
||||||
|
if (_previousLayout == HKL.Null) return;
|
||||||
|
|
||||||
|
var hwnd = PInvoke.GetForegroundWindow();
|
||||||
|
if (hwnd == HWND.Null) return;
|
||||||
|
|
||||||
|
PInvoke.PostMessage(
|
||||||
|
hwnd,
|
||||||
|
PInvoke.WM_INPUTLANGCHANGEREQUEST,
|
||||||
|
PInvoke.INPUTLANGCHANGE_FORWARD,
|
||||||
|
_previousLayout.Value
|
||||||
|
);
|
||||||
|
|
||||||
|
_previousLayout = HKL.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an installed English keyboard layout.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Win32Exception"></exception>
|
||||||
|
private static unsafe HKL FindEnglishKeyboardLayout()
|
||||||
|
{
|
||||||
|
// Get the number of keyboard layouts
|
||||||
|
int count = PInvoke.GetKeyboardLayoutList(0, null);
|
||||||
|
if (count <= 0) return HKL.Null;
|
||||||
|
|
||||||
|
// Get all keyboard layouts
|
||||||
|
var handles = new HKL[count];
|
||||||
|
fixed (HKL* h = handles)
|
||||||
|
{
|
||||||
|
var result = PInvoke.GetKeyboardLayoutList(count, h);
|
||||||
|
if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for any English keyboard layout
|
||||||
|
foreach (var hkl in handles)
|
||||||
|
{
|
||||||
|
// The lower word contains the language identifier
|
||||||
|
var langId = (uint)hkl.Value & KeyboardLayoutLoWord;
|
||||||
|
var langTag = GetLanguageTag(langId);
|
||||||
|
|
||||||
|
// Check if it's an English layout
|
||||||
|
if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return hkl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HKL.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the
|
||||||
|
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
|
||||||
|
/// BCP 47 language tag
|
||||||
|
/// </see>
|
||||||
|
/// of the current input language.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Edited from: https://github.com/dotnet/winforms
|
||||||
|
/// </remarks>
|
||||||
|
private static string GetLanguageTag(uint langId)
|
||||||
|
{
|
||||||
|
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
|
||||||
|
// transient value.
|
||||||
|
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
|
||||||
|
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
|
||||||
|
//
|
||||||
|
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
|
||||||
|
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
|
||||||
|
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
|
||||||
|
//
|
||||||
|
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
|
||||||
|
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
|
||||||
|
//
|
||||||
|
// NOTE: this logic may break in future versions of Windows since it is not documented.
|
||||||
|
if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1
|
||||||
|
or PInvoke.LOCALE_TRANSIENT_KEYBOARD2
|
||||||
|
or PInvoke.LOCALE_TRANSIENT_KEYBOARD3
|
||||||
|
or PInvoke.LOCALE_TRANSIENT_KEYBOARD4)
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath);
|
||||||
|
if (key?.GetValue("Languages") is string[] languages)
|
||||||
|
{
|
||||||
|
foreach (string language in languages)
|
||||||
|
{
|
||||||
|
using var subKey = key.OpenSubKey(language);
|
||||||
|
if (subKey?.GetValue("TransientLangId") is int transientLangId
|
||||||
|
&& transientLangId == langId)
|
||||||
|
{
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CultureInfo.GetCultureInfo((int)langId).Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,26 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class App : IDisposable, ISingleInstanceApp
|
public partial class App : IDisposable, ISingleInstanceApp
|
||||||
{
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
public static IPublicAPI API { get; private set; }
|
public static IPublicAPI API { get; private set; }
|
||||||
private const string Unique = "Flow.Launcher_Unique_Application_Mutex";
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
private static bool _disposed;
|
private static bool _disposed;
|
||||||
|
private MainWindow _mainWindow;
|
||||||
|
private readonly MainViewModel _mainVM;
|
||||||
private readonly Settings _settings;
|
private readonly Settings _settings;
|
||||||
|
|
||||||
|
// To prevent two disposals running at the same time.
|
||||||
|
private static readonly object _disposingLock = new();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
// Initialize settings
|
// Initialize settings
|
||||||
|
|
@ -79,27 +94,33 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
_settings.Initialize();
|
_settings.Initialize();
|
||||||
|
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
|
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Local function
|
||||||
|
static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
||||||
|
{
|
||||||
|
// Firstly show users the message
|
||||||
|
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
|
||||||
|
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
||||||
|
Environment.FailFast(message, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
#endregion
|
||||||
{
|
|
||||||
// Firstly show users the message
|
|
||||||
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
|
|
||||||
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
#region Main
|
||||||
Environment.FailFast(message, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main()
|
public static void Main()
|
||||||
{
|
{
|
||||||
if (SingleInstance<App>.InitializeAsFirstInstance(Unique))
|
if (SingleInstance<App>.InitializeAsFirstInstance())
|
||||||
{
|
{
|
||||||
using var application = new App();
|
using var application = new App();
|
||||||
application.InitializeComponent();
|
application.InitializeComponent();
|
||||||
|
|
@ -107,6 +128,10 @@ namespace Flow.Launcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region App Events
|
||||||
|
|
||||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||||
|
|
||||||
private async void OnStartup(object sender, StartupEventArgs e)
|
private async void OnStartup(object sender, StartupEventArgs e)
|
||||||
|
|
@ -127,21 +152,26 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||||
|
|
||||||
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
|
||||||
Ioc.Default.GetRequiredService<Internationalization>().ChangeLanguage(_settings.Language);
|
|
||||||
|
|
||||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||||
|
|
||||||
|
// Register ResultsUpdated event after all plugins are loaded
|
||||||
|
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
|
||||||
|
|
||||||
Http.Proxy = _settings.Proxy;
|
Http.Proxy = _settings.Proxy;
|
||||||
|
|
||||||
await PluginManager.InitializePluginsAsync();
|
await PluginManager.InitializePluginsAsync();
|
||||||
|
|
||||||
|
// Change language after all plugins are initialized because we need to update plugin title based on their api
|
||||||
|
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
||||||
|
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
|
||||||
|
|
||||||
await imageLoadertask;
|
await imageLoadertask;
|
||||||
|
|
||||||
var window = new MainWindow();
|
_mainWindow = new MainWindow();
|
||||||
|
|
||||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||||
|
|
||||||
Current.MainWindow = window;
|
Current.MainWindow = _mainWindow;
|
||||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||||
|
|
||||||
HotKeyMapper.Initialize();
|
HotKeyMapper.Initialize();
|
||||||
|
|
@ -158,8 +188,7 @@ namespace Flow.Launcher
|
||||||
AutoUpdates();
|
AutoUpdates();
|
||||||
|
|
||||||
API.SaveAppAllSettings();
|
API.SaveAppAllSettings();
|
||||||
Log.Info(
|
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
|
||||||
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +221,6 @@ namespace Flow.Launcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//[Conditional("RELEASE")]
|
|
||||||
private void AutoUpdates()
|
private void AutoUpdates()
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
|
|
@ -210,11 +238,29 @@ namespace Flow.Launcher
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Register Events
|
||||||
|
|
||||||
private void RegisterExitEvents()
|
private void RegisterExitEvents()
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
|
AppDomain.CurrentDomain.ProcessExit += (s, e) =>
|
||||||
Current.Exit += (s, e) => Dispose();
|
{
|
||||||
Current.SessionEnding += (s, e) => Dispose();
|
Log.Info("|App.RegisterExitEvents|Process Exit");
|
||||||
|
Dispose();
|
||||||
|
};
|
||||||
|
|
||||||
|
Current.Exit += (s, e) =>
|
||||||
|
{
|
||||||
|
Log.Info("|App.RegisterExitEvents|Application Exit");
|
||||||
|
Dispose();
|
||||||
|
};
|
||||||
|
|
||||||
|
Current.SessionEnding += (s, e) =>
|
||||||
|
{
|
||||||
|
Log.Info("|App.RegisterExitEvents|Session Ending");
|
||||||
|
Dispose();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -235,20 +281,60 @@ namespace Flow.Launcher
|
||||||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
|
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
#endregion
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
// if sessionending is called, exit proverbially be called when log off / shutdown
|
// Prevent two disposes at the same time.
|
||||||
// but if sessionending is not called, exit won't be called when log off / shutdown
|
lock (_disposingLock)
|
||||||
if (!_disposed)
|
|
||||||
{
|
{
|
||||||
API.SaveAppAllSettings();
|
if (!disposing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stopwatch.Normal("|App.Dispose|Dispose cost", () =>
|
||||||
|
{
|
||||||
|
Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------");
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
// Dispose needs to be called on the main Windows thread,
|
||||||
|
// since some resources owned by the thread need to be disposed.
|
||||||
|
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
|
||||||
|
_mainVM?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISingleInstanceApp
|
||||||
|
|
||||||
public void OnSecondAppStarted()
|
public void OnSecondAppStarted()
|
||||||
{
|
{
|
||||||
Ioc.Default.GetRequiredService<MainViewModel>().Show();
|
Ioc.Default.GetRequiredService<MainViewModel>().Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ using System.Windows;
|
||||||
// modified to allow single instace restart
|
// modified to allow single instace restart
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper
|
||||||
{
|
{
|
||||||
public interface ISingleInstanceApp
|
public interface ISingleInstanceApp
|
||||||
{
|
{
|
||||||
void OnSecondAppStarted();
|
void OnSecondAppStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This class checks to make sure that only one instance of
|
/// 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.
|
/// running as Administrator, can activate it with command line arguments.
|
||||||
/// For most apps, this will not be much of an issue.
|
/// For most apps, this will not be much of an issue.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static class SingleInstance<TApplication>
|
public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
|
||||||
where TApplication: Application , ISingleInstanceApp
|
|
||||||
|
|
||||||
{
|
{
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
|
|
@ -39,11 +37,12 @@ namespace Flow.Launcher.Helper
|
||||||
/// Suffix to the channel name.
|
/// Suffix to the channel name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
|
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
|
||||||
|
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Application mutex.
|
/// Application mutex.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static Mutex singleInstanceMutex;
|
internal static Mutex SingleInstanceMutex { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
@ -54,24 +53,23 @@ namespace Flow.Launcher.Helper
|
||||||
/// If not, activates the first instance.
|
/// If not, activates the first instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>True if this is the first instance of the application.</returns>
|
/// <returns>True if this is the first instance of the application.</returns>
|
||||||
public static bool InitializeAsFirstInstance( string uniqueName )
|
public static bool InitializeAsFirstInstance()
|
||||||
{
|
{
|
||||||
// Build unique application Id and the IPC channel name.
|
// Build unique application Id and the IPC channel name.
|
||||||
string applicationIdentifier = uniqueName + Environment.UserName;
|
string applicationIdentifier = InstanceMutexName + Environment.UserName;
|
||||||
|
|
||||||
string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
||||||
|
|
||||||
// Create mutex based on unique application Id to check if this is the first instance of the application.
|
// Create mutex based on unique application Id to check if this is the first instance of the application.
|
||||||
bool firstInstance;
|
SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance);
|
||||||
singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
|
|
||||||
if (firstInstance)
|
if (firstInstance)
|
||||||
{
|
{
|
||||||
_ = CreateRemoteService(channelName);
|
_ = CreateRemoteServiceAsync(channelName);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_ = SignalFirstInstance(channelName);
|
_ = SignalFirstInstanceAsync(channelName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +79,7 @@ namespace Flow.Launcher.Helper
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Cleanup()
|
public static void Cleanup()
|
||||||
{
|
{
|
||||||
singleInstanceMutex?.ReleaseMutex();
|
SingleInstanceMutex?.ReleaseMutex();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -93,22 +91,19 @@ namespace Flow.Launcher.Helper
|
||||||
/// Once receives signal from client, will activate first instance.
|
/// Once receives signal from client, will activate first instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channelName">Application's IPC channel name.</param>
|
/// <param name="channelName">Application's IPC channel name.</param>
|
||||||
private static async Task CreateRemoteService(string channelName)
|
private static async Task CreateRemoteServiceAsync(string channelName)
|
||||||
{
|
{
|
||||||
using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In))
|
using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In);
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
while(true)
|
// Wait for connection to the pipe
|
||||||
{
|
await pipeServer.WaitForConnectionAsync();
|
||||||
// Wait for connection to the pipe
|
|
||||||
await pipeServer.WaitForConnectionAsync();
|
// Do an asynchronous call to ActivateFirstInstance function
|
||||||
if (Application.Current != null)
|
Application.Current?.Dispatcher.Invoke(ActivateFirstInstance);
|
||||||
{
|
|
||||||
// Do an asynchronous call to ActivateFirstInstance function
|
// Disconect client
|
||||||
Application.Current.Dispatcher.Invoke(ActivateFirstInstance);
|
pipeServer.Disconnect();
|
||||||
}
|
|
||||||
// Disconect client
|
|
||||||
pipeServer.Disconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,25 +114,13 @@ namespace Flow.Launcher.Helper
|
||||||
/// <param name="args">
|
/// <param name="args">
|
||||||
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
|
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
|
||||||
/// </param>
|
/// </param>
|
||||||
private static async Task SignalFirstInstance(string channelName)
|
private static async Task SignalFirstInstanceAsync(string channelName)
|
||||||
{
|
{
|
||||||
// Create a client pipe connected to server
|
// Create a client pipe connected to server
|
||||||
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out))
|
using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out);
|
||||||
{
|
|
||||||
// Connect to the available pipe
|
|
||||||
await pipeClient.ConnectAsync(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
// Connect to the available pipe
|
||||||
/// Callback for activating first instance of the application.
|
await pipeClient.ConnectAsync(0);
|
||||||
/// </summary>
|
|
||||||
/// <param name="arg">Callback argument.</param>
|
|
||||||
/// <returns>Always null.</returns>
|
|
||||||
private static object ActivateFirstInstanceCallback(object o)
|
|
||||||
{
|
|
||||||
ActivateFirstInstance();
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
AllowDrop="True"
|
AllowDrop="True"
|
||||||
AllowsTransparency="True"
|
AllowsTransparency="True"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
|
Closed="OnClosed"
|
||||||
Closing="OnClosing"
|
Closing="OnClosing"
|
||||||
Deactivated="OnDeactivated"
|
Deactivated="OnDeactivated"
|
||||||
Icon="Images/app.png"
|
Icon="Images/app.png"
|
||||||
|
|
@ -215,7 +216,7 @@
|
||||||
|
|
||||||
<Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}">
|
<Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}">
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<Grid>
|
<Grid x:Name="QueryBoxArea">
|
||||||
<Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}">
|
<Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextBox
|
<TextBox
|
||||||
|
|
@ -338,7 +339,7 @@
|
||||||
Y2="0" />
|
Y2="0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid ClipToBounds="True">
|
<Grid x:Name="MiddleSeparatorArea" ClipToBounds="True">
|
||||||
<ContentControl>
|
<ContentControl>
|
||||||
<ContentControl.Style>
|
<ContentControl.Style>
|
||||||
<Style TargetType="ContentControl">
|
<Style TargetType="ContentControl">
|
||||||
|
|
@ -378,7 +379,7 @@
|
||||||
</ContentControl>
|
</ContentControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Border Style="{DynamicResource WindowRadius}">
|
<Border x:Name="ResultPreviewAreaBoarder" Style="{DynamicResource WindowRadius}">
|
||||||
<Border.Clip>
|
<Border.Clip>
|
||||||
<MultiBinding Converter="{StaticResource BorderClipConverter}">
|
<MultiBinding Converter="{StaticResource BorderClipConverter}">
|
||||||
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
|
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
|
||||||
|
|
@ -386,12 +387,14 @@
|
||||||
<Binding Path="CornerRadius" RelativeSource="{RelativeSource Self}" />
|
<Binding Path="CornerRadius" RelativeSource="{RelativeSource Self}" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</Border.Clip>
|
</Border.Clip>
|
||||||
<Grid>
|
|
||||||
|
<Grid x:Name="ResultPreviewArea">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" MinWidth="80" />
|
<ColumnDefinition Width="*" MinWidth="80" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="0.85*" MinWidth="244" />
|
<ColumnDefinition Width="0.85*" MinWidth="244" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
x:Name="ResultArea"
|
x:Name="ResultArea"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
|
|
@ -418,7 +421,9 @@
|
||||||
RightClickResultCommand="{Binding RightClickResultCommand}" />
|
RightClickResultCommand="{Binding RightClickResultCommand}" />
|
||||||
</ContentControl>
|
</ContentControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<GridSplitter
|
<GridSplitter
|
||||||
|
x:Name="PreviewMiddleSeparator"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="0"
|
Margin="0"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
|
|
@ -432,6 +437,7 @@
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</GridSplitter.Template>
|
</GridSplitter.Template>
|
||||||
</GridSplitter>
|
</GridSplitter>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
x:Name="Preview"
|
x:Name="Preview"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
|
|
@ -441,7 +447,7 @@
|
||||||
<Border
|
<Border
|
||||||
MinHeight="380"
|
MinHeight="380"
|
||||||
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
|
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
|
||||||
DataContext="{Binding SelectedItem, ElementName=ResultListBox}"
|
DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
|
||||||
Visibility="{Binding ShowDefaultPreview}">
|
Visibility="{Binding ShowDefaultPreview}">
|
||||||
<Grid
|
<Grid
|
||||||
Margin="0 0 10 5"
|
Margin="0 0 10 5"
|
||||||
|
|
@ -518,7 +524,7 @@
|
||||||
MaxHeight="{Binding ElementName=ResultListBox, Path=ActualHeight}"
|
MaxHeight="{Binding ElementName=ResultListBox, Path=ActualHeight}"
|
||||||
Padding="0 0 10 10"
|
Padding="0 0 10 10"
|
||||||
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
|
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
|
||||||
DataContext="{Binding SelectedItem, ElementName=ResultListBox}"
|
DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
|
||||||
Visibility="{Binding ShowCustomizedPreview}">
|
Visibility="{Binding ShowCustomizedPreview}">
|
||||||
<ContentControl Content="{Binding Result.PreviewPanel.Value}" />
|
<ContentControl Content="{Binding Result.PreviewPanel.Value}" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ using Screen = System.Windows.Forms.Screen;
|
||||||
|
|
||||||
namespace Flow.Launcher
|
namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class MainWindow
|
public partial class MainWindow : IDisposable
|
||||||
{
|
{
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
|
|
@ -42,17 +42,20 @@ namespace Flow.Launcher
|
||||||
private NotifyIcon _notifyIcon;
|
private NotifyIcon _notifyIcon;
|
||||||
|
|
||||||
// Window Context Menu
|
// Window Context Menu
|
||||||
private readonly ContextMenu contextMenu = new();
|
private readonly ContextMenu _contextMenu = new();
|
||||||
private readonly MainViewModel _viewModel;
|
private readonly MainViewModel _viewModel;
|
||||||
|
|
||||||
// Window Event : Key Event
|
// Window Event: Close Event
|
||||||
private bool isArrowKeyPressed = false;
|
private bool _canClose = false;
|
||||||
|
// Window Event: Key Event
|
||||||
|
private bool _isArrowKeyPressed = false;
|
||||||
|
|
||||||
// Window Sound Effects
|
// Window Sound Effects
|
||||||
private MediaPlayer animationSoundWMP;
|
private MediaPlayer animationSoundWMP;
|
||||||
private SoundPlayer animationSoundWPF;
|
private SoundPlayer animationSoundWPF;
|
||||||
|
|
||||||
// Window WndProc
|
// Window WndProc
|
||||||
|
private HwndSource _hwndSource;
|
||||||
private int _initialWidth;
|
private int _initialWidth;
|
||||||
private int _initialHeight;
|
private int _initialHeight;
|
||||||
|
|
||||||
|
|
@ -64,6 +67,9 @@ namespace Flow.Launcher
|
||||||
// Search Delay
|
// Search Delay
|
||||||
private IDisposable _reactiveSubscription;
|
private IDisposable _reactiveSubscription;
|
||||||
|
|
||||||
|
// IDisposable
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructor
|
#region Constructor
|
||||||
|
|
@ -91,8 +97,8 @@ namespace Flow.Launcher
|
||||||
private void OnSourceInitialized(object sender, EventArgs e)
|
private void OnSourceInitialized(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var handle = Win32Helper.GetWindowHandle(this, true);
|
var handle = Win32Helper.GetWindowHandle(this, true);
|
||||||
var win = HwndSource.FromHwnd(handle);
|
_hwndSource = HwndSource.FromHwnd(handle);
|
||||||
win.AddHook(WndProc);
|
_hwndSource.AddHook(WndProc);
|
||||||
Win32Helper.HideFromAltTab(this);
|
Win32Helper.HideFromAltTab(this);
|
||||||
Win32Helper.DisableControlBox(this);
|
Win32Helper.DisableControlBox(this);
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +113,9 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
_settings.FirstLaunch = false;
|
_settings.FirstLaunch = false;
|
||||||
App.API.SaveAppAllSettings();
|
App.API.SaveAppAllSettings();
|
||||||
|
/* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */
|
||||||
|
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
|
||||||
|
_settings.BackdropType = BackdropTypes.Acrylic;
|
||||||
var WelcomeWindow = new WelcomeWindow();
|
var WelcomeWindow = new WelcomeWindow();
|
||||||
WelcomeWindow.Show();
|
WelcomeWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +158,11 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
// Since the default main window visibility is visible, so we need set focus during startup
|
// Since the default main window visibility is visible, so we need set focus during startup
|
||||||
QueryTextBox.Focus();
|
QueryTextBox.Focus();
|
||||||
|
|
||||||
|
// Set the initial state of the QueryTextBoxCursorMovedToEnd property
|
||||||
|
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
|
||||||
|
_viewModel.QueryTextCursorMovedToEnd = false;
|
||||||
|
|
||||||
// View model property changed event
|
// View model property changed event
|
||||||
_viewModel.PropertyChanged += (o, e) =>
|
_viewModel.PropertyChanged += (o, e) =>
|
||||||
{
|
{
|
||||||
|
|
@ -227,15 +240,15 @@ namespace Flow.Launcher
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정)
|
// QueryTextBox.Text change detection (modified to only work when character count is 1 or higher)
|
||||||
QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility();
|
QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility();
|
||||||
|
|
||||||
// ✅ ContextMenu.Visibility 변경 감지
|
// Detecting ContextMenu.Visibility changes
|
||||||
DependencyPropertyDescriptor
|
DependencyPropertyDescriptor
|
||||||
.FromProperty(VisibilityProperty, typeof(ContextMenu))
|
.FromProperty(VisibilityProperty, typeof(ContextMenu))
|
||||||
.AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility());
|
.AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility());
|
||||||
|
|
||||||
// ✅ History.Visibility 변경 감지
|
// Detect History.Visibility changes
|
||||||
DependencyPropertyDescriptor
|
DependencyPropertyDescriptor
|
||||||
.FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정
|
.FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정
|
||||||
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
|
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
|
||||||
|
|
@ -243,18 +256,37 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
private async void OnClosing(object sender, CancelEventArgs e)
|
private async void OnClosing(object sender, CancelEventArgs e)
|
||||||
{
|
{
|
||||||
_notifyIcon.Visible = false;
|
if (!_canClose)
|
||||||
App.API.SaveAppAllSettings();
|
{
|
||||||
e.Cancel = true;
|
_notifyIcon.Visible = false;
|
||||||
await PluginManager.DisposePluginsAsync();
|
App.API.SaveAppAllSettings();
|
||||||
Notification.Uninstall();
|
e.Cancel = true;
|
||||||
Environment.Exit(0);
|
await PluginManager.DisposePluginsAsync();
|
||||||
|
Notification.Uninstall();
|
||||||
|
// After plugins are all disposed, we can close the main window
|
||||||
|
_canClose = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClosed(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_hwndSource.RemoveHook(WndProc);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
_hwndSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLocationChanged(object sender, EventArgs e)
|
private void OnLocationChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_animating)
|
if (_animating) return;
|
||||||
return;
|
|
||||||
if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation)
|
if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation)
|
||||||
{
|
{
|
||||||
_settings.WindowLeft = Left;
|
_settings.WindowLeft = Left;
|
||||||
|
|
@ -292,12 +324,12 @@ namespace Flow.Launcher
|
||||||
switch (e.Key)
|
switch (e.Key)
|
||||||
{
|
{
|
||||||
case Key.Down:
|
case Key.Down:
|
||||||
isArrowKeyPressed = true;
|
_isArrowKeyPressed = true;
|
||||||
_viewModel.SelectNextItemCommand.Execute(null);
|
_viewModel.SelectNextItemCommand.Execute(null);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
break;
|
break;
|
||||||
case Key.Up:
|
case Key.Up:
|
||||||
isArrowKeyPressed = true;
|
_isArrowKeyPressed = true;
|
||||||
_viewModel.SelectPrevItemCommand.Execute(null);
|
_viewModel.SelectPrevItemCommand.Execute(null);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
break;
|
break;
|
||||||
|
|
@ -310,7 +342,7 @@ namespace Flow.Launcher
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
break;
|
break;
|
||||||
case Key.Right:
|
case Key.Right:
|
||||||
if (_viewModel.SelectedIsFromQueryResults()
|
if (_viewModel.QueryResultsSelected()
|
||||||
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length
|
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length
|
||||||
&& !string.IsNullOrEmpty(QueryTextBox.Text))
|
&& !string.IsNullOrEmpty(QueryTextBox.Text))
|
||||||
{
|
{
|
||||||
|
|
@ -320,7 +352,7 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case Key.Left:
|
case Key.Left:
|
||||||
if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0)
|
if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0)
|
||||||
{
|
{
|
||||||
_viewModel.EscCommand.Execute(null);
|
_viewModel.EscCommand.Execute(null);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|
@ -330,7 +362,7 @@ namespace Flow.Launcher
|
||||||
case Key.Back:
|
case Key.Back:
|
||||||
if (specialKeyState.CtrlPressed)
|
if (specialKeyState.CtrlPressed)
|
||||||
{
|
{
|
||||||
if (_viewModel.SelectedIsFromQueryResults()
|
if (_viewModel.QueryResultsSelected()
|
||||||
&& QueryTextBox.Text.Length > 0
|
&& QueryTextBox.Text.Length > 0
|
||||||
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
|
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
|
||||||
{
|
{
|
||||||
|
|
@ -355,13 +387,13 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
if (e.Key == Key.Up || e.Key == Key.Down)
|
if (e.Key == Key.Up || e.Key == Key.Down)
|
||||||
{
|
{
|
||||||
isArrowKeyPressed = false;
|
_isArrowKeyPressed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (isArrowKeyPressed)
|
if (_isArrowKeyPressed)
|
||||||
{
|
{
|
||||||
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
|
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
|
||||||
}
|
}
|
||||||
|
|
@ -531,11 +563,11 @@ namespace Flow.Launcher
|
||||||
gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip");
|
gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip");
|
||||||
positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip");
|
positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip");
|
||||||
|
|
||||||
contextMenu.Items.Add(open);
|
_contextMenu.Items.Add(open);
|
||||||
contextMenu.Items.Add(gamemode);
|
_contextMenu.Items.Add(gamemode);
|
||||||
contextMenu.Items.Add(positionreset);
|
_contextMenu.Items.Add(positionreset);
|
||||||
contextMenu.Items.Add(settings);
|
_contextMenu.Items.Add(settings);
|
||||||
contextMenu.Items.Add(exit);
|
_contextMenu.Items.Add(exit);
|
||||||
|
|
||||||
_notifyIcon.MouseClick += (o, e) =>
|
_notifyIcon.MouseClick += (o, e) =>
|
||||||
{
|
{
|
||||||
|
|
@ -546,14 +578,14 @@ namespace Flow.Launcher
|
||||||
break;
|
break;
|
||||||
case MouseButtons.Right:
|
case MouseButtons.Right:
|
||||||
|
|
||||||
contextMenu.IsOpen = true;
|
_contextMenu.IsOpen = true;
|
||||||
// Get context menu handle and bring it to the foreground
|
// Get context menu handle and bring it to the foreground
|
||||||
if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource)
|
if (PresentationSource.FromVisual(_contextMenu) is HwndSource hwndSource)
|
||||||
{
|
{
|
||||||
Win32Helper.SetForegroundWindow(hwndSource.Handle);
|
Win32Helper.SetForegroundWindow(hwndSource.Handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMenu.Focus();
|
_contextMenu.Focus();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -561,7 +593,7 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
private void UpdateNotifyIconText()
|
private void UpdateNotifyIconText()
|
||||||
{
|
{
|
||||||
var menu = contextMenu;
|
var menu = _contextMenu;
|
||||||
((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") +
|
((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") +
|
||||||
" (" + _settings.Hotkey + ")";
|
" (" + _settings.Hotkey + ")";
|
||||||
((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode");
|
((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode");
|
||||||
|
|
@ -757,7 +789,7 @@ namespace Flow.Launcher
|
||||||
if (_animating)
|
if (_animating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
isArrowKeyPressed = true;
|
_isArrowKeyPressed = true;
|
||||||
_animating = true;
|
_animating = true;
|
||||||
UpdatePosition(false);
|
UpdatePosition(false);
|
||||||
|
|
||||||
|
|
@ -835,7 +867,7 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
clocksb.Completed += (_, _) => _animating = false;
|
clocksb.Completed += (_, _) => _animating = false;
|
||||||
_settings.WindowLeft = Left;
|
_settings.WindowLeft = Left;
|
||||||
isArrowKeyPressed = false;
|
_isArrowKeyPressed = false;
|
||||||
|
|
||||||
if (QueryTextBox.Text.Length == 0)
|
if (QueryTextBox.Text.Length == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -1048,5 +1080,31 @@ namespace Flow.Launcher
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_hwndSource?.Dispose();
|
||||||
|
_notifyIcon?.Dispose();
|
||||||
|
_reactiveSubscription?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,8 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
||||||
|
|
||||||
Settings.BackdropType = value;
|
Settings.BackdropType = value;
|
||||||
|
|
||||||
|
// Can only apply blur because drop shadow effect is not supported with backdrop
|
||||||
|
// So drop shadow effect has been disabled
|
||||||
_ = _theme.SetBlurForWindowAsync();
|
_ = _theme.SetBlurForWindowAsync();
|
||||||
|
|
||||||
OnPropertyChanged(nameof(IsDropShadowEnabled));
|
OnPropertyChanged(nameof(IsDropShadowEnabled));
|
||||||
|
|
@ -342,7 +344,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
Settings.QueryBoxFont = value.ToString();
|
Settings.QueryBoxFont = value.ToString();
|
||||||
_theme.ChangeTheme();
|
_theme.UpdateFonts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,7 +366,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
||||||
Settings.QueryBoxFontStretch = value.Stretch.ToString();
|
Settings.QueryBoxFontStretch = value.Stretch.ToString();
|
||||||
Settings.QueryBoxFontWeight = value.Weight.ToString();
|
Settings.QueryBoxFontWeight = value.Weight.ToString();
|
||||||
Settings.QueryBoxFontStyle = value.Style.ToString();
|
Settings.QueryBoxFontStyle = value.Style.ToString();
|
||||||
_theme.ChangeTheme();
|
_theme.UpdateFonts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,7 +388,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
Settings.ResultFont = value.ToString();
|
Settings.ResultFont = value.ToString();
|
||||||
_theme.ChangeTheme();
|
_theme.UpdateFonts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,7 +410,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
||||||
Settings.ResultFontStretch = value.Stretch.ToString();
|
Settings.ResultFontStretch = value.Stretch.ToString();
|
||||||
Settings.ResultFontWeight = value.Weight.ToString();
|
Settings.ResultFontWeight = value.Weight.ToString();
|
||||||
Settings.ResultFontStyle = value.Style.ToString();
|
Settings.ResultFontStyle = value.Style.ToString();
|
||||||
_theme.ChangeTheme();
|
_theme.UpdateFonts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,7 +434,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
Settings.ResultSubFont = value.ToString();
|
Settings.ResultSubFont = value.ToString();
|
||||||
_theme.ChangeTheme();
|
_theme.UpdateFonts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,7 +455,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
||||||
Settings.ResultSubFontStretch = value.Stretch.ToString();
|
Settings.ResultSubFontStretch = value.Stretch.ToString();
|
||||||
Settings.ResultSubFontWeight = value.Weight.ToString();
|
Settings.ResultSubFontWeight = value.Weight.ToString();
|
||||||
Settings.ResultSubFontStyle = value.Style.ToString();
|
Settings.ResultSubFontStyle = value.Style.ToString();
|
||||||
_theme.ChangeTheme();
|
_theme.UpdateFonts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
<Style x:Key="BaseQueryBoxStyle" TargetType="{x:Type TextBox}">
|
<Style x:Key="BaseQueryBoxStyle" TargetType="{x:Type TextBox}">
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="FontSize" Value="28" />
|
<Setter Property="FontSize" Value="28" />
|
||||||
<Setter Property="FontWeight" Value="Regular" />
|
|
||||||
<Setter Property="Margin" Value="16 7 0 7" />
|
<Setter Property="Margin" Value="16 7 0 7" />
|
||||||
<Setter Property="Padding" Value="0 0 68 0" />
|
<Setter Property="Padding" Value="0 0 68 0" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
|
@ -181,12 +180,10 @@
|
||||||
<Style x:Key="BaseItemTitleStyle" TargetType="{x:Type TextBlock}">
|
<Style x:Key="BaseItemTitleStyle" TargetType="{x:Type TextBlock}">
|
||||||
<Setter Property="Foreground" Value="#FFFFF8" />
|
<Setter Property="Foreground" Value="#FFFFF8" />
|
||||||
<Setter Property="FontSize" Value="16" />
|
<Setter Property="FontSize" Value="16" />
|
||||||
<Setter Property="FontWeight" Value="Medium" />
|
|
||||||
</Style>
|
</Style>
|
||||||
<Style x:Key="BaseItemSubTitleStyle" TargetType="{x:Type TextBlock}">
|
<Style x:Key="BaseItemSubTitleStyle" TargetType="{x:Type TextBlock}">
|
||||||
<Setter Property="Foreground" Value="#D9D9D4" />
|
<Setter Property="Foreground" Value="#D9D9D4" />
|
||||||
<Setter Property="FontSize" Value="13" />
|
<Setter Property="FontSize" Value="13" />
|
||||||
<Setter Property="FontWeight" Value="Normal" />
|
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding ElementName=SubTitle, UpdateSourceTrigger=PropertyChanged, Path=Text.Length}" Value="0">
|
<DataTrigger Binding="{Binding ElementName=SubTitle, UpdateSourceTrigger=PropertyChanged, Path=Text.Length}" Value="0">
|
||||||
<Setter Property="Height" Value="0" />
|
<Setter Property="Height" Value="0" />
|
||||||
|
|
@ -218,7 +215,6 @@
|
||||||
<Style x:Key="BaseItemTitleSelectedStyle" TargetType="{x:Type TextBlock}">
|
<Style x:Key="BaseItemTitleSelectedStyle" TargetType="{x:Type TextBlock}">
|
||||||
<Setter Property="Foreground" Value="#FFFFF8" />
|
<Setter Property="Foreground" Value="#FFFFF8" />
|
||||||
<Setter Property="FontSize" Value="16" />
|
<Setter Property="FontSize" Value="16" />
|
||||||
<Setter Property="FontWeight" Value="Normal" />
|
|
||||||
</Style>
|
</Style>
|
||||||
<Style x:Key="BaseItemSubTitleSelectedStyle" TargetType="{x:Type TextBlock}">
|
<Style x:Key="BaseItemSubTitleSelectedStyle" TargetType="{x:Type TextBlock}">
|
||||||
<Setter Property="Foreground" Value="#D9D9D4" />
|
<Setter Property="Foreground" Value="#D9D9D4" />
|
||||||
|
|
@ -394,6 +390,7 @@
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<MultiDataTrigger>
|
<MultiDataTrigger>
|
||||||
<MultiDataTrigger.Conditions>
|
<MultiDataTrigger.Conditions>
|
||||||
|
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
|
||||||
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
|
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
|
||||||
</MultiDataTrigger.Conditions>
|
</MultiDataTrigger.Conditions>
|
||||||
<MultiDataTrigger.Setters>
|
<MultiDataTrigger.Setters>
|
||||||
|
|
@ -435,12 +432,12 @@
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style
|
<Style
|
||||||
x:Key="PreviewBorderStyle"
|
x:Key="PreviewBorderStyle"
|
||||||
BasedOn="{StaticResource BasePreviewBorderStyle}"
|
BasedOn="{StaticResource BasePreviewBorderStyle}"
|
||||||
TargetType="{x:Type Border}">
|
TargetType="{x:Type Border}">
|
||||||
<Setter Property="BorderBrush" Value="Gray" />
|
<Setter Property="BorderBrush" Value="Gray" />
|
||||||
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="PreviewArea" TargetType="{x:Type Grid}">
|
<Style x:Key="PreviewArea" TargetType="{x:Type Grid}">
|
||||||
|
|
@ -450,8 +447,8 @@
|
||||||
<MultiDataTrigger.Conditions>
|
<MultiDataTrigger.Conditions>
|
||||||
<!--
|
<!--
|
||||||
<Condition Binding="{Binding ElementName=ResultListBox, Path=Visibility}" Value="Collapsed" />
|
<Condition Binding="{Binding ElementName=ResultListBox, Path=Visibility}" Value="Collapsed" />
|
||||||
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />
|
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />-->
|
||||||
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />-->
|
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
|
||||||
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
|
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
|
||||||
</MultiDataTrigger.Conditions>
|
</MultiDataTrigger.Conditions>
|
||||||
<MultiDataTrigger.Setters>
|
<MultiDataTrigger.Setters>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style
|
<Style
|
||||||
x:Key="SeparatorStyle"
|
x:Key="SeparatorStyle"
|
||||||
BasedOn="{StaticResource BaseSeparatorStyle}"
|
BasedOn="{StaticResource BaseSeparatorStyle}"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ using Microsoft.VisualStudio.Threading;
|
||||||
|
|
||||||
namespace Flow.Launcher.ViewModel
|
namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
public partial class MainViewModel : BaseModel, ISavable
|
public partial class MainViewModel : BaseModel, ISavable, IDisposable
|
||||||
{
|
{
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
|
|
@ -49,6 +49,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
|
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
|
||||||
private Task _resultsViewUpdateTask;
|
private Task _resultsViewUpdateTask;
|
||||||
|
|
||||||
|
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructor
|
#region Constructor
|
||||||
|
|
@ -162,13 +164,26 @@ namespace Flow.Launcher.ViewModel
|
||||||
switch (args.PropertyName)
|
switch (args.PropertyName)
|
||||||
{
|
{
|
||||||
case nameof(Results.SelectedItem):
|
case nameof(Results.SelectedItem):
|
||||||
UpdatePreview();
|
_selectedItemFromQueryResults = true;
|
||||||
|
PreviewSelectedItem = Results.SelectedItem;
|
||||||
|
_ = UpdatePreviewAsync();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
History.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
switch (args.PropertyName)
|
||||||
|
{
|
||||||
|
case nameof(History.SelectedItem):
|
||||||
|
_selectedItemFromQueryResults = false;
|
||||||
|
PreviewSelectedItem = History.SelectedItem;
|
||||||
|
_ = UpdatePreviewAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
RegisterViewUpdate();
|
RegisterViewUpdate();
|
||||||
RegisterResultsUpdatedEvent();
|
|
||||||
_ = RegisterClockAndDateUpdateAsync();
|
_ = RegisterClockAndDateUpdateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,7 +228,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterResultsUpdatedEvent()
|
public void RegisterResultsUpdatedEvent()
|
||||||
{
|
{
|
||||||
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
|
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
|
||||||
{
|
{
|
||||||
|
|
@ -266,7 +281,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void LoadHistory()
|
private void LoadHistory()
|
||||||
{
|
{
|
||||||
if (SelectedIsFromQueryResults())
|
if (QueryResultsSelected())
|
||||||
{
|
{
|
||||||
SelectedResults = History;
|
SelectedResults = History;
|
||||||
History.SelectedIndex = _history.Items.Count - 1;
|
History.SelectedIndex = _history.Items.Count - 1;
|
||||||
|
|
@ -280,7 +295,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public void ReQuery()
|
public void ReQuery()
|
||||||
{
|
{
|
||||||
if (SelectedIsFromQueryResults())
|
if (QueryResultsSelected())
|
||||||
{
|
{
|
||||||
_ = QueryResultsAsync(false, isReQuery: true);
|
_ = QueryResultsAsync(false, isReQuery: true);
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +336,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void LoadContextMenu()
|
private void LoadContextMenu()
|
||||||
{
|
{
|
||||||
if (SelectedIsFromQueryResults())
|
if (QueryResultsSelected())
|
||||||
{
|
{
|
||||||
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
|
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
|
||||||
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
|
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
|
||||||
|
|
@ -351,7 +366,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
private void AutocompleteQuery()
|
private void AutocompleteQuery()
|
||||||
{
|
{
|
||||||
var result = SelectedResults.SelectedItem?.Result;
|
var result = SelectedResults.SelectedItem?.Result;
|
||||||
if (result != null && SelectedIsFromQueryResults()) // SelectedItem returns null if selection is empty.
|
if (result != null && QueryResultsSelected()) // SelectedItem returns null if selection is empty.
|
||||||
{
|
{
|
||||||
var autoCompleteText = result.Title;
|
var autoCompleteText = result.Title;
|
||||||
|
|
||||||
|
|
@ -403,7 +418,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
})
|
})
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (SelectedIsFromQueryResults())
|
if (QueryResultsSelected())
|
||||||
{
|
{
|
||||||
_userSelectedRecord.Add(result);
|
_userSelectedRecord.Add(result);
|
||||||
// origin query is null when user select the context menu item directly of one item from query list
|
// origin query is null when user select the context menu item directly of one item from query list
|
||||||
|
|
@ -482,7 +497,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
if (_history.Items.Count > 0
|
if (_history.Items.Count > 0
|
||||||
&& QueryText == string.Empty
|
&& QueryText == string.Empty
|
||||||
&& SelectedIsFromQueryResults())
|
&& QueryResultsSelected())
|
||||||
{
|
{
|
||||||
lastHistoryIndex = 1;
|
lastHistoryIndex = 1;
|
||||||
ReverseHistory();
|
ReverseHistory();
|
||||||
|
|
@ -502,7 +517,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Esc()
|
private void Esc()
|
||||||
{
|
{
|
||||||
if (!SelectedIsFromQueryResults())
|
if (!QueryResultsSelected())
|
||||||
{
|
{
|
||||||
SelectedResults = Results;
|
SelectedResults = Results;
|
||||||
}
|
}
|
||||||
|
|
@ -514,7 +529,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
public void BackToQueryResults()
|
public void BackToQueryResults()
|
||||||
{
|
{
|
||||||
if (!SelectedIsFromQueryResults())
|
if (!QueryResultsSelected())
|
||||||
{
|
{
|
||||||
SelectedResults = Results;
|
SelectedResults = Results;
|
||||||
}
|
}
|
||||||
|
|
@ -645,13 +660,16 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
private ResultsViewModel SelectedResults
|
private ResultsViewModel SelectedResults
|
||||||
{
|
{
|
||||||
get { return _selectedResults; }
|
get => _selectedResults;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
var isReturningFromQueryResults = QueryResultsSelected();
|
||||||
var isReturningFromContextMenu = ContextMenuSelected();
|
var isReturningFromContextMenu = ContextMenuSelected();
|
||||||
|
var isReturningFromHistory = HistorySelected();
|
||||||
_selectedResults = value;
|
_selectedResults = value;
|
||||||
if (SelectedIsFromQueryResults())
|
if (QueryResultsSelected())
|
||||||
{
|
{
|
||||||
|
Results.Visibility = Visibility.Visible;
|
||||||
ContextMenu.Visibility = Visibility.Collapsed;
|
ContextMenu.Visibility = Visibility.Collapsed;
|
||||||
History.Visibility = Visibility.Collapsed;
|
History.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
|
@ -669,10 +687,27 @@ namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
ChangeQueryText(_queryTextBeforeLeaveResults);
|
ChangeQueryText(_queryTextBeforeLeaveResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are returning from history and we have not set select item yet,
|
||||||
|
// we need to clear the preview selected item
|
||||||
|
if (isReturningFromHistory && _selectedItemFromQueryResults.HasValue && (!_selectedItemFromQueryResults.Value))
|
||||||
|
{
|
||||||
|
PreviewSelectedItem = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Results.Visibility = Visibility.Collapsed;
|
Results.Visibility = Visibility.Collapsed;
|
||||||
|
if (HistorySelected())
|
||||||
|
{
|
||||||
|
ContextMenu.Visibility = Visibility.Collapsed;
|
||||||
|
History.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ContextMenu.Visibility = Visibility.Visible;
|
||||||
|
History.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
_queryTextBeforeLeaveResults = QueryText;
|
_queryTextBeforeLeaveResults = QueryText;
|
||||||
|
|
||||||
// Because of Fody's optimization
|
// Because of Fody's optimization
|
||||||
|
|
@ -681,6 +716,16 @@ namespace Flow.Launcher.ViewModel
|
||||||
// http://stackoverflow.com/posts/25895769/revisions
|
// http://stackoverflow.com/posts/25895769/revisions
|
||||||
QueryText = string.Empty;
|
QueryText = string.Empty;
|
||||||
Query(false);
|
Query(false);
|
||||||
|
|
||||||
|
if (HistorySelected())
|
||||||
|
{
|
||||||
|
// If we are returning from query results and we have not set select item yet,
|
||||||
|
// we need to clear the preview selected item
|
||||||
|
if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value)
|
||||||
|
{
|
||||||
|
PreviewSelectedItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectedResults.Visibility = Visibility.Visible;
|
_selectedResults.Visibility = Visibility.Visible;
|
||||||
|
|
@ -780,6 +825,22 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
#region Preview
|
#region Preview
|
||||||
|
|
||||||
|
private static readonly int ResultAreaColumnPreviewShown = 1;
|
||||||
|
private static readonly int ResultAreaColumnPreviewHidden = 3;
|
||||||
|
|
||||||
|
private bool? _selectedItemFromQueryResults;
|
||||||
|
|
||||||
|
private ResultViewModel _previewSelectedItem;
|
||||||
|
public ResultViewModel PreviewSelectedItem
|
||||||
|
{
|
||||||
|
get => _previewSelectedItem;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_previewSelectedItem = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool InternalPreviewVisible
|
public bool InternalPreviewVisible
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -798,18 +859,14 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly int ResultAreaColumnPreviewShown = 1;
|
|
||||||
|
|
||||||
private static readonly int ResultAreaColumnPreviewHidden = 3;
|
|
||||||
|
|
||||||
public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown;
|
public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown;
|
||||||
|
|
||||||
// This is not a reliable indicator of whether external preview is visible due to the
|
// This is not a reliable indicator of whether external preview is visible due to the
|
||||||
// ability of manually closing/exiting the external preview program which, does not inform flow that
|
// ability of manually closing/exiting the external preview program which, does not inform flow that
|
||||||
// preview is no longer available.
|
// preview is no longer available.
|
||||||
public bool ExternalPreviewVisible { get; set; } = false;
|
public bool ExternalPreviewVisible { get; private set; }
|
||||||
|
|
||||||
private void ShowPreview()
|
private async Task ShowPreviewAsync()
|
||||||
{
|
{
|
||||||
var useExternalPreview = PluginManager.UseExternalPreview();
|
var useExternalPreview = PluginManager.UseExternalPreview();
|
||||||
|
|
||||||
|
|
@ -820,13 +877,15 @@ namespace Flow.Launcher.ViewModel
|
||||||
// Internal preview may still be on when user switches to external
|
// Internal preview may still be on when user switches to external
|
||||||
if (InternalPreviewVisible)
|
if (InternalPreviewVisible)
|
||||||
HideInternalPreview();
|
HideInternalPreview();
|
||||||
OpenExternalPreview(path);
|
|
||||||
|
_ = OpenExternalPreviewAsync(path);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case true
|
case true
|
||||||
when !CanExternalPreviewSelectedResult(out var _):
|
when !CanExternalPreviewSelectedResult(out var _):
|
||||||
if (ExternalPreviewVisible)
|
if (ExternalPreviewVisible)
|
||||||
CloseExternalPreview();
|
await CloseExternalPreviewAsync();
|
||||||
|
|
||||||
ShowInternalPreview();
|
ShowInternalPreview();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -839,7 +898,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
private void HidePreview()
|
private void HidePreview()
|
||||||
{
|
{
|
||||||
if (PluginManager.UseExternalPreview())
|
if (PluginManager.UseExternalPreview())
|
||||||
CloseExternalPreview();
|
_ = CloseExternalPreviewAsync();
|
||||||
|
|
||||||
if (InternalPreviewVisible)
|
if (InternalPreviewVisible)
|
||||||
HideInternalPreview();
|
HideInternalPreview();
|
||||||
|
|
@ -854,31 +913,31 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ShowPreview();
|
_ = ShowPreviewAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenExternalPreview(string path, bool sendFailToast = true)
|
private async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||||
{
|
{
|
||||||
_ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
|
await PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
|
||||||
ExternalPreviewVisible = true;
|
ExternalPreviewVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseExternalPreview()
|
private async Task CloseExternalPreviewAsync()
|
||||||
{
|
{
|
||||||
_ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
|
await PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
|
||||||
ExternalPreviewVisible = false;
|
ExternalPreviewVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SwitchExternalPreview(string path, bool sendFailToast = true)
|
private static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||||
{
|
{
|
||||||
_ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false);
|
await PluginManager.SwitchExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowInternalPreview()
|
private void ShowInternalPreview()
|
||||||
{
|
{
|
||||||
ResultAreaColumn = ResultAreaColumnPreviewShown;
|
ResultAreaColumn = ResultAreaColumnPreviewShown;
|
||||||
Results.SelectedItem?.LoadPreviewImage();
|
PreviewSelectedItem?.LoadPreviewImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HideInternalPreview()
|
private void HideInternalPreview()
|
||||||
|
|
@ -892,20 +951,18 @@ namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
case true
|
case true
|
||||||
when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path):
|
when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path):
|
||||||
OpenExternalPreview(path);
|
_ = OpenExternalPreviewAsync(path);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case true:
|
case true:
|
||||||
ShowInternalPreview();
|
ShowInternalPreview();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false:
|
case false:
|
||||||
HidePreview();
|
HidePreview();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePreview()
|
private async Task UpdatePreviewAsync()
|
||||||
{
|
{
|
||||||
switch (PluginManager.UseExternalPreview())
|
switch (PluginManager.UseExternalPreview())
|
||||||
{
|
{
|
||||||
|
|
@ -913,44 +970,48 @@ namespace Flow.Launcher.ViewModel
|
||||||
when CanExternalPreviewSelectedResult(out var path):
|
when CanExternalPreviewSelectedResult(out var path):
|
||||||
if (ExternalPreviewVisible)
|
if (ExternalPreviewVisible)
|
||||||
{
|
{
|
||||||
SwitchExternalPreview(path, false);
|
_ = SwitchExternalPreviewAsync(path, false);
|
||||||
}
|
}
|
||||||
else if (InternalPreviewVisible)
|
else if (InternalPreviewVisible)
|
||||||
{
|
{
|
||||||
HideInternalPreview();
|
HideInternalPreview();
|
||||||
OpenExternalPreview(path);
|
_ = OpenExternalPreviewAsync(path);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case true
|
case true
|
||||||
when !CanExternalPreviewSelectedResult(out var _):
|
when !CanExternalPreviewSelectedResult(out var _):
|
||||||
if (ExternalPreviewVisible)
|
if (ExternalPreviewVisible)
|
||||||
{
|
{
|
||||||
CloseExternalPreview();
|
await CloseExternalPreviewAsync();
|
||||||
ShowInternalPreview();
|
ShowInternalPreview();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false
|
case false
|
||||||
when InternalPreviewVisible:
|
when InternalPreviewVisible:
|
||||||
Results.SelectedItem?.LoadPreviewImage();
|
PreviewSelectedItem?.LoadPreviewImage();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanExternalPreviewSelectedResult(out string path)
|
private bool CanExternalPreviewSelectedResult(out string path)
|
||||||
{
|
{
|
||||||
path = Results.SelectedItem?.Result?.Preview.FilePath;
|
path = QueryResultsPreviewed() ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty;
|
||||||
return !string.IsNullOrEmpty(path);
|
return !string.IsNullOrEmpty(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool QueryResultsPreviewed()
|
||||||
|
{
|
||||||
|
var previewed = PreviewSelectedItem == Results.SelectedItem;
|
||||||
|
return previewed;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Query
|
#region Query
|
||||||
|
|
||||||
public void Query(bool searchDelay, bool isReQuery = false)
|
private void Query(bool searchDelay, bool isReQuery = false)
|
||||||
{
|
{
|
||||||
if (SelectedIsFromQueryResults())
|
if (QueryResultsSelected())
|
||||||
{
|
{
|
||||||
_ = QueryResultsAsync(searchDelay, isReQuery);
|
_ = QueryResultsAsync(searchDelay, isReQuery);
|
||||||
}
|
}
|
||||||
|
|
@ -1022,6 +1083,11 @@ namespace Flow.Launcher.ViewModel
|
||||||
Title = string.Format(title, h.Query),
|
Title = string.Format(title, h.Query),
|
||||||
SubTitle = string.Format(time, h.ExecutedDateTime),
|
SubTitle = string.Format(time, h.ExecutedDateTime),
|
||||||
IcoPath = "Images\\history.png",
|
IcoPath = "Images\\history.png",
|
||||||
|
Preview = new Result.PreviewInfo
|
||||||
|
{
|
||||||
|
PreviewImagePath = Constant.HistoryIcon,
|
||||||
|
Description = string.Format(time, h.ExecutedDateTime)
|
||||||
|
},
|
||||||
OriginQuery = new Query { RawQuery = h.Query },
|
OriginQuery = new Query { RawQuery = h.Query },
|
||||||
Action = _ =>
|
Action = _ =>
|
||||||
{
|
{
|
||||||
|
|
@ -1048,8 +1114,6 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
|
|
||||||
|
|
||||||
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
|
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
|
||||||
{
|
{
|
||||||
// TODO: Remove debug codes.
|
// TODO: Remove debug codes.
|
||||||
|
|
@ -1347,7 +1411,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool SelectedIsFromQueryResults()
|
internal bool QueryResultsSelected()
|
||||||
{
|
{
|
||||||
var selected = SelectedResults == Results;
|
var selected = SelectedResults == Results;
|
||||||
return selected;
|
return selected;
|
||||||
|
|
@ -1381,6 +1445,18 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if Flow Launcher should ignore any hotkeys
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldIgnoreHotkeys()
|
||||||
|
{
|
||||||
|
return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
public void Show()
|
public void Show()
|
||||||
{
|
{
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
|
@ -1401,6 +1477,11 @@ namespace Flow.Launcher.ViewModel
|
||||||
MainWindowOpacity = 1;
|
MainWindowOpacity = 1;
|
||||||
MainWindowVisibilityStatus = true;
|
MainWindowVisibilityStatus = true;
|
||||||
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
|
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
|
||||||
|
|
||||||
|
if (StartWithEnglishMode)
|
||||||
|
{
|
||||||
|
Win32Helper.SwitchToEnglishKeyboardLayout(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1412,10 +1493,10 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
if (ExternalPreviewVisible)
|
if (ExternalPreviewVisible)
|
||||||
{
|
{
|
||||||
CloseExternalPreview();
|
await CloseExternalPreviewAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SelectedIsFromQueryResults())
|
if (!QueryResultsSelected())
|
||||||
{
|
{
|
||||||
SelectedResults = Results;
|
SelectedResults = Results;
|
||||||
}
|
}
|
||||||
|
|
@ -1425,10 +1506,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
ChangeQueryText(string.Empty);
|
ChangeQueryText(string.Empty);
|
||||||
await Task.Delay(1); // Wait for one frame to ensure UI reflects changes
|
await Task.Delay(1); // Wait for one frame to ensure UI reflects changes
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
Application.Current.Dispatcher.Invoke(Application.Current.MainWindow.UpdateLayout); // Force UI update
|
||||||
{
|
|
||||||
Application.Current.MainWindow.UpdateLayout(); // Force UI update
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (Settings.LastQueryMode)
|
switch (Settings.LastQueryMode)
|
||||||
|
|
@ -1469,7 +1547,12 @@ namespace Flow.Launcher.ViewModel
|
||||||
// 📌 Apply DWM Cloak (Completely hide the window)
|
// 📌 Apply DWM Cloak (Completely hide the window)
|
||||||
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
|
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StartWithEnglishMode)
|
||||||
|
{
|
||||||
|
Win32Helper.RestorePreviousKeyboardLayout();
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(50);
|
await Task.Delay(50);
|
||||||
|
|
||||||
// Update WPF properties
|
// Update WPF properties
|
||||||
|
|
@ -1482,17 +1565,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if Flow Launcher should ignore any hotkeys
|
/// Save history, user selected records and top most records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ShouldIgnoreHotkeys()
|
|
||||||
{
|
|
||||||
return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
_historyItemsStorage.Save();
|
_historyItemsStorage.Save();
|
||||||
|
|
@ -1570,5 +1644,35 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_updateSource?.Dispose();
|
||||||
|
_resultsUpdateChannelWriter?.Complete();
|
||||||
|
if (_resultsViewUpdateTask?.IsCompleted == true)
|
||||||
|
{
|
||||||
|
_resultsViewUpdateTask.Dispose();
|
||||||
|
}
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing.Text;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
|
@ -6,25 +9,20 @@ using Flow.Launcher.Infrastructure.Image;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System.IO;
|
|
||||||
using System.Drawing.Text;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.ViewModel
|
namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
public class ResultViewModel : BaseModel
|
public class ResultViewModel : BaseModel
|
||||||
{
|
{
|
||||||
private static PrivateFontCollection fontCollection = new();
|
private static readonly PrivateFontCollection FontCollection = new();
|
||||||
private static Dictionary<string, string> fonts = new();
|
private static readonly Dictionary<string, string> Fonts = new();
|
||||||
|
|
||||||
public ResultViewModel(Result result, Settings settings)
|
public ResultViewModel(Result result, Settings settings)
|
||||||
{
|
{
|
||||||
Settings = settings;
|
Settings = settings;
|
||||||
|
|
||||||
if (result == null)
|
if (result == null) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Result = result;
|
Result = result;
|
||||||
|
|
||||||
if (Result.Glyph is { FontFamily: not null } glyph)
|
if (Result.Glyph is { FontFamily: not null } glyph)
|
||||||
|
|
@ -39,20 +37,20 @@ namespace Flow.Launcher.ViewModel
|
||||||
fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath);
|
fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fonts.ContainsKey(fontFamilyPath))
|
if (Fonts.TryGetValue(fontFamilyPath, out var value))
|
||||||
{
|
{
|
||||||
Glyph = glyph with
|
Glyph = glyph with
|
||||||
{
|
{
|
||||||
FontFamily = fonts[fontFamilyPath]
|
FontFamily = value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
fontCollection.AddFontFile(fontFamilyPath);
|
FontCollection.AddFontFile(fontFamilyPath);
|
||||||
fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{fontCollection.Families[^1].Name}";
|
Fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{FontCollection.Families[^1].Name}";
|
||||||
Glyph = glyph with
|
Glyph = glyph with
|
||||||
{
|
{
|
||||||
FontFamily = fonts[fontFamilyPath]
|
FontFamily = Fonts[fontFamilyPath]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +59,6 @@ namespace Flow.Launcher.ViewModel
|
||||||
Glyph = glyph;
|
Glyph = glyph;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Settings Settings { get; }
|
public Settings Settings { get; }
|
||||||
|
|
@ -95,14 +92,10 @@ namespace Flow.Launcher.ViewModel
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (PreviewImageAvailable)
|
if (PreviewImageAvailable)
|
||||||
{
|
|
||||||
return Visibility.Visible;
|
return Visibility.Visible;
|
||||||
}
|
|
||||||
else
|
// Fall back to icon
|
||||||
{
|
return ShowIcon;
|
||||||
// Fall back to icon
|
|
||||||
return ShowIcon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,9 +104,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (Result.RoundedIcon)
|
if (Result.RoundedIcon)
|
||||||
{
|
|
||||||
return IconXY / 2;
|
return IconXY / 2;
|
||||||
}
|
|
||||||
return IconXY;
|
return IconXY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,31 +140,40 @@ namespace Flow.Launcher.ViewModel
|
||||||
? Result.SubTitle
|
? Result.SubTitle
|
||||||
: Result.SubTitleToolTip;
|
: Result.SubTitleToolTip;
|
||||||
|
|
||||||
private volatile bool ImageLoaded;
|
private volatile bool _imageLoaded;
|
||||||
private volatile bool PreviewImageLoaded;
|
private volatile bool _previewImageLoaded;
|
||||||
|
|
||||||
private ImageSource image = ImageLoader.LoadingImage;
|
private ImageSource _image = ImageLoader.LoadingImage;
|
||||||
private ImageSource previewImage = ImageLoader.LoadingImage;
|
private ImageSource _previewImage = ImageLoader.LoadingImage;
|
||||||
|
|
||||||
public ImageSource Image
|
public ImageSource Image
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!ImageLoaded)
|
if (!_imageLoaded)
|
||||||
{
|
{
|
||||||
ImageLoaded = true;
|
_imageLoaded = true;
|
||||||
_ = LoadImageAsync();
|
_ = LoadImageAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return _image;
|
||||||
}
|
}
|
||||||
private set => image = value;
|
private set => _image = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageSource PreviewImage
|
public ImageSource PreviewImage
|
||||||
{
|
{
|
||||||
get => previewImage;
|
get
|
||||||
private set => previewImage = value;
|
{
|
||||||
|
if (!_previewImageLoaded)
|
||||||
|
{
|
||||||
|
_previewImageLoaded = true;
|
||||||
|
_ = LoadPreviewImageAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _previewImage;
|
||||||
|
}
|
||||||
|
private set => _previewImage = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -188,8 +189,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = icon();
|
return icon();
|
||||||
return image;
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -208,7 +208,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
var iconDelegate = Result.Icon;
|
var iconDelegate = Result.Icon;
|
||||||
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
|
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
|
||||||
{
|
{
|
||||||
image = img;
|
_image = img;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -223,7 +223,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
|
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
|
||||||
if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
|
if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
|
||||||
{
|
{
|
||||||
previewImage = img;
|
_previewImage = img;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -234,13 +234,10 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
public void LoadPreviewImage()
|
public void LoadPreviewImage()
|
||||||
{
|
{
|
||||||
if (ShowDefaultPreview == Visibility.Visible)
|
if (ShowDefaultPreview == Visibility.Visible && !_previewImageLoaded && ShowPreviewImage == Visibility.Visible)
|
||||||
{
|
{
|
||||||
if (!PreviewImageLoaded && ShowPreviewImage == Visibility.Visible)
|
_previewImageLoaded = true;
|
||||||
{
|
_ = LoadPreviewImageAsync();
|
||||||
PreviewImageLoaded = true;
|
|
||||||
_ = LoadPreviewImageAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
|
||||||
using Flow.Launcher.Plugin;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -10,6 +8,8 @@ using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
using System.Windows.Documents;
|
using System.Windows.Documents;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.ViewModel
|
namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
|
|
@ -28,6 +28,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
Results = new ResultCollection();
|
Results = new ResultCollection();
|
||||||
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
|
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResultsViewModel(Settings settings) : this()
|
public ResultsViewModel(Settings settings) : this()
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
|
@ -219,7 +220,6 @@ namespace Flow.Launcher.ViewModel
|
||||||
if (newRawResults.Count == 0)
|
if (newRawResults.Count == 0)
|
||||||
return Results;
|
return Results;
|
||||||
|
|
||||||
|
|
||||||
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
|
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
|
||||||
|
|
||||||
return Results.Where(r => r.Result.PluginID != resultId)
|
return Results.Where(r => r.Result.PluginID != resultId)
|
||||||
|
|
@ -241,6 +241,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region FormattedText Dependency Property
|
#region FormattedText Dependency Property
|
||||||
|
|
||||||
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
|
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
|
||||||
"FormattedText",
|
"FormattedText",
|
||||||
typeof(Inline),
|
typeof(Inline),
|
||||||
|
|
@ -259,8 +260,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
var textBlock = d as TextBlock;
|
if (d is not TextBlock textBlock) return;
|
||||||
if (textBlock == null) return;
|
|
||||||
|
|
||||||
var inline = (Inline)e.NewValue;
|
var inline = (Inline)e.NewValue;
|
||||||
|
|
||||||
|
|
@ -269,6 +269,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
textBlock.Inlines.Add(inline);
|
textBlock.Inlines.Add(inline);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged
|
public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged
|
||||||
|
|
@ -279,7 +280,6 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||||
|
|
||||||
|
|
||||||
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
CollectionChanged?.Invoke(this, e);
|
CollectionChanged?.Invoke(this, e);
|
||||||
|
|
@ -297,6 +297,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
// wpf use DirectX / double buffered already, so just reset all won't cause ui flickering
|
// wpf use DirectX / double buffered already, so just reset all won't cause ui flickering
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddAll(List<ResultViewModel> Items)
|
private void AddAll(List<ResultViewModel> Items)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Items.Count; i++)
|
for (int i = 0; i < Items.Count; i++)
|
||||||
|
|
@ -308,6 +309,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveAll(int Capacity = 512)
|
public void RemoveAll(int Capacity = 512)
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
|
||||||
|
|
||||||
## 📦 Plugins
|
## 📦 Plugins
|
||||||
|
|
||||||
- Support wide range of plugins. Visit [here](https://flowlauncher.com/docs/#/plugins) for our plugin portfolio.
|
- Support wide range of plugins. Visit [here](https://www.flowlauncher.com/plugins/) for our plugin portfolio.
|
||||||
- Publish your own plugin to flow! Create plugins in:
|
- Publish your own plugin to flow! Create plugins in:
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue