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