Merge branch 'dev' into github-action

This commit is contained in:
Kevin Zhang 2025-03-29 11:29:29 -05:00 committed by GitHub
commit 577ae3428d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 4769 additions and 2834 deletions

View file

@ -1,18 +1,14 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Forms;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
using CheckBox = System.Windows.Controls.CheckBox;
using ComboBox = System.Windows.Controls.ComboBox;
using Control = System.Windows.Controls.Control;
using Orientation = System.Windows.Controls.Orientation;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
#nullable enable
namespace Flow.Launcher.Core.Plugin
{
@ -23,51 +19,72 @@ namespace Flow.Launcher.Core.Plugin
public required string SettingPath { get; init; }
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
public IReadOnlyDictionary<string, object> Inner => Settings;
protected ConcurrentDictionary<string, object> Settings { get; set; }
public IReadOnlyDictionary<string, object?> Inner => Settings;
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
public required IPublicAPI API { get; init; }
private JsonStorage<ConcurrentDictionary<string, object>> _storage;
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
// maybe move to resource?
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin");
private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin");
private static readonly Thickness SettingPanelItemLeftTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftTopBottomMargin");
private static readonly double SettingPanelTextBoxMinWidth = (double)Application.Current.FindResource("SettingPanelTextBoxMinWidth");
private static readonly double SettingPanelPathTextBoxWidth = (double)Application.Current.FindResource("SettingPanelPathTextBoxWidth");
private static readonly double SettingPanelAreaTextBoxMinHeight = (double)Application.Current.FindResource("SettingPanelAreaTextBoxMinHeight");
public async Task InitializeAsync()
{
_storage = new JsonStorage<ConcurrentDictionary<string, object>>(SettingPath);
Settings = await _storage.LoadAsync();
if (Configuration == null)
if (Settings == null)
{
return;
_storage = new JsonStorage<ConcurrentDictionary<string, object?>>(SettingPath);
Settings = await _storage.LoadAsync();
// Because value type of settings dictionary is object which causes them to be JsonElement when loading from json files,
// we need to convert it to the correct type
foreach (var (key, value) in Settings)
{
if (value is not JsonElement jsonElement) continue;
Settings[key] = jsonElement.ValueKind switch
{
JsonValueKind.String => jsonElement.GetString() ?? value,
JsonValueKind.True => jsonElement.GetBoolean(),
JsonValueKind.False => jsonElement.GetBoolean(),
JsonValueKind.Null => null,
_ => value
};
}
}
if (Configuration == null) return;
foreach (var (type, attributes) in Configuration.Body)
{
if (attributes.Name == null)
{
continue;
}
// Skip if the setting does not have attributes or name
if (attributes?.Name == null) continue;
if (!Settings.ContainsKey(attributes.Name))
// Skip if the setting does not have attributes or name
if (!NeedSaveInSettings(type)) continue;
// If need save in settings, we need to make sure the setting exists in the settings file
if (Settings.ContainsKey(attributes.Name)) continue;
if (type == "checkbox")
{
// If can parse the default value to bool, use it, otherwise use false
Settings[attributes.Name] = bool.TryParse(attributes.DefaultValue, out var value) && value;
}
else
{
Settings[attributes.Name] = attributes.DefaultValue;
}
}
}
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
{
if (settings == null || settings.Count == 0)
return;
if (settings == null || settings.Count == 0) return;
foreach (var (key, value) in settings)
{
@ -78,19 +95,23 @@ namespace Flow.Launcher.Core.Plugin
switch (control)
{
case TextBox textBox:
textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty);
var text = value as string ?? string.Empty;
textBox.Dispatcher.Invoke(() => textBox.Text = text);
break;
case PasswordBox passwordBox:
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty);
var password = value as string ?? string.Empty;
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = password);
break;
case ComboBox comboBox:
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
break;
case CheckBox checkBox:
checkBox.Dispatcher.Invoke(() =>
checkBox.IsChecked = value is bool isChecked
? isChecked
: bool.Parse(value as string ?? string.Empty));
var isChecked = value is bool boolValue
? boolValue
// If can parse the default value to bool, use it, otherwise use false
: value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString)
&& boolValueFromString;
checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked);
break;
}
}
@ -108,7 +129,7 @@ namespace Flow.Launcher.Core.Plugin
{
_storage.Save();
}
public bool NeedCreateSettingPanel()
{
// If there are no settings or the settings configuration is empty, return null
@ -118,329 +139,350 @@ namespace Flow.Launcher.Core.Plugin
public Control CreateSettingPanel()
{
// No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true
// if (!NeedCreateSettingPanel()) return null;
var settingWindow = new UserControl();
var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
ColumnDefinition gridCol1 = new ColumnDefinition();
ColumnDefinition gridCol2 = new ColumnDefinition();
gridCol1.Width = new GridLength(70, GridUnitType.Star);
gridCol2.Width = new GridLength(30, GridUnitType.Star);
mainPanel.ColumnDefinitions.Add(gridCol1);
mainPanel.ColumnDefinitions.Add(gridCol2);
settingWindow.Content = mainPanel;
int rowCount = 0;
foreach (var (type, attribute) in Configuration.Body)
// Create main grid with two columns (Column 1: Auto, Column 2: *)
var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
{
Separator sep = new Separator();
sep.VerticalAlignment = VerticalAlignment.Top;
sep.Margin = settingSepMargin;
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
var panel = new StackPanel
Width = new GridLength(0, GridUnitType.Auto)
});
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
{
Width = new GridLength(1, GridUnitType.Star)
});
// Iterate over each setting and create one row for it
var rowCount = 0;
foreach (var (type, attributes) in Configuration!.Body)
{
// Skip if the setting does not have attributes or name
if (attributes?.Name == null) continue;
// Add a new row to the main grid
mainPanel.RowDefinitions.Add(new RowDefinition()
{
Orientation = Orientation.Vertical,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelPanelMargin
};
RowDefinition gridRow = new RowDefinition();
mainPanel.RowDefinitions.Add(gridRow);
var name = new TextBlock()
{
Text = attribute.Label,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
var desc = new TextBlock()
{
Text = attribute.Description,
FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingDescMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
if (attribute.Description == null) /* if no description, hide */
desc.Visibility = Visibility.Collapsed;
if (type != "textBlock") /* if textBlock, hide desc */
{
panel.Children.Add(name);
panel.Children.Add(desc);
}
Grid.SetColumn(panel, 0);
Grid.SetRow(panel, rowCount);
Height = new GridLength(0, GridUnitType.Auto)
});
// State controls for column 0 and 1
StackPanel? panel = null;
FrameworkElement contentControl;
// If the type is textBlock, separator, or checkbox, we do not need to create a panel
if (type != "textBlock" && type != "separator" && type != "checkbox")
{
// Create a panel to hold the label and description
panel = new StackPanel
{
Margin = SettingPanelItemTopBottomMargin,
Orientation = Orientation.Vertical,
VerticalAlignment = VerticalAlignment.Center
};
// Create a text block for name
var name = new TextBlock()
{
Text = attributes.Label,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow
};
// Create a text block for description
TextBlock? desc = null;
if (attributes.Description != null)
{
desc = new TextBlock()
{
Text = attributes.Description,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow
};
desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change
}
// Add the name and description to the panel
panel.Children.Add(name);
if (desc != null) panel.Children.Add(desc);
}
switch (type)
{
case "textBlock":
{
contentControl = new TextBlock
{
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin,
Padding = new Thickness(0, 0, 0, 0),
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap
};
contentControl = new TextBlock
{
Text = attributes.Description?.Replace("\\r\\n", "\r\n") ?? string.Empty,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemTopBottomMargin,
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap
};
Grid.SetColumn(contentControl, 0);
Grid.SetColumnSpan(contentControl, 2);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
break;
}
case "input":
{
var textBox = new TextBox()
{
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
var textBox = new TextBox()
{
MinWidth = SettingPanelTextBoxMinWidth,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftTopBottomMargin,
Text = Settings[attributes.Name] as string ?? string.Empty,
ToolTip = attributes.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
textBox.TextChanged += (_, _) =>
{
Settings[attributes.Name] = textBox.Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
contentControl = textBox;
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
break;
}
case "inputWithFileBtn":
case "inputWithFolderBtn":
{
var textBox = new TextBox()
{
Margin = new Thickness(10, 0, 0, 0),
Text = Settings[attribute.Name] as string ?? string.Empty,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
var Btn = new System.Windows.Controls.Button()
{
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
};
Btn.Click += (_, _) =>
{
using CommonDialog dialog = type switch
var textBox = new TextBox()
{
"inputWithFolderBtn" => new FolderBrowserDialog(),
_ => new OpenFileDialog(),
Width = SettingPanelPathTextBoxWidth,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftMargin,
Text = Settings[attributes.Name] as string ?? string.Empty,
ToolTip = attributes.Description
};
if (dialog.ShowDialog() != DialogResult.OK) return;
var path = dialog switch
textBox.TextChanged += (_, _) =>
{
FolderBrowserDialog folderDialog => folderDialog.SelectedPath,
OpenFileDialog fileDialog => fileDialog.FileName,
Settings[attributes.Name] = textBox.Text;
};
textBox.Text = path;
Settings[attribute.Name] = path;
};
var dockPanel = new DockPanel() { Margin = settingControlMargin };
var Btn = new Button()
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftMargin,
Content = API.GetTranslation("select")
};
DockPanel.SetDock(Btn, Dock.Right);
dockPanel.Children.Add(Btn);
dockPanel.Children.Add(textBox);
contentControl = dockPanel;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Btn.Click += (_, _) =>
{
using System.Windows.Forms.CommonDialog dialog = type switch
{
"inputWithFolderBtn" => new System.Windows.Forms.FolderBrowserDialog(),
_ => new System.Windows.Forms.OpenFileDialog(),
};
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
{
return;
}
break;
}
var path = dialog switch
{
System.Windows.Forms.FolderBrowserDialog folderDialog => folderDialog.SelectedPath,
System.Windows.Forms.OpenFileDialog fileDialog => fileDialog.FileName,
_ => throw new System.NotImplementedException()
};
textBox.Text = path;
Settings[attributes.Name] = path;
};
var stackPanel = new StackPanel()
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemTopBottomMargin,
Orientation = Orientation.Horizontal
};
// Create a stack panel to wrap the button and text box
stackPanel.Children.Add(textBox);
stackPanel.Children.Add(Btn);
contentControl = stackPanel;
break;
}
case "textarea":
{
var textBox = new TextBox()
{
Height = 120,
Margin = settingControlMargin,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
Text = Settings[attribute.Name] as string ?? string.Empty,
ToolTip = attribute.Description
};
var textBox = new TextBox()
{
MinHeight = SettingPanelAreaTextBoxMinHeight,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftTopBottomMargin,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
Text = Settings[attributes.Name] as string ?? string.Empty,
ToolTip = attributes.Description
};
textBox.TextChanged += (sender, _) =>
{
Settings[attribute.Name] = ((TextBox)sender).Text;
};
textBox.TextChanged += (sender, _) =>
{
Settings[attributes.Name] = ((TextBox)sender).Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
contentControl = textBox;
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
break;
}
case "passwordBox":
{
var passwordBox = new PasswordBox()
{
Margin = settingControlMargin,
Password = Settings[attribute.Name] as string ?? string.Empty,
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
var passwordBox = new PasswordBox()
{
MinWidth = SettingPanelTextBoxMinWidth,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftTopBottomMargin,
Password = Settings[attributes.Name] as string ?? string.Empty,
PasswordChar = attributes.passwordChar == default ? '*' : attributes.passwordChar,
ToolTip = attributes.Description,
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attribute.Name] = ((PasswordBox)sender).Password;
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attributes.Name] = ((PasswordBox)sender).Password;
};
contentControl = passwordBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
contentControl = passwordBox;
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
break;
}
case "dropdown":
{
var comboBox = new System.Windows.Controls.ComboBox()
{
ItemsSource = attribute.Options,
SelectedItem = Settings[attribute.Name],
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
var comboBox = new ComboBox()
{
ItemsSource = attributes.Options,
SelectedItem = Settings[attributes.Name],
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftTopBottomMargin,
ToolTip = attributes.Description
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attributes.Name] = (string)((ComboBox)sender).SelectedItem;
};
contentControl = comboBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
contentControl = comboBox;
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
break;
}
case "checkbox":
var checkBox = new CheckBox
{
IsChecked =
Settings[attribute.Name] is bool isChecked
// If can parse the default value to bool, use it, otherwise use false
var defaultValue = bool.TryParse(attributes.DefaultValue, out var value) && value;
var checkBox = new CheckBox
{
IsChecked =
Settings[attributes.Name] is bool isChecked
? isChecked
: bool.Parse(attribute.DefaultValue),
Margin = settingCheckboxMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
: defaultValue,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemTopBottomMargin,
Content = attributes.Label,
ToolTip = attributes.Description
};
checkBox.Click += (sender, _) =>
{
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
};
checkBox.Click += (sender, _) =>
{
Settings[attributes.Name] = ((CheckBox)sender).IsChecked ?? defaultValue;
};
contentControl = checkBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
contentControl = checkBox;
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
break;
}
case "hyperlink":
var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url };
var linkbtn = new System.Windows.Controls.Button
{
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
Margin = settingControlMargin
};
var hyperlink = new Hyperlink
{
ToolTip = attributes.Description,
NavigateUri = attributes.url
};
linkbtn.Content = attribute.urlLabel;
hyperlink.Inlines.Add(attributes.urlLabel);
hyperlink.RequestNavigate += (sender, e) =>
{
API.OpenUrl(e.Uri);
e.Handled = true;
};
contentControl = linkbtn;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
var textBlock = new TextBlock()
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftTopBottomMargin,
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap
};
textBlock.Inlines.Add(hyperlink);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
contentControl = textBlock;
break;
break;
}
case "separator":
{
var sep = new Separator();
sep.SetResourceReference(Separator.StyleProperty, "SettingPanelSeparatorStyle");
contentControl = sep;
break;
}
default:
continue;
}
if (type != "textBlock")
SettingControls[attribute.Name] = contentControl;
// If type is textBlock or separator, we just add the content control to the main grid
if (panel == null)
{
// Add the content control to the column 0, row rowCount and columnSpan 2 of the main grid
mainPanel.Children.Add(contentControl);
Grid.SetColumn(contentControl, 0);
Grid.SetColumnSpan(contentControl, 2);
Grid.SetRow(contentControl, rowCount);
}
else
{
// Add the panel to the column 0 and row rowCount of the main grid
mainPanel.Children.Add(panel);
Grid.SetColumn(panel, 0);
Grid.SetRow(panel, rowCount);
// Add the content control to the column 1 and row rowCount of the main grid
mainPanel.Children.Add(contentControl);
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
}
// Add into SettingControls for settings storage if need
if (NeedSaveInSettings(type)) SettingControls[attributes.Name] = contentControl;
mainPanel.Children.Add(panel);
mainPanel.Children.Add(contentControl);
rowCount++;
}
return settingWindow;
// Wrap the main grid in a user control
return new UserControl()
{
Content = mainPanel
};
}
private static bool NeedSaveInSettings(string type)
{
return type != "textBlock" && type != "separator" && type != "hyperlink";
}
}
}

View file

@ -205,9 +205,6 @@ namespace Flow.Launcher.Core.Plugin
}
}
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
if (failedPlugins.Any())
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
@ -231,7 +228,6 @@ namespace Flow.Launcher.Core.Plugin
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
return GlobalPlugins;
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair>
{
@ -367,7 +363,16 @@ namespace Flow.Launcher.Core.Plugin
NonGlobalPlugins[newActionKeyword] = plugin;
}
// Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
if (plugin.Metadata.ActionKeywords.Count > 0)
{
plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
}
else
{
plugin.Metadata.ActionKeyword = string.Empty;
}
}
/// <summary>
@ -388,16 +393,15 @@ namespace Flow.Launcher.Core.Plugin
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
NonGlobalPlugins.Remove(oldActionkeyword);
// Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
}
public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword)
{
if (oldActionKeyword != newActionKeyword)
if (plugin.Metadata.ActionKeywords.Count > 0)
{
AddActionKeyword(id, newActionKeyword);
RemoveActionKeyword(id, oldActionKeyword);
plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
}
else
{
plugin.Metadata.ActionKeyword = string.Empty;
}
}

View file

@ -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)

View file

@ -3,21 +3,29 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
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;
using System.Windows.Shell;
using System.Windows.Threading;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Microsoft.Win32;
namespace Flow.Launcher.Core.Resource
{
public class Theme
{
#region Properties & Fields
public bool BlurEnabled { get; private set; }
private const string ThemeMetadataNamePrefix = "Name:";
private const string ThemeMetadataIsDarkPrefix = "IsDark:";
private const string ThemeMetadataHasBlurPrefix = "HasBlur:";
@ -31,14 +39,14 @@ namespace Flow.Launcher.Core.Resource
private string _oldTheme;
private const string Folder = Constant.Themes;
private const string Extension = ".xaml";
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
private static string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
private static string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
public string CurrentTheme => _settings.Theme;
private Thickness _themeResizeBorderThickness;
public bool BlurEnabled { get; set; }
#endregion
private double mainWindowWidth;
#region Constructor
public Theme(IPublicAPI publicAPI, Settings settings)
{
@ -50,20 +58,32 @@ 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
#region Theme Resources
public string GetCurrentTheme()
{
return _settings.Theme;
}
private void MakeSureThemeDirectoriesExist()
@ -81,68 +101,154 @@ namespace Flow.Launcher.Core.Resource
}
}
public bool ChangeTheme(string theme)
{
const string defaultTheme = Constant.DefaultTheme;
string path = GetThemePath(theme);
try
{
if (string.IsNullOrEmpty(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));
_settings.Theme = theme;
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
if (_oldTheme != theme || theme == defaultTheme)
{
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
BlurEnabled = Win32Helper.IsBlurTheme();
if (_settings.UseDropShadowEffect && !BlurEnabled)
AddDropShadowEffectToCurrentTheme();
Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
}
catch (DirectoryNotFoundException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
if (theme != defaultTheme)
{
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
ChangeTheme(defaultTheme);
}
return false;
}
catch (XamlParseException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
if (theme != defaultTheme)
{
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
ChangeTheme(defaultTheme);
}
return false;
}
return true;
}
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);
@ -154,9 +260,7 @@ namespace Flow.Launcher.Core.Resource
return dict;
}
private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme);
public ResourceDictionary GetResourceDictionary(string theme)
private ResourceDictionary GetResourceDictionary(string theme)
{
var dict = GetThemeResourceDictionary(theme);
@ -168,22 +272,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 &&
@ -213,36 +317,20 @@ namespace Flow.Launcher.Core.Resource
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
Array.ForEach(
new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o
new[] { resultSubItemStyle, resultSubItemSelectedStyle }, o
=> Array.ForEach(setters, p => o.Setters.Add(p)));
}
/* 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));
mainWindowWidth = (double)width;
windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width));
return dict;
}
private ResourceDictionary GetCurrentResourceDictionary( )
private ResourceDictionary GetCurrentResourceDictionary()
{
return GetResourceDictionary(_settings.Theme);
}
public List<ThemeData> LoadAvailableThemes()
{
List<ThemeData> themes = new List<ThemeData>();
foreach (var themeDirectory in _themeDirectories)
{
var filePaths = Directory
.GetFiles(themeDirectory)
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
.Select(GetThemeDataFromPath);
themes.AddRange(filePaths);
}
return themes.OrderBy(o => o.Name).ToList();
return GetResourceDictionary(GetCurrentTheme());
}
private ThemeData GetThemeDataFromPath(string path)
@ -264,15 +352,15 @@ namespace Flow.Launcher.Core.Resource
{
if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase))
{
name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim();
name = line[ThemeMetadataNamePrefix.Length..].Trim();
}
else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase))
{
isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim());
isDark = bool.Parse(line[ThemeMetadataIsDarkPrefix.Length..].Trim());
}
else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase))
{
hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim());
hasBlur = bool.Parse(line[ThemeMetadataHasBlurPrefix.Length..].Trim());
}
}
@ -293,6 +381,82 @@ namespace Flow.Launcher.Core.Resource
return string.Empty;
}
#endregion
#region Load & Change
public List<ThemeData> LoadAvailableThemes()
{
List<ThemeData> themes = new List<ThemeData>();
foreach (var themeDirectory in _themeDirectories)
{
var filePaths = Directory
.GetFiles(themeDirectory)
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
.Select(GetThemeDataFromPath);
themes.AddRange(filePaths);
}
return themes.OrderBy(o => o.Name).ToList();
}
public bool ChangeTheme(string theme = null)
{
if (string.IsNullOrEmpty(theme))
theme = GetCurrentTheme();
string path = GetThemePath(theme);
try
{
if (string.IsNullOrEmpty(path))
throw new DirectoryNotFoundException($"Theme path can't be found <{path}>");
// Retrieve theme resource always use the resource with font settings applied.
var resourceDict = GetResourceDictionary(theme);
UpdateResourceDictionary(resourceDict);
_settings.Theme = theme;
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
if (_oldTheme != theme || theme == Constant.DefaultTheme)
{
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
BlurEnabled = IsBlurTheme();
// Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues
_ = RefreshFrameAsync();
return true;
}
catch (DirectoryNotFoundException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;
}
catch (XamlParseException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;
}
}
#endregion
#region Shadow Effect
public void AddDropShadowEffectToCurrentTheme()
{
var dict = GetCurrentResourceDictionary();
@ -301,7 +465,7 @@ namespace Flow.Launcher.Core.Resource
var effectSetter = new Setter
{
Property = Border.EffectProperty,
Property = UIElement.EffectProperty,
Value = new DropShadowEffect
{
Opacity = 0.3,
@ -311,13 +475,12 @@ namespace Flow.Launcher.Core.Resource
}
};
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
if (marginSetter == null)
if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is not Setter marginSetter)
{
var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin);
marginSetter = new Setter()
{
Property = Border.MarginProperty,
Property = FrameworkElement.MarginProperty,
Value = margin,
};
windowBorderStyle.Setters.Add(marginSetter);
@ -347,14 +510,12 @@ namespace Flow.Launcher.Core.Resource
var dict = GetCurrentResourceDictionary();
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
if (effectSetter != null)
if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == UIElement.EffectProperty) is Setter effectSetter)
{
windowBorderStyle.Setters.Remove(effectSetter);
}
if (marginSetter != null)
if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is Setter marginSetter)
{
var currentMargin = (Thickness)marginSetter.Value;
var newMargin = new Thickness(
@ -370,31 +531,378 @@ namespace Flow.Launcher.Core.Resource
UpdateResourceDictionary(dict);
}
public void SetResizeBorderThickness(WindowChrome windowChrome, bool fixedWindowSize)
{
if (fixedWindowSize)
{
windowChrome.ResizeBorderThickness = new Thickness(0);
}
else
{
windowChrome.ResizeBorderThickness = _themeResizeBorderThickness;
}
}
// because adding drop shadow effect will change the margin of the window,
// we need to update the window chrome thickness to correct set the resize border
private static void SetResizeBoarderThickness(Thickness? effectMargin)
private void SetResizeBoarderThickness(Thickness? effectMargin)
{
var window = Application.Current.MainWindow;
if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome)
{
Thickness thickness;
// Save the theme resize border thickness so that we can restore it if we change ResizeWindow setting
if (effectMargin == null)
{
thickness = SystemParameters.WindowResizeBorderThickness;
_themeResizeBorderThickness = SystemParameters.WindowResizeBorderThickness;
}
else
{
thickness = new Thickness(
_themeResizeBorderThickness = new Thickness(
effectMargin.Value.Left + SystemParameters.WindowResizeBorderThickness.Left,
effectMargin.Value.Top + SystemParameters.WindowResizeBorderThickness.Top,
effectMargin.Value.Right + SystemParameters.WindowResizeBorderThickness.Right,
effectMargin.Value.Bottom + SystemParameters.WindowResizeBorderThickness.Bottom);
}
windowChrome.ResizeBorderThickness = thickness;
// Apply the resize border thickness to the window chrome
SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults);
}
}
#endregion
#region Blur Handling
/// <summary>
/// Refreshes the frame to apply the current theme settings.
/// </summary>
public async Task RefreshFrameAsync()
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// Get the actual backdrop type and drop shadow effect settings
var (backdropType, useDropShadowEffect) = GetActualValue();
// Remove OS minimizing/maximizing animation
// Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3);
// The timing of adding the shadow effect should vary depending on whether the theme is transparent.
if (BlurEnabled)
{
AutoDropShadow(useDropShadowEffect);
}
SetBlurForWindow(GetCurrentTheme(), backdropType);
if (!BlurEnabled)
{
AutoDropShadow(useDropShadowEffect);
}
}, DispatcherPriority.Render);
}
/// <summary>
/// Sets the blur for a window via SetWindowCompositionAttribute
/// </summary>
public async Task SetBlurForWindowAsync()
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// Get the actual backdrop type and drop shadow effect settings
var (backdropType, _) = GetActualValue();
SetBlurForWindow(GetCurrentTheme(), backdropType);
}, DispatcherPriority.Render);
}
/// <summary>
/// Gets the actual backdrop type and drop shadow effect settings based on the current theme status.
/// </summary>
public (BackdropTypes BackdropType, bool UseDropShadowEffect) GetActualValue()
{
var backdropType = _settings.BackdropType;
var useDropShadowEffect = _settings.UseDropShadowEffect;
// When changed non-blur theme, change to backdrop to none
if (!BlurEnabled)
{
backdropType = BackdropTypes.None;
}
// Dropshadow on and control disabled.(user can't change dropshadow with blur theme)
if (BlurEnabled)
{
useDropShadowEffect = true;
}
return (backdropType, useDropShadowEffect);
}
private void SetBlurForWindow(string theme, BackdropTypes backdropType)
{
var dict = GetResourceDictionary(theme);
if (dict == null) return;
var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
if (windowBorderStyle == 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;
if (BlurEnabled && hasBlur && Win32Helper.IsBackdropSupported())
{
// If the BackdropType is Mica or MicaAlt, set the windowborderstyle's background to transparent
if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt)
{
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property.Name == "Background"));
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(1, 0, 0, 0))));
}
else if (backdropType == BackdropTypes.Acrylic)
{
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property.Name == "Background"));
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent)));
}
// Apply the blur effect
Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType);
ColorizeWindow(theme, backdropType);
}
else
{
// Apply default style when Blur is disabled
Win32Helper.DWMSetBackdropForWindow(mainWindow, BackdropTypes.None);
ColorizeWindow(theme, backdropType);
}
UpdateResourceDictionary(dict);
}
private void AutoDropShadow(bool useDropShadowEffect)
{
SetWindowCornerPreference("Default");
RemoveDropShadowEffectFromCurrentTheme();
if (useDropShadowEffect)
{
if (BlurEnabled && Win32Helper.IsBackdropSupported())
{
SetWindowCornerPreference("Round");
}
else
{
SetWindowCornerPreference("Default");
AddDropShadowEffectToCurrentTheme();
}
}
else
{
if (BlurEnabled && Win32Helper.IsBackdropSupported())
{
SetWindowCornerPreference("Default");
}
else
{
RemoveDropShadowEffectFromCurrentTheme();
}
}
}
private static void SetWindowCornerPreference(string cornerType)
{
Window mainWindow = Application.Current.MainWindow;
if (mainWindow == null)
return;
Win32Helper.DWMSetCornerPreferenceForWindow(mainWindow, cornerType);
}
// Get Background Color from WindowBorderStyle when there not color for BG.
// for theme has not "LightBG" or "DarkBG" case.
private Color GetWindowBorderStyleBackground(string theme)
{
var Resources = GetThemeResourceDictionary(theme);
var windowBorderStyle = (Style)Resources["WindowBorderStyle"];
var backgroundSetter = windowBorderStyle.Setters
.OfType<Setter>()
.FirstOrDefault(s => s.Property == Border.BackgroundProperty);
if (backgroundSetter != null)
{
// Background's Value is DynamicColor Case
var backgroundValue = backgroundSetter.Value;
if (backgroundValue is SolidColorBrush solidColorBrush)
{
return solidColorBrush.Color; // Return SolidColorBrush's Color
}
else if (backgroundValue is DynamicResourceExtension dynamicResource)
{
// When DynamicResource Extension it is, Key is resource's name.
var resourceKey = backgroundSetter.Value.ToString();
// find key in resource and return color.
if (Resources.Contains(resourceKey))
{
var colorResource = Resources[resourceKey];
if (colorResource is SolidColorBrush colorBrush)
{
return colorBrush.Color;
}
else if (colorResource is Color color)
{
return color;
}
}
}
}
return Colors.Transparent; // Default is transparent
}
private void ApplyPreviewBackground(Color? bgColor = null)
{
if (bgColor == null) return;
// Copy the existing WindowBorderStyle
var previewStyle = new Style(typeof(Border));
if (Application.Current.Resources.Contains("WindowBorderStyle"))
{
if (Application.Current.Resources["WindowBorderStyle"] is Style originalStyle)
{
foreach (var setter in originalStyle.Setters.OfType<Setter>())
{
previewStyle.Setters.Add(new Setter(setter.Property, setter.Value));
}
}
}
// Apply background color (remove transparency in color)
// WPF does not allow the use of an acrylic brush within the window's internal area,
// so transparency effects are not applied to the preview.
Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B);
previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor)));
// The blur theme keeps the corner round fixed (applying DWM code to modify it causes rendering issues).
// The non-blur theme retains the previously set WindowBorderStyle.
if (BlurEnabled)
{
previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5)));
previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1)));
}
Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle;
}
private void ColorizeWindow(string theme, BackdropTypes backdropType)
{
var dict = GetThemeResourceDictionary(theme);
if (dict == 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;
// SystemBG value check (Auto, Light, Dark)
string systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto
// Check the user's ColorScheme setting
string colorScheme = _settings.ColorScheme;
// Check system dark mode setting (read AppsUseLightTheme value)
int themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1);
bool isSystemDark = themeValue == 0;
// Final decision on whether to use dark mode
bool useDarkMode = false;
// If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value
if (systemBG == "Dark")
{
useDarkMode = true; // Dark
}
else if (systemBG == "Light")
{
useDarkMode = false; // Light
}
else if (systemBG == "Auto")
{
// If systemBG is "Auto", decide based on ColorScheme
if (colorScheme == "Dark")
useDarkMode = true;
else if (colorScheme == "Light")
useDarkMode = false;
else
useDarkMode = isSystemDark; // Auto (based on system setting)
}
// Apply DWM Dark Mode
Win32Helper.DWMSetDarkModeForWindow(mainWindow, useDarkMode);
Color LightBG;
Color DarkBG;
// Retrieve LightBG value (fallback to WindowBorderStyle background color if not found)
try
{
LightBG = dict.Contains("LightBG") ? (Color)dict["LightBG"] : GetWindowBorderStyleBackground(theme);
}
catch (Exception)
{
LightBG = GetWindowBorderStyleBackground(theme);
}
// Retrieve DarkBG value (fallback to LightBG if not found)
try
{
DarkBG = dict.Contains("DarkBG") ? (Color)dict["DarkBG"] : LightBG;
}
catch (Exception)
{
DarkBG = LightBG;
}
// Select background color based on ColorScheme and SystemBG
Color selectedBG = useDarkMode ? DarkBG : LightBG;
ApplyPreviewBackground(selectedBG);
bool isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); // Windows 11 미만이면 hasBlur를 강제 false
if (!isBlurAvailable)
{
mainWindow.Background = Brushes.Transparent;
}
else
{
// Only set the background to transparent if the theme supports blur
if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt)
{
mainWindow.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
}
else
{
mainWindow.Background = new SolidColorBrush(selectedBG);
}
}
}
private static bool IsBlurTheme()
{
if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false
return false;
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
return resource is bool b && b;
}
#endregion
#region Classes
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
#endregion
}
}

View file

@ -1,12 +0,0 @@
using System;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Resource
{
[Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Theme>() instead.")]
public class ThemeManager
{
public static Theme Instance
=> Ioc.Default.GetRequiredService<Theme>();
}
}

View file

@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
public static readonly string 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;

View file

@ -67,8 +67,6 @@ namespace Flow.Launcher.Infrastructure
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
}
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
/// <summary>
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
/// </summary>

View file

@ -0,0 +1,123 @@
using System.Collections.Generic;
using System;
using System.Runtime.InteropServices;
using System.Windows;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Flow.Launcher.Infrastructure;
/// <summary>
/// Contains full information about a display monitor.
/// Codes are edited from: <see href="https://github.com/Jack251970/DesktopWidgets3">.
/// </summary>
internal class MonitorInfo
{
/// <summary>
/// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers).
/// </summary>
/// <returns>A list of display monitors</returns>
public static unsafe IList<MonitorInfo> GetDisplayMonitors()
{
var monitorCount = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS);
var list = new List<MonitorInfo>(monitorCount);
var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
{
list.Add(new MonitorInfo(monitor, rect));
return true;
});
var dwData = new LPARAM();
var hdc = new HDC();
bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
if (!ok)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}
return list;
}
/// <summary>
/// Gets the display monitor that is nearest to a given window.
/// </summary>
/// <param name="hwnd">Window handle</param>
/// <returns>The display monitor that is nearest to a given window, or null if no monitor is found.</returns>
public static unsafe MonitorInfo GetNearestDisplayMonitor(HWND hwnd)
{
var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
MonitorInfo nearestMonitorInfo = null;
var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
{
if (monitor == nearestMonitor)
{
nearestMonitorInfo = new MonitorInfo(monitor, rect);
return false;
}
return true;
});
var dwData = new LPARAM();
var hdc = new HDC();
bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
if (!ok)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}
return nearestMonitorInfo;
}
private readonly HMONITOR _monitor;
internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
{
RectMonitor =
new Rect(new Point(rect->left, rect->top),
new Point(rect->right, rect->bottom));
_monitor = monitor;
var info = new MONITORINFOEXW() { monitorInfo = new MONITORINFO() { cbSize = (uint)sizeof(MONITORINFOEXW) } };
GetMonitorInfo(monitor, ref info);
RectWork =
new Rect(new Point(info.monitorInfo.rcWork.left, info.monitorInfo.rcWork.top),
new Point(info.monitorInfo.rcWork.right, info.monitorInfo.rcWork.bottom));
Name = new string(info.szDevice.AsSpan()).Replace("\0", "").Trim();
}
/// <summary>
/// Gets the name of the display.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the display monitor rectangle, expressed in virtual-screen coordinates.
/// </summary>
/// <remarks>
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
/// </remarks>
public Rect RectMonitor { get; }
/// <summary>
/// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates.
/// </summary>
/// <remarks>
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
/// </remarks>
public Rect RectWork { get; }
/// <summary>
/// Gets if the monitor is the the primary display monitor.
/// </summary>
public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
/// <inheritdoc />
public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}";
private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi)
{
fixed (MONITORINFOEXW* lpmiLocal = &lpmi)
{
var lpmiBase = (MONITORINFO*)lpmiLocal;
var __result = PInvoke.GetMonitorInfo(hMonitor, lpmiBase);
return __result;
}
}
}

View file

@ -16,4 +16,46 @@ WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
EnumWindows
EnumWindows
DwmSetWindowAttribute
DWM_SYSTEMBACKDROP_TYPE
DWM_WINDOW_CORNER_PREFERENCE
MAX_PATH
SystemParametersInfo
SetForegroundWindow
GetWindowLong
GetForegroundWindow
GetDesktopWindow
GetShellWindow
GetWindowRect
GetClassName
FindWindowEx
WINDOW_STYLE
SetLastError
WINDOW_EX_STYLE
GetSystemMetrics
EnumDisplayMonitors
MonitorFromWindow
GetMonitorInfo
MONITORINFOEXW
WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE
GetKeyboardLayout
GetWindowThreadProcessId
ActivateKeyboardLayout
GetKeyboardLayoutList
PostMessage
WM_INPUTLANGCHANGEREQUEST
INPUTLANGCHANGE_FORWARD
LOCALE_TRANSIENT_KEYBOARD1
LOCALE_TRANSIENT_KEYBOARD2
LOCALE_TRANSIENT_KEYBOARD3
LOCALE_TRANSIENT_KEYBOARD4

View file

@ -0,0 +1,25 @@
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Windows.Win32;
// Edited from: https://github.com/files-community/Files
internal static partial class PInvoke
{
[DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)]
static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong);
[DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)]
static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong);
// NOTE:
// CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa.
// For more info, visit https://github.com/microsoft/CsWin32/issues/882
public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong)
{
return sizeof(nint) is 4
? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong)
: _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong);
}
}

View file

@ -68,14 +68,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
get => _theme;
set
{
if (value == _theme)
return;
_theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(MaxResultsToShow));
if (value != _theme)
{
_theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(MaxResultsToShow));
}
}
}
public bool UseDropShadowEffect { get; set; } = true;
public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
/* Appearance Settings. It should be separated from the setting later.*/
public double WindowHeightSize { get; set; } = 42;
@ -112,6 +114,33 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public double? SettingWindowLeft { get; set; } = null;
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
bool _showPlaceholder { get; set; } = false;
public bool ShowPlaceholder
{
get => _showPlaceholder;
set
{
if (_showPlaceholder != value)
{
_showPlaceholder = value;
OnPropertyChanged();
}
}
}
string _placeholderText { get; set; } = string.Empty;
public string PlaceholderText
{
get => _placeholderText;
set
{
if (_placeholderText != value)
{
_placeholderText = value;
OnPropertyChanged();
}
}
}
public int CustomExplorerIndex { get; set; } = 0;
[JsonIgnore]
@ -240,10 +269,26 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/// </summary>
public double CustomWindowTop { get; set; } = 0;
public bool KeepMaxResults { get; set; } = false;
public int MaxResultsToShow { get; set; } = 5;
public int ActivateTimes { get; set; }
/// <summary>
/// Fixed window size
/// </summary>
private bool _keepMaxResults { get; set; } = false;
public bool KeepMaxResults
{
get => _keepMaxResults;
set
{
if (_keepMaxResults != value)
{
_keepMaxResults = value;
OnPropertyChanged();
}
}
}
public int MaxResultsToShow { get; set; } = 5;
public int ActivateTimes { get; set; }
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
@ -430,4 +475,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
Fast,
Custom
}
public enum BackdropTypes
{
None,
Acrylic,
Mica,
MicaAlt
}
}

View file

@ -1,7 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Windows.Interop;
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 Point = System.Windows.Point;
namespace Flow.Launcher.Infrastructure
{
@ -9,93 +20,473 @@ namespace Flow.Launcher.Infrastructure
{
#region Blur Handling
/*
Found on https://github.com/riverar/sample-win10-aeroglass
*/
private enum AccentState
public static bool IsBackdropSupported()
{
ACCENT_DISABLED = 0,
ACCENT_ENABLE_GRADIENT = 1,
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
ACCENT_ENABLE_BLURBEHIND = 3,
ACCENT_INVALID_STATE = 4
// Mica and Acrylic only supported Windows 11 22000+
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
Environment.OSVersion.Version.Build >= 22000;
}
[StructLayout(LayoutKind.Sequential)]
private struct AccentPolicy
public static unsafe bool DWMSetCloakForWindow(Window window, bool cloak)
{
public AccentState AccentState;
public int AccentFlags;
public int GradientColor;
public int AnimationId;
var cloaked = cloak ? 1 : 0;
return PInvoke.DwmSetWindowAttribute(
GetWindowHandle(window),
DWMWINDOWATTRIBUTE.DWMWA_CLOAK,
&cloaked,
(uint)Marshal.SizeOf<int>()).Succeeded;
}
[StructLayout(LayoutKind.Sequential)]
private struct WindowCompositionAttributeData
public static unsafe bool DWMSetBackdropForWindow(Window window, BackdropTypes backdrop)
{
public WindowCompositionAttribute Attribute;
public IntPtr Data;
public int SizeOfData;
var backdropType = backdrop switch
{
BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW,
BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW,
BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW,
_ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO
};
return PInvoke.DwmSetWindowAttribute(
GetWindowHandle(window),
DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE,
&backdropType,
(uint)Marshal.SizeOf<int>()).Succeeded;
}
private enum WindowCompositionAttribute
public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMode)
{
WCA_ACCENT_POLICY = 19
}
var darkMode = useDarkMode ? 1 : 0;
[DllImport("user32.dll")]
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
return PInvoke.DwmSetWindowAttribute(
GetWindowHandle(window),
DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
&darkMode,
(uint)Marshal.SizeOf<int>()).Succeeded;
}
/// <summary>
/// Checks if the blur theme is enabled
///
/// </summary>
public static bool IsBlurTheme()
/// <param name="window"></param>
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
/// <returns></returns>
public static unsafe bool DWMSetCornerPreferenceForWindow(Window window, string cornerType)
{
if (Environment.OSVersion.Version >= new Version(6, 2))
var preference = cornerType switch
{
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
"DoNotRound" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND,
"Round" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND,
"RoundSmall" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUNDSMALL,
"Default" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT,
_ => throw new InvalidOperationException("Invalid corner type")
};
if (resource is bool b)
return b;
return PInvoke.DwmSetWindowAttribute(
GetWindowHandle(window),
DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE,
&preference,
(uint)Marshal.SizeOf<int>()).Succeeded;
}
#endregion
#region Wallpaper
public static unsafe string GetWallpaperPath()
{
var wallpaperPtr = stackalloc char[(int)PInvoke.MAX_PATH];
PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, PInvoke.MAX_PATH,
wallpaperPtr,
0);
var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr);
return wallpaper.ToString();
}
#endregion
#region Window Foreground
public static nint GetForegroundWindow()
{
return PInvoke.GetForegroundWindow().Value;
}
public static bool SetForegroundWindow(Window window)
{
return PInvoke.SetForegroundWindow(GetWindowHandle(window));
}
public static bool SetForegroundWindow(nint handle)
{
return PInvoke.SetForegroundWindow(new(handle));
}
#endregion
#region Task Switching
/// <summary>
/// Hide windows in the Alt+Tab window list
/// </summary>
/// <param name="window">To hide a window</param>
public static void HideFromAltTab(Window window)
{
var hwnd = GetWindowHandle(window);
var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
// Add TOOLWINDOW style, remove APPWINDOW style
var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
}
/// <summary>
/// Restore window display in the Alt+Tab window list.
/// </summary>
/// <param name="window">To restore the displayed window</param>
public static void ShowInAltTab(Window window)
{
var hwnd = GetWindowHandle(window);
var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
// Remove the TOOLWINDOW style and add the APPWINDOW style.
var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
SetWindowStyle(GetWindowHandle(window), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
}
/// <summary>
/// Disable windows toolbar's control box
/// This will also disable system menu with Alt+Space hotkey
/// </summary>
public static void DisableControlBox(Window window)
{
var hwnd = GetWindowHandle(window);
var style = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
style &= ~(int)WINDOW_STYLE.WS_SYSMENU;
SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style);
}
private static int GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex)
{
var style = PInvoke.GetWindowLong(hWnd, nIndex);
if (style == 0 && Marshal.GetLastPInvokeError() != 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
return style;
}
private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong)
{
PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error
var result = PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong);
if (result == 0 && Marshal.GetLastPInvokeError() != 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
return result;
}
#endregion
#region Window Fullscreen
private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
private const string WINDOW_CLASS_WINTAB = "Flip3D";
private const string WINDOW_CLASS_PROGMAN = "Progman";
private const string WINDOW_CLASS_WORKERW = "WorkerW";
private static HWND _hwnd_shell;
private static HWND HWND_SHELL =>
_hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow();
private static HWND _hwnd_desktop;
private static HWND HWND_DESKTOP =>
_hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow();
public static unsafe bool IsForegroundWindowFullscreen()
{
// Get current active window
var hWnd = PInvoke.GetForegroundWindow();
if (hWnd.Equals(HWND.Null))
{
return false;
}
return false;
// If current active window is desktop or shell, exit early
if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))
{
return false;
}
string windowClass;
const int capacity = 256;
Span<char> buffer = stackalloc char[capacity];
int validLength;
fixed (char* pBuffer = buffer)
{
validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity);
}
windowClass = buffer[..validLength].ToString();
// For Win+Tab (Flip3D)
if (windowClass == WINDOW_CLASS_WINTAB)
{
return false;
}
PInvoke.GetWindowRect(hWnd, out var appBounds);
// For console (ConsoleWindowClass), we have to check for negative dimensions
if (windowClass == WINDOW_CLASS_CONSOLE)
{
return appBounds.top < 0 && appBounds.bottom < 0;
}
// For desktop (Progman or WorkerW, depends on the system), we have to check
if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW)
{
var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
if (hWndDesktop.Value != IntPtr.Zero)
{
return false;
}
}
var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd);
return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height &&
(appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width;
}
#endregion
#region Pixel to DIP
/// <summary>
/// Transforms pixels to Device Independent Pixels used by WPF
/// </summary>
/// <param name="visual">current window, required to get presentation source</param>
/// <param name="unitX">horizontal position in pixels</param>
/// <param name="unitY">vertical position in pixels</param>
/// <returns>point containing device independent pixels</returns>
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
{
Matrix matrix;
var source = PresentationSource.FromVisual(visual);
if (source is not null)
{
matrix = source.CompositionTarget.TransformFromDevice;
}
else
{
using var src = new HwndSource(new HwndSourceParameters());
matrix = src.CompositionTarget.TransformFromDevice;
}
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
}
#endregion
#region WndProc
public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE;
public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE;
#endregion
#region Window Handle
internal static HWND GetWindowHandle(Window window, bool ensure = false)
{
var windowHelper = new WindowInteropHelper(window);
if (ensure)
{
windowHelper.EnsureHandle();
}
return new(windowHelper.Handle);
}
#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>
/// Sets the blur for a window via SetWindowCompositionAttribute
/// 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 SetBlurForWindow(Window w, bool blur)
public static void RestorePreviousKeyboardLayout()
{
SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED);
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;
}
private static void SetWindowAccent(Window w, AccentState state)
/// <summary>
/// Finds an installed English keyboard layout.
/// </summary>
/// <returns></returns>
/// <exception cref="Win32Exception"></exception>
private static unsafe HKL FindEnglishKeyboardLayout()
{
var windowHelper = new WindowInteropHelper(w);
// Get the number of keyboard layouts
int count = PInvoke.GetKeyboardLayoutList(0, null);
if (count <= 0) return HKL.Null;
windowHelper.EnsureHandle();
var accent = new AccentPolicy { AccentState = state };
var accentStructSize = Marshal.SizeOf(accent);
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
Marshal.StructureToPtr(accent, accentPtr, false);
var data = new WindowCompositionAttributeData
// Get all keyboard layouts
var handles = new HKL[count];
fixed (HKL* h = handles)
{
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
SizeOfData = accentStructSize,
Data = accentPtr
};
var result = PInvoke.GetKeyboardLayoutList(count, h);
if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
}
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
// 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);
Marshal.FreeHGlobal(accentPtr);
// Check if it's an English layout
if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase))
{
return hkl;
}
}
return HKL.Null;
}
/// <summary>
/// Returns the
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
/// BCP 47 language tag
/// </see>
/// of the current input language.
/// </summary>
/// <remarks>
/// Edited from: https://github.com/dotnet/winforms
/// </remarks>
private static string GetLanguageTag(uint langId)
{
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
// transient value.
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
//
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
//
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
//
// NOTE: this logic may break in future versions of Windows since it is not documented.
if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1
or PInvoke.LOCALE_TRANSIENT_KEYBOARD2
or PInvoke.LOCALE_TRANSIENT_KEYBOARD3
or PInvoke.LOCALE_TRANSIENT_KEYBOARD4)
{
using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath);
if (key?.GetValue("Languages") is string[] languages)
{
foreach (string language in languages)
{
using var subKey = key.OpenSubKey(language);
if (subKey?.GetValue("TransientLangId") is int transientLangId
&& transientLangId == langId)
{
return language;
}
}
}
}
return CultureInfo.GetCultureInfo((int)langId).Name;
}
#endregion
}
}

View file

@ -152,7 +152,6 @@ namespace Flow.Launcher.Plugin
/// <param name="callback"></param>
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
/// <summary>
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
/// </summary>
@ -191,14 +190,15 @@ namespace Flow.Launcher.Plugin
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default);
/// <summary>
/// Add ActionKeyword for specific plugin
/// Add ActionKeyword and update action keyword metadata for specific plugin
/// Before adding, please check if action keyword is already assigned by <see cref="ActionKeywordAssigned"/>
/// </summary>
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
void AddActionKeyword(string pluginId, string newActionKeyword);
/// <summary>
/// Remove ActionKeyword for specific plugin
/// Remove ActionKeyword and update action keyword metadata for specific plugin
/// </summary>
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
/// <param name="oldActionKeyword">The actionkeyword that is supposed to be removed</param>

View file

@ -34,6 +34,8 @@ namespace Flow.Launcher.Plugin
public List<string> ActionKeywords { get; set; }
public bool HideActionKeywordPanel { get; set; }
public string IcoPath { get; set;}
public override string ToString()

View file

@ -39,10 +39,9 @@ namespace Flow.Launcher.Plugin
public const string TermSeparator = " ";
/// <summary>
/// User can set multiple action keywords seperated by ';'
/// User can set multiple action keywords seperated by whitespace
/// </summary>
public const string ActionKeywordSeparator = ";";
public const string ActionKeywordSeparator = TermSeparator;
/// <summary>
/// Wildcard action keyword. Plugins using this value will be queried on every search.

View file

@ -3,6 +3,8 @@ using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using Flow.Launcher.Core;
using System.Linq;
using System.Collections.Generic;
namespace Flow.Launcher
{
@ -32,14 +34,37 @@ namespace Flow.Launcher
private void btnDone_OnClick(object sender, RoutedEventArgs _)
{
var oldActionKeyword = plugin.Metadata.ActionKeywords[0];
var newActionKeyword = tbAction.Text.Trim();
newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*";
if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword))
var oldActionKeywords = plugin.Metadata.ActionKeywords;
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
newActionKeywords.RemoveAll(string.IsNullOrEmpty);
newActionKeywords = newActionKeywords.Distinct().ToList();
newActionKeywords = newActionKeywords.Count > 0 ? newActionKeywords : new() { Query.GlobalPluginWildcardSign };
var addedActionKeywords = newActionKeywords.Except(oldActionKeywords).ToList();
var removedActionKeywords = oldActionKeywords.Except(newActionKeywords).ToList();
if (!addedActionKeywords.Any(App.API.ActionKeywordAssigned))
{
pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword);
Close();
if (oldActionKeywords.Count != newActionKeywords.Count)
{
ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
return;
}
var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
{
// User just changes the sequence of action keywords
var msg = translater.GetTranslation("newActionKeywordsSameAsOld");
MessageBoxEx.Show(msg);
}
else
{
ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
}
}
else
{
@ -47,5 +72,21 @@ namespace Flow.Launcher
App.API.ShowMsgBox(msg);
}
}
private void ReplaceActionKeyword(string id, IReadOnlyList<string> removedActionKeywords, IReadOnlyList<string> addedActionKeywords)
{
foreach (var actionKeyword in removedActionKeywords)
{
App.API.RemoveActionKeyword(id, actionKeyword);
}
foreach (var actionKeyword in addedActionKeywords)
{
App.API.AddActionKeyword(id, actionKeyword);
}
// Update action keywords text and close window
pluginViewModel.OnActionKeywordsChanged();
Close();
}
}
}

View file

@ -4,7 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
ShutdownMode="OnMainWindowClose"
Startup="OnStartupAsync">
Startup="OnStartup">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>

View file

@ -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,37 +94,47 @@ 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();
application.Run();
}
using var application = new App();
application.InitializeComponent();
application.Run();
}
}
private async void OnStartupAsync(object sender, StartupEventArgs e)
#endregion
#region App Events
#pragma warning disable VSTHRD100 // Avoid async void methods
private async void OnStartup(object sender, StartupEventArgs e)
{
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
{
@ -127,29 +152,32 @@ namespace Flow.Launcher
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
InternationalizationManager.Instance.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 mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
var window = new MainWindow(_settings, mainVM);
_mainWindow = new MainWindow();
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
Current.MainWindow = window;
Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher;
HotKeyMapper.Initialize();
// main windows needs initialized before theme change because of blur settings
// TODO: Clean ThemeManager.Instance in future
ThemeManager.Instance.ChangeTheme(_settings.Theme);
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@ -159,11 +187,12 @@ namespace Flow.Launcher
AutoUpdates();
API.SaveAppAllSettings();
Log.Info(
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
});
}
#pragma warning restore VSTHRD100 // Avoid async void methods
private void AutoStartup()
{
// we try to enable auto-startup on first launch, or reenable if it was removed
@ -186,13 +215,11 @@ namespace Flow.Launcher
// but if it fails (permissions, etc) then don't keep retrying
// this also gives the user a visual indication in the Settings widget
_settings.StartFlowLauncherOnSystemStartup = false;
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
e.Message);
API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
}
}
}
//[Conditional("RELEASE")]
private void AutoUpdates()
{
_ = Task.Run(async () =>
@ -210,11 +237,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 +280,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
}
}

View file

@ -58,9 +58,9 @@
</Button>
</Grid>
</StackPanel>
<StackPanel Margin="26,0,26,0">
<StackPanel Margin="26 0 26 0">
<TextBlock
Margin="0,0,0,12"
Margin="0 0 0 12"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource customeQueryHotkeyTitle}"
@ -72,10 +72,10 @@
TextWrapping="WrapWithOverflow" />
<Image
Width="478"
Margin="0,20,0,0"
Margin="0 20 0 0"
Source="/Images/illustration_01.png" />
<Grid Width="478" Margin="0,20,0,0">
<Grid Width="478" Margin="0 20 0 0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
@ -99,11 +99,12 @@
Grid.Row="0"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="10,0,10,0"
Margin="10 0 10 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
DefaultHotkey="" />
DefaultHotkey=""
Type="CustomQueryHotkey" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
@ -123,8 +124,8 @@
x:Name="btnTestActionKeyword"
Grid.Row="1"
Grid.Column="2"
Margin="0,0,10,0"
Padding="10,5,10,5"
Margin="0 0 10 0"
Padding="10 5 10 5"
Click="BtnTestActionKeyword_OnClick"
Content="{DynamicResource preview}" />
</Grid>
@ -132,21 +133,21 @@
</StackPanel>
<Border
Grid.Row="1"
Margin="0,14,0,0"
Margin="0 14 0 0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0">
BorderThickness="0 1 0 0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
MinWidth="140"
Margin="10,0,5,0"
Margin="10 0 5 0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
<Button
x:Name="btnAdd"
MinWidth="140"
Margin="5,0,10,0"
Margin="5 0 10 0"
Click="btnAdd_OnClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />

View file

@ -94,10 +94,6 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
<PackageReference Include="ModernWpfUI" Version="0.9.4" />

View file

@ -1,66 +0,0 @@
using System;
using System.Windows;
using System.Windows.Interop;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Controls;
namespace Flow.Launcher.Helper;
public class DwmDropShadow
{
/// <summary>
/// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven).
/// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
/// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window).
/// </summary>
/// <param name="window">Window to which the shadow will be applied</param>
public static void DropShadowToWindow(Window window)
{
if (!DropShadow(window))
{
window.SourceInitialized += window_SourceInitialized;
}
}
private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo
{
Window window = (Window)sender;
DropShadow(window);
window.SourceInitialized -= window_SourceInitialized;
}
/// <summary>
/// The actual method that makes API calls to drop the shadow to the window
/// </summary>
/// <param name="window">Window to which the shadow will be applied</param>
/// <returns>True if the method succeeded, false if not</returns>
private static unsafe bool DropShadow(Window window)
{
try
{
WindowInteropHelper helper = new WindowInteropHelper(window);
int val = 2;
var ret1 = PInvoke.DwmSetWindowAttribute(new (helper.Handle), DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY, &val, 4);
if (ret1 == HRESULT.S_OK)
{
var m = new MARGINS { cyBottomHeight = 0, cxLeftWidth = 0, cxRightWidth = 0, cyTopHeight = 0 };
var ret2 = PInvoke.DwmExtendFrameIntoClientArea(new(helper.Handle), &m);
return ret2 == HRESULT.S_OK;
}
return false;
}
catch (Exception)
{
// Probably dwmapi.dll not found (incompatible OS)
return false;
}
}
}

View file

@ -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>

View file

@ -2,22 +2,19 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Flow.Launcher.Infrastructure;
using Microsoft.Win32;
using Windows.Win32;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Flow.Launcher.Helper;
public static class WallpaperPathRetrieval
{
private static readonly int MAX_PATH = 260;
private static readonly int MAX_CACHE_SIZE = 3;
private static readonly Dictionary<(string, DateTime), ImageBrush> wallpaperCache = new();
private const int MaxCacheSize = 3;
private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new();
private static readonly object CacheLock = new();
public static Brush GetWallpaperBrush()
{
@ -29,47 +26,72 @@ public static class WallpaperPathRetrieval
try
{
var wallpaperPath = GetWallpaperPath();
if (wallpaperPath is not null && File.Exists(wallpaperPath))
var wallpaperPath = Win32Helper.GetWallpaperPath();
if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath))
{
// Since the wallpaper file name can be the same (TranscodedWallpaper),
// we need to add the last modified date to differentiate them
var dateModified = File.GetLastWriteTime(wallpaperPath);
wallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper);
App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Wallpaper path is invalid: {wallpaperPath}");
var wallpaperColor = GetWallpaperColor();
return new SolidColorBrush(wallpaperColor);
}
// Since the wallpaper file name can be the same (TranscodedWallpaper),
// we need to add the last modified date to differentiate them
var dateModified = File.GetLastWriteTime(wallpaperPath);
lock (CacheLock)
{
WallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper);
if (cachedWallpaper != null)
{
return cachedWallpaper;
}
}
using var fileStream = File.OpenRead(wallpaperPath);
var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
var frame = decoder.Frames[0];
var originalWidth = frame.PixelWidth;
var originalHeight = frame.PixelHeight;
// We should not dispose the memory stream since the bitmap is still in use
var memStream = new MemoryStream(File.ReadAllBytes(wallpaperPath));
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = memStream;
bitmap.DecodePixelWidth = 800;
bitmap.DecodePixelHeight = 600;
bitmap.EndInit();
bitmap.Freeze(); // Make the bitmap thread-safe
var wallpaperBrush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill };
wallpaperBrush.Freeze(); // Make the brush thread-safe
if (originalWidth == 0 || originalHeight == 0)
{
App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}");
return new SolidColorBrush(Colors.Transparent);
}
// Manage cache size
if (wallpaperCache.Count >= MAX_CACHE_SIZE)
// Calculate the scaling factor to fit the image within 800x600 while preserving aspect ratio
var widthRatio = 800.0 / originalWidth;
var heightRatio = 600.0 / originalHeight;
var scaleFactor = Math.Min(widthRatio, heightRatio);
var decodedPixelWidth = (int)(originalWidth * scaleFactor);
var decodedPixelHeight = (int)(originalHeight * scaleFactor);
// Set DecodePixelWidth and DecodePixelHeight to resize the image while preserving aspect ratio
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(wallpaperPath);
bitmap.DecodePixelWidth = decodedPixelWidth;
bitmap.DecodePixelHeight = decodedPixelHeight;
bitmap.EndInit();
bitmap.Freeze(); // Make the bitmap thread-safe
var wallpaperBrush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill };
wallpaperBrush.Freeze(); // Make the brush thread-safe
// Manage cache size
lock (CacheLock)
{
if (WallpaperCache.Count >= MaxCacheSize)
{
// Remove the oldest wallpaper from the cache
var oldestCache = wallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault();
var oldestCache = WallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault();
if (oldestCache != default)
{
wallpaperCache.Remove(oldestCache);
WallpaperCache.Remove(oldestCache);
}
}
wallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush);
WallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush);
return wallpaperBrush;
}
var wallpaperColor = GetWallpaperColor();
return new SolidColorBrush(wallpaperColor);
}
catch (Exception ex)
{
@ -78,20 +100,9 @@ public static class WallpaperPathRetrieval
}
}
private static unsafe string GetWallpaperPath()
{
var wallpaperPtr = stackalloc char[MAX_PATH];
PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, (uint)MAX_PATH,
wallpaperPtr,
0);
var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr);
return wallpaper.ToString();
}
private static Color GetWallpaperColor()
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true);
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false);
var result = key?.GetValue("Background", null);
if (result is string strResult)
{
@ -100,8 +111,9 @@ public static class WallpaperPathRetrieval
var parts = strResult.Trim().Split(new[] { ' ' }, 3).Select(byte.Parse).ToList();
return Color.FromRgb(parts[0], parts[1], parts[2]);
}
catch
catch (Exception ex)
{
App.API.LogException(nameof(WallpaperPathRetrieval), "Error parsing wallpaper color", ex);
}
}

View file

@ -1,211 +0,0 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows.Media;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using Point = System.Windows.Point;
namespace Flow.Launcher.Helper;
public class WindowsInteropHelper
{
private static HWND _hwnd_shell;
private static HWND _hwnd_desktop;
//Accessors for shell and desktop handlers
//Will set the variables once and then will return them
private static HWND HWND_SHELL
{
get
{
return _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow();
}
}
private static HWND HWND_DESKTOP
{
get
{
return _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow();
}
}
const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
const string WINDOW_CLASS_WINTAB = "Flip3D";
const string WINDOW_CLASS_PROGMAN = "Progman";
const string WINDOW_CLASS_WORKERW = "WorkerW";
public unsafe static bool IsWindowFullscreen()
{
//get current active window
var hWnd = PInvoke.GetForegroundWindow();
if (hWnd.Equals(HWND.Null))
{
return false;
}
//if current active window is desktop or shell, exit early
if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))
{
return false;
}
string windowClass;
const int capacity = 256;
Span<char> buffer = stackalloc char[capacity];
int validLength;
fixed (char* pBuffer = buffer)
{
validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity);
}
windowClass = buffer[..validLength].ToString();
//for Win+Tab (Flip3D)
if (windowClass == WINDOW_CLASS_WINTAB)
{
return false;
}
PInvoke.GetWindowRect(hWnd, out var appBounds);
//for console (ConsoleWindowClass), we have to check for negative dimensions
if (windowClass == WINDOW_CLASS_CONSOLE)
{
return appBounds.top < 0 && appBounds.bottom < 0;
}
//for desktop (Progman or WorkerW, depends on the system), we have to check
if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW)
{
var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
if (hWndDesktop.Value != (IntPtr.Zero))
{
return false;
}
}
Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds;
return (appBounds.bottom - appBounds.top) == screenBounds.Height &&
(appBounds.right - appBounds.left) == screenBounds.Width;
}
/// <summary>
/// disable windows toolbar's control box
/// this will also disable system menu with Alt+Space hotkey
/// </summary>
public static void DisableControlBox(Window win)
{
var hwnd = new HWND(new WindowInteropHelper(win).Handle);
var style = PInvoke.GetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
if (style == 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
style &= ~(int)WINDOW_STYLE.WS_SYSMENU;
var previousStyle = PInvoke.SetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE,
style);
if (previousStyle == 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
}
/// <summary>
/// Transforms pixels to Device Independent Pixels used by WPF
/// </summary>
/// <param name="visual">current window, required to get presentation source</param>
/// <param name="unitX">horizontal position in pixels</param>
/// <param name="unitY">vertical position in pixels</param>
/// <returns>point containing device independent pixels</returns>
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
{
Matrix matrix;
var source = PresentationSource.FromVisual(visual);
if (source is not null)
{
matrix = source.CompositionTarget.TransformFromDevice;
}
else
{
using var src = new HwndSource(new HwndSourceParameters());
matrix = src.CompositionTarget.TransformFromDevice;
}
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
}
#region Alt Tab
private static int SetWindowLong(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong)
{
PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error
var result = PInvoke.SetWindowLong(hWnd, nIndex, dwNewLong);
if (result == 0 && Marshal.GetLastPInvokeError() != 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
return result;
}
/// <summary>
/// Hide windows in the Alt+Tab window list
/// </summary>
/// <param name="window">To hide a window</param>
public static void HideFromAltTab(Window window)
{
var exStyle = GetCurrentWindowStyle(window);
// Add TOOLWINDOW style, remove APPWINDOW style
var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
}
/// <summary>
/// Restore window display in the Alt+Tab window list.
/// </summary>
/// <param name="window">To restore the displayed window</param>
public static void ShowInAltTab(Window window)
{
var exStyle = GetCurrentWindowStyle(window);
// Remove the TOOLWINDOW style and add the APPWINDOW style.
var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
}
/// <summary>
/// To obtain the current overridden style of a window.
/// </summary>
/// <param name="window">To obtain the style dialog window</param>
/// <returns>current extension style value</returns>
private static int GetCurrentWindowStyle(Window window)
{
var style = PInvoke.GetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
if (style == 0 && Marshal.GetLastPInvokeError() != 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
return style;
}
#endregion
}

View file

@ -84,7 +84,7 @@ namespace Flow.Launcher
nameof(Type),
typeof(HotkeyType),
typeof(HotkeyControl),
new FrameworkPropertyMetadata(HotkeyType.Hotkey, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged)
new FrameworkPropertyMetadata(HotkeyType.None, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged)
);
public HotkeyType Type
@ -95,6 +95,10 @@ namespace Flow.Launcher
public enum HotkeyType
{
None,
// Custom query hotkeys
CustomQueryHotkey,
// Settings hotkeys
Hotkey,
PreviewHotkey,
OpenContextMenuHotkey,
@ -115,12 +119,16 @@ namespace Flow.Launcher
// and it will not construct settings instances twice
private static readonly Settings _settings = Ioc.Default.GetRequiredService<Settings>();
private string hotkey = string.Empty;
public string Hotkey
{
get
{
return Type switch
{
// Custom query hotkeys
HotkeyType.CustomQueryHotkey => hotkey,
// Settings hotkeys
HotkeyType.Hotkey => _settings.Hotkey,
HotkeyType.PreviewHotkey => _settings.PreviewHotkey,
HotkeyType.OpenContextMenuHotkey => _settings.OpenContextMenuHotkey,
@ -135,13 +143,20 @@ namespace Flow.Launcher
HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2,
HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey,
HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2,
_ => string.Empty
_ => throw new System.NotImplementedException("Hotkey type not set")
};
}
set
{
switch (Type)
{
// Custom query hotkeys
case HotkeyType.CustomQueryHotkey:
// We just need to store it in a local field
// because we will save to settings in other place
hotkey = value;
break;
// Settings hotkeys
case HotkeyType.Hotkey:
_settings.Hotkey = value;
break;
@ -185,7 +200,7 @@ namespace Flow.Launcher
_settings.SelectNextItemHotkey2 = value;
break;
default:
return;
throw new System.NotImplementedException("Hotkey type not set");
}
// After setting the hotkey, we need to refresh the interface
@ -199,13 +214,14 @@ namespace Flow.Launcher
HotkeyList.ItemsSource = KeysToDisplay;
RefreshHotkeyInterface(Hotkey);
// We should not call RefreshHotkeyInterface here because DependencyProperty is not set yet
// And it will be called in OnHotkeyChanged event or Hotkey setter later
}
private void RefreshHotkeyInterface(string hotkey)
{
SetKeysToDisplay(new HotkeyModel(Hotkey));
CurrentHotkey = new HotkeyModel(Hotkey);
SetKeysToDisplay(new HotkeyModel(hotkey));
CurrentHotkey = new HotkeyModel(hotkey);
}
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>

View file

@ -39,7 +39,7 @@
<system:String x:Key="GameMode">Game Mode</system:String>
<system:String x:Key="GameModeToolTip">Suspend the use of Hotkeys.</system:String>
<system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Settings</system:String>
@ -72,8 +72,6 @@
<system:String x:Key="LastQueryEmpty">Empty last Query</system:String>
<system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String>
<system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Height</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window height is not adjustable by dragging.</system:String>
<system:String x:Key="maxShowResults">Maximum results shown</system:String>
<system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
@ -195,8 +193,19 @@
<system:String x:Key="AnimationSpeedCustom">Custom</system:String>
<system:String x:Key="Clock">Clock</system:String>
<system:String x:Key="Date">Date</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropTypesNone">None</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String>
<system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="PlaceholderText">Placeholder text</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<!-- Setting Hotkey -->
<system:String x:Key="hotkey">Hotkey</system:String>
@ -337,9 +346,10 @@
<system:String x:Key="cannotFindSpecifiedPlugin">Can't find specified plugin</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
<system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String>
<system:String x:Key="success">Success</system:String>
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
<system:String x:Key="actionkeyword_tips">Enter the action keyword you like to use to start the plugin. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
<system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
<!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">Custom Query Hotkey</system:String>

View file

@ -17,20 +17,20 @@
AllowDrop="True"
AllowsTransparency="True"
Background="Transparent"
Closed="OnClosed"
Closing="OnClosing"
Deactivated="OnDeactivated"
Icon="Images/app.png"
SourceInitialized="OnSourceInitialized"
Initialized="OnInitialized"
Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Loaded="OnLoaded"
LocationChanged="OnLocationChanged"
Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
PreviewKeyDown="OnKeyDown"
PreviewKeyUp="OnKeyUp"
PreviewMouseMove="OnPreviewMouseMove"
ResizeMode="CanResize"
ShowInTaskbar="False"
SizeToContent="Height"
SourceInitialized="OnSourceInitialized"
Topmost="True"
Visibility="{Binding MainWindowVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
WindowStartupLocation="Manual"
@ -215,9 +215,17 @@
<Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}">
<StackPanel Orientation="Vertical">
<Grid>
<Grid x:Name="QueryBoxArea">
<Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}">
<Grid>
<TextBox
x:Name="QueryTextPlaceholderBox"
Height="{Binding MainWindowHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
FontSize="{Binding QueryBoxFontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="False"
Style="{DynamicResource QuerySuggestionBoxStyle}"
Text="{Binding PlaceholderText, Mode=OneWay}"
Visibility="Collapsed" />
<TextBox
x:Name="QueryTextSuggestionBox"
Height="{Binding MainWindowHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
@ -240,14 +248,14 @@
FontSize="{Binding QueryBoxFontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
InputMethod.PreferredImeConversionMode="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEConversionModeConverter}}"
InputMethod.PreferredImeState="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEStateConverter}}"
PreviewDragOver="OnPreviewDragOver"
PreviewDragOver="QueryTextBox_OnPreviewDragOver"
PreviewKeyUp="QueryTextBox_KeyUp"
Style="{DynamicResource QueryBoxStyle}"
Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Visibility="Visible"
WindowChrome.IsHitTestVisibleInChrome="True">
<TextBox.CommandBindings>
<CommandBinding Command="ApplicationCommands.Copy" Executed="OnCopy" />
<CommandBinding Command="ApplicationCommands.Copy" Executed="QueryTextBox_OnCopy" />
</TextBox.CommandBindings>
<TextBox.ContextMenu>
<ContextMenu MinWidth="160">
@ -289,7 +297,9 @@
<StackPanel
x:Name="ClockPanel"
IsHitTestVisible="False"
Style="{DynamicResource ClockPanel}">
Opacity="{Binding ClockPanelOpacity}"
Style="{DynamicResource ClockPanel}"
Visibility="{Binding ClockPanelVisibility}">
<TextBlock
x:Name="ClockBox"
Style="{DynamicResource ClockBox}"
@ -317,6 +327,7 @@
Name="SearchIcon"
Margin="0"
Data="{DynamicResource SearchIconImg}"
Opacity="{Binding SearchIconOpacity}"
Stretch="Fill"
Style="{DynamicResource SearchIconStyle}"
Visibility="{Binding SearchIconVisibility}" />
@ -338,7 +349,7 @@
Y2="0" />
</Grid>
<Grid ClipToBounds="True">
<Grid x:Name="MiddleSeparatorArea" ClipToBounds="True">
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
@ -377,8 +388,8 @@
Style="{DynamicResource SeparatorStyle}" />
</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 +397,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 +431,9 @@
RightClickResultCommand="{Binding RightClickResultCommand}" />
</ContentControl>
</StackPanel>
<GridSplitter
x:Name="PreviewMiddleSeparator"
Grid.Column="1"
Margin="0"
HorizontalAlignment="Center"
@ -432,6 +447,7 @@
</ControlTemplate>
</GridSplitter.Template>
</GridSplitter>
<Grid
x:Name="Preview"
Grid.Column="2"
@ -441,7 +457,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 +534,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>

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
using System;
using System;
using System.IO;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media.Animation;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Image;
@ -12,14 +11,14 @@ namespace Flow.Launcher
{
public partial class Msg : Window
{
Storyboard fadeOutStoryboard = new Storyboard();
private readonly Storyboard fadeOutStoryboard = new();
private bool closing;
public Msg()
{
InitializeComponent();
var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var dipWorkingArea = WindowsInteropHelper.TransformPixelsToDIP(this,
var dipWorkingArea = Win32Helper.TransformPixelsToDIP(this,
screen.WorkingArea.Width,
screen.WorkingArea.Height);
Left = dipWorkingArea.X - Width;
@ -82,13 +81,13 @@ namespace Flow.Launcher
Show();
await Dispatcher.InvokeAsync(async () =>
{
if (!closing)
{
closing = true;
await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin);
}
});
{
if (!closing)
{
closing = true;
await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin);
}
});
}
}
}

View file

@ -1,20 +0,0 @@
DwmSetWindowAttribute
DwmExtendFrameIntoClientArea
SystemParametersInfo
SetForegroundWindow
GetWindowLong
SetWindowLong
GetForegroundWindow
GetDesktopWindow
GetShellWindow
GetWindowRect
GetClassName
FindWindowEx
WINDOW_STYLE
WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE
SetLastError
WINDOW_EX_STYLE

View file

@ -1,46 +1,48 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Squirrel;
using Flow.Launcher.Core;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Plugin.SharedCommands;
using System.Threading;
using System.IO;
using Flow.Launcher.Infrastructure.Http;
using JetBrains.Annotations;
using System.Runtime.CompilerServices;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Collections.Specialized;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
using JetBrains.Annotations;
using Flow.Launcher.Core.Resource;
namespace Flow.Launcher
{
public class PublicAPIInstance : IPublicAPI
{
private readonly Settings _settings;
private readonly Internationalization _translater;
private readonly MainViewModel _mainVM;
#region Constructor
public PublicAPIInstance(Settings settings, MainViewModel mainVM)
public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM)
{
_settings = settings;
_translater = translater;
_mainVM = mainVM;
GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback;
WebRequest.RegisterPrefix("data", new DataWebRequestFactory());
@ -153,17 +155,17 @@ namespace Flow.Launcher
public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;
public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key);
public string GetTranslation(string key) => _translater.GetTranslation(key);
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
public MatchResult FuzzySearch(string query, string stringToCompare) =>
StringMatcher.FuzzySearch(query, stringToCompare);
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url);
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url, token);
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) =>
Http.GetStreamAsync(url);
Http.GetStreamAsync(url, token);
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null,
CancellationToken token = default) => Http.DownloadAsync(url, filePath, reportProgress, token);
@ -329,7 +331,6 @@ namespace Flow.Launcher
return _mainVM.GameModeStatus;
}
private readonly List<Func<int, int, SpecialKeyState, bool>> _globalKeyboardHandlers = new();
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) => _globalKeyboardHandlers.Add(callback);

View file

@ -11,30 +11,33 @@
mc:Ignorable="d">
<Border
Width="Auto"
Height="52"
Height="Auto"
Margin="0"
Padding="0"
BorderThickness="0 1 0 0"
CornerRadius="0"
Style="{DynamicResource SettingGroupBox}"
Visibility="{Binding ActionKeywordsVisibility}">
<DockPanel Margin="22 0 18 0" VerticalAlignment="Center">
<DockPanel Margin="{StaticResource SettingPanelMargin}">
<TextBlock
Margin="48 0 10 0"
Margin="{StaticResource SettingPanelItemRightMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
DockPanel.Dock="Left"
Style="{StaticResource Glyph}">
&#xe819;
</TextBlock>
<TextBlock
Margin="{StaticResource SettingPanelItemRightMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
DockPanel.Dock="Left"
Style="{DynamicResource SettingTitleLabel}"
Text="{DynamicResource actionKeywords}" />
<!-- Here Margin="0 -4.5 0 -4.5" is to remove redundant top bottom margin from Margin="{StaticResource SettingPanelMargin}" -->
<Button
Width="100"
Height="34"
Margin="5 0 0 0"
Margin="0 -4.5 0 -4.5"
HorizontalAlignment="Right"
Command="{Binding SetActionKeywordsCommand}"
Content="{Binding ActionKeywordsText}"

View file

@ -5585,4 +5585,31 @@
</Setter.Value>
</Setter>
</Style>
<!-- Settings Panel -->
<Thickness x:Key="SettingPanelMargin">70,13.5,18,13.5</Thickness>
<Thickness x:Key="SettingPanelItemLeftMargin">9,0,0,0</Thickness>
<Thickness x:Key="SettingPanelItemRightMargin">0,0,9,0</Thickness>
<Thickness x:Key="SettingPanelItemTopBottomMargin">0,4.5,0,4.5</Thickness>
<Thickness x:Key="SettingPanelItemLeftTopBottomMargin">9,4.5,0,4.5</Thickness>
<Thickness x:Key="SettingPanelItemRightTopBottomMargin">0,4.5,9,4.5</Thickness>
<Style x:Key="SettingPanelSeparatorStyle" TargetType="Separator">
<Setter Property="Margin" Value="-70 13.5 -18 13.5" />
<Setter Property="Height" Value="1" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Background" Value="{DynamicResource Color03B}" />
</Style>
<system:Double x:Key="SettingPanelTextBoxMinWidth">180</system:Double>
<system:Double x:Key="SettingPanelPathTextBoxWidth">240</system:Double>
<system:Double x:Key="SettingPanelAreaTextBoxMinHeight">150</system:Double>
<Style x:Key="SettingPanelTextBlockDescriptionStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0 2 0 0" />
<Setter Property="Foreground" Value="{DynamicResource Color04B}" />
</Style>
</ResourceDictionary>

View file

@ -7,17 +7,17 @@
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<SolidColorBrush x:Key="SystemThemeBorder" Color="#3f3f3f" />
<SolidColorBrush x:Key="QuerySuggestionBoxForeground" Color="#4d4d4d" />
<SolidColorBrush x:Key="QuerySuggestionBoxForeground" Color="#33FFFFFF" />
<SolidColorBrush x:Key="QuerySelectionBrush" Color="#4a5459" />
<SolidColorBrush x:Key="SubTitleForeground" Color="#7b7b7b" />
<SolidColorBrush x:Key="SubTitleSelectedForeground" Color="#7b7b7b" />
<SolidColorBrush x:Key="SearchIconForeground" Color="#4d4d4d" />
<SolidColorBrush x:Key="SeparatorForeground" Color="#14ffffff" />
<SolidColorBrush x:Key="SubTitleForeground" Color="#33FFFFFF" />
<SolidColorBrush x:Key="SubTitleSelectedForeground" Color="#33FFFFFF" />
<SolidColorBrush x:Key="SearchIconForeground" Color="#33FFFFFF" />
<SolidColorBrush x:Key="SeparatorForeground" Color="#24FFFFFF" />
<SolidColorBrush x:Key="ThumbColor" Color="#9a9a9a" />
<SolidColorBrush x:Key="InlineHighlight" Color="#0078d7" />
<SolidColorBrush x:Key="HotkeyForeground" Color="#7b7b7b" />
<SolidColorBrush x:Key="HotkeySelectedForeground" Color="#7b7b7b" />
<SolidColorBrush x:Key="ClockDateForeground" Color="#4d4d4d" />
<SolidColorBrush x:Key="HotkeyForeground" Color="#33FFFFFF" />
<SolidColorBrush x:Key="HotkeySelectedForeground" Color="#33FFFFFF" />
<SolidColorBrush x:Key="ClockDateForeground" Color="#26FFFFFF" />
<Color x:Key="ItemSelectedBackgroundColorBrush">#198F8F8F</Color>
@ -115,6 +115,8 @@
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#FCE100" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#433519" />
<SolidColorBrush x:Key="InfoBarBD" Color="#19000000" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />
<SolidColorBrush x:Key="ButtonOutBorder" Color="Transparent" />
<SolidColorBrush x:Key="ButtonInsideBorder" Color="#3f3f3f" />

View file

@ -7,18 +7,18 @@
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<SolidColorBrush x:Key="SystemThemeBorder" Color="#aaaaaa" />
<SolidColorBrush x:Key="QuerySuggestionBoxForeground" Color="#d8d2d0" />
<SolidColorBrush x:Key="QuerySuggestionBoxForeground" Color="#51000000" />
<SolidColorBrush x:Key="QuerySelectionBrush" Color="#0a68d8" />
<SolidColorBrush x:Key="SubTitleForeground" Color="#818181" />
<SolidColorBrush x:Key="SubTitleSelectedForeground" Color="#72767d" />
<SolidColorBrush x:Key="SearchIconForeground" Color="#d3d3d3" />
<SolidColorBrush x:Key="SeparatorForeground" Color="#14000000" />
<SolidColorBrush x:Key="SearchIconForeground" Color="#20000000" />
<SolidColorBrush x:Key="SeparatorForeground" Color="#20000000" />
<SolidColorBrush x:Key="ThumbColor" Color="#868686" />
<SolidColorBrush x:Key="InlineHighlight" Color="#0078d7" />
<SolidColorBrush x:Key="HotkeyForeground" Color="#acacac" />
<SolidColorBrush x:Key="HotkeySelectedForeground" Color="#acacac" />
<SolidColorBrush x:Key="ClockDateForeground" Color="#d3d3d3" />
<Color x:Key="ItemSelectedBackgroundColorBrush">#198F8F8F</Color>
<SolidColorBrush x:Key="HotkeyForeground" Color="#51000000" />
<SolidColorBrush x:Key="HotkeySelectedForeground" Color="#51000000" />
<SolidColorBrush x:Key="ClockDateForeground" Color="#44000000" />
<Color x:Key="ItemSelectedBackgroundColorBrush">#0C000000</Color>
<SolidColorBrush x:Key="Color00B" Color="#fbfbfb" />
<SolidColorBrush x:Key="Color01B" Color="#f3f3f3" />
@ -107,6 +107,7 @@
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#FFF4CE" />
<SolidColorBrush x:Key="InfoBarBD" Color="#0F000000" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />
<SolidColorBrush x:Key="ButtonOutBorder" Color="#e5e5e5" />
<SolidColorBrush x:Key="ButtonInsideBorder" Color="#d3d3d3" />

View file

@ -110,8 +110,8 @@
<Border.Background>
<LinearGradientBrush StartPoint="0 0" EndPoint="1 1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#1494df" />
<GradientStop Offset="1.0" Color="#1073bd" />
<GradientStop Offset="0.0" Color="#2A4D8C" />
<GradientStop Offset="1.0" Color="#1E3160" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>

View file

@ -60,7 +60,7 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Border Width="450" Style="{DynamicResource WindowBorderStyle}">
<Border Width="450" Style="{DynamicResource PreviewWindowBorderStyle}">
<Border Style="{DynamicResource WindowRadius}">
<Grid>
<Grid.RowDefinitions>

View file

@ -58,10 +58,10 @@
<Border Grid.Row="0" HorizontalAlignment="Stretch">
<Border.Background>
<LinearGradientBrush StartPoint="0 0" EndPoint="1 1">
<LinearGradientBrush StartPoint="0 1" EndPoint="0 0">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#7b83eb" />
<GradientStop Offset="1.0" Color="#555dc0" />
<GradientStop Offset="0.0" Color="#E5F3F7" />
<GradientStop Offset="1.0" Color="#FAFAFD" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>

View file

@ -5,6 +5,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows.Media;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
@ -13,7 +14,6 @@ using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using ModernWpf;
using ThemeManager = Flow.Launcher.Core.Resource.ThemeManager;
using ThemeManagerForColorSchemeSwitch = ModernWpf.ThemeManager;
namespace Flow.Launcher.SettingPages.ViewModels;
@ -22,45 +22,68 @@ public partial class SettingsPaneThemeViewModel : BaseModel
{
private const string DefaultFont = "Segoe UI";
public Settings Settings { get; }
private readonly Theme _theme = Ioc.Default.GetRequiredService<Theme>();
public static string LinkHowToCreateTheme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme";
public static string LinkThemeGallery => "https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438";
private List<Theme.ThemeData> _themes;
public List<Theme.ThemeData> Themes => _themes ??= _theme.LoadAvailableThemes();
private Theme.ThemeData _selectedTheme;
public Theme.ThemeData SelectedTheme
{
get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == Settings.Theme);
get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.GetCurrentTheme());
set
{
_selectedTheme = value;
ThemeManager.Instance.ChangeTheme(value.FileNameWithoutExtension);
_theme.ChangeTheme(value.FileNameWithoutExtension);
if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect)
DropShadowEffect = false;
// Update UI state
OnPropertyChanged(nameof(BackdropType));
OnPropertyChanged(nameof(IsBackdropEnabled));
OnPropertyChanged(nameof(IsDropShadowEnabled));
OnPropertyChanged(nameof(DropShadowEffect));
_ = _theme.RefreshFrameAsync();
}
}
public bool IsBackdropEnabled
{
get
{
if (!Win32Helper.IsBackdropSupported()) return false;
return SelectedTheme?.HasBlur ?? false;
}
}
public bool IsDropShadowEnabled => !_theme.BlurEnabled;
public bool DropShadowEffect
{
get => Settings.UseDropShadowEffect;
set
{
if (ThemeManager.Instance.BlurEnabled && value)
if (_theme.BlurEnabled)
{
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed"));
// Always DropShadowEffect = true with blur theme
Settings.UseDropShadowEffect = true;
return;
}
// User can change shadow with non-blur theme.
if (value)
{
ThemeManager.Instance.AddDropShadowEffectToCurrentTheme();
_theme.AddDropShadowEffectToCurrentTheme();
}
else
{
ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme();
_theme.RemoveDropShadowEffectFromCurrentTheme();
}
Settings.UseDropShadowEffect = value;
OnPropertyChanged(nameof(DropShadowEffect));
}
}
@ -94,12 +117,25 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set => Settings.ResultSubItemFontSize = value;
}
private List<Theme.ThemeData> _themes;
public List<Theme.ThemeData> Themes => _themes ??= ThemeManager.Instance.LoadAvailableThemes();
public class ColorSchemeData : DropdownDataGeneric<ColorSchemes> { }
public List<ColorSchemeData> ColorSchemes { get; } = DropdownDataGeneric<ColorSchemes>.GetValues<ColorSchemeData>("ColorScheme");
public string ColorScheme
{
get => Settings.ColorScheme;
set
{
ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = value switch
{
Constant.Light => ApplicationTheme.Light,
Constant.Dark => ApplicationTheme.Dark,
Constant.System => null,
_ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme
};
Settings.ColorScheme = value;
_ = _theme.RefreshFrameAsync();
}
}
public List<string> TimeFormatList { get; } = new()
{
@ -174,6 +210,33 @@ public partial class SettingsPaneThemeViewModel : BaseModel
public class AnimationSpeedData : DropdownDataGeneric<AnimationSpeeds> { }
public List<AnimationSpeedData> AnimationSpeeds { get; } = DropdownDataGeneric<AnimationSpeeds>.GetValues<AnimationSpeedData>("AnimationSpeed");
public class BackdropTypeData : DropdownDataGeneric<BackdropTypes> { }
public List<BackdropTypeData> BackdropTypesList { get; } =
DropdownDataGeneric<BackdropTypes>.GetValues<BackdropTypeData>("BackdropTypes");
public BackdropTypes BackdropType
{
get => Enum.IsDefined(typeof(BackdropTypes), Settings.BackdropType)
? Settings.BackdropType
: BackdropTypes.None;
set
{
if (!Enum.IsDefined(typeof(BackdropTypes), value))
{
value = BackdropTypes.None;
}
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));
}
}
public bool UseSound
{
get => Settings.UseSound;
@ -196,6 +259,23 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set => Settings.SoundVolume = value;
}
public bool ShowPlaceholder
{
get => Settings.ShowPlaceholder;
set => Settings.ShowPlaceholder = value;
}
public string PlaceholderTextTip
{
get => string.Format(App.API.GetTranslation("PlaceholderTextTip"), App.API.GetTranslation("queryTextBoxPlaceholder"));
}
public string PlaceholderText
{
get => Settings.PlaceholderText;
set => Settings.PlaceholderText = value;
}
public bool UseClock
{
get => Settings.UseClock;
@ -219,37 +299,37 @@ public partial class SettingsPaneThemeViewModel : BaseModel
{
var results = new List<Result>
{
new Result
new()
{
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"),
Title = App.API.GetTranslation("SampleTitleExplorer"),
SubTitle = App.API.GetTranslation("SampleSubTitleExplorer"),
IcoPath = Path.Combine(
Constant.ProgramDirectory,
@"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png"
)
},
new Result
new()
{
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"),
Title = App.API.GetTranslation("SampleTitleWebSearch"),
SubTitle = App.API.GetTranslation("SampleSubTitleWebSearch"),
IcoPath = Path.Combine(
Constant.ProgramDirectory,
@"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png"
)
},
new Result
new()
{
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"),
Title = App.API.GetTranslation("SampleTitleProgram"),
SubTitle = App.API.GetTranslation("SampleSubTitleProgram"),
IcoPath = Path.Combine(
Constant.ProgramDirectory,
@"Plugins\Flow.Launcher.Plugin.Program\Images\program.png"
)
},
new Result
new()
{
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"),
Title = App.API.GetTranslation("SampleTitleProcessKiller"),
SubTitle = App.API.GetTranslation("SampleSubTitleProcessKiller"),
IcoPath = Path.Combine(
Constant.ProgramDirectory,
@"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png"
@ -281,7 +361,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set
{
Settings.QueryBoxFont = value.ToString();
ThemeManager.Instance.ChangeTheme(Settings.Theme);
_theme.UpdateFonts();
}
}
@ -303,7 +383,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
Settings.QueryBoxFontStretch = value.Stretch.ToString();
Settings.QueryBoxFontWeight = value.Weight.ToString();
Settings.QueryBoxFontStyle = value.Style.ToString();
ThemeManager.Instance.ChangeTheme(Settings.Theme);
_theme.UpdateFonts();
}
}
@ -325,7 +405,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set
{
Settings.ResultFont = value.ToString();
ThemeManager.Instance.ChangeTheme(Settings.Theme);
_theme.UpdateFonts();
}
}
@ -347,7 +427,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
Settings.ResultFontStretch = value.Stretch.ToString();
Settings.ResultFontWeight = value.Weight.ToString();
Settings.ResultFontStyle = value.Style.ToString();
ThemeManager.Instance.ChangeTheme(Settings.Theme);
_theme.UpdateFonts();
}
}
@ -355,9 +435,9 @@ public partial class SettingsPaneThemeViewModel : BaseModel
{
get
{
if (Fonts.SystemFontFamilies.Count(o =>
if (Fonts.SystemFontFamilies.Any(o =>
o.FamilyNames.Values != null &&
o.FamilyNames.Values.Contains(Settings.ResultSubFont)) > 0)
o.FamilyNames.Values.Contains(Settings.ResultSubFont)))
{
var font = new FontFamily(Settings.ResultSubFont);
return font;
@ -371,7 +451,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set
{
Settings.ResultSubFont = value.ToString();
ThemeManager.Instance.ChangeTheme(Settings.Theme);
_theme.UpdateFonts();
}
}
@ -392,34 +472,23 @@ public partial class SettingsPaneThemeViewModel : BaseModel
Settings.ResultSubFontStretch = value.Stretch.ToString();
Settings.ResultSubFontWeight = value.Weight.ToString();
Settings.ResultSubFontStyle = value.Style.ToString();
ThemeManager.Instance.ChangeTheme(Settings.Theme);
_theme.UpdateFonts();
}
}
public string ThemeImage => Constant.QueryTextBoxIconImagePath;
public SettingsPaneThemeViewModel(Settings settings)
{
Settings = settings;
}
[RelayCommand]
private void OpenThemesFolder()
{
App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes));
}
public void UpdateColorScheme()
{
ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = Settings.ColorScheme switch
{
Constant.Light => ApplicationTheme.Light,
Constant.Dark => ApplicationTheme.Dark,
Constant.System => null,
_ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme
};
}
public SettingsPaneThemeViewModel(Settings settings)
{
Settings = settings;
}
[RelayCommand]
public void Reset()
{

View file

@ -30,6 +30,7 @@
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.ScrollUnit="Pixel">
<StackPanel>
<!-- Page title -->
<TextBlock
Margin="5 23 0 5"
@ -38,6 +39,7 @@
Text="{DynamicResource appearance}"
TextAlignment="left"
Visibility="Collapsed" />
<!-- Theme Preview and Editor -->
<Grid>
<Grid.Style>
@ -68,6 +70,7 @@
<ColumnDefinition Width="8*" />
<ColumnDefinition MaxWidth="250" Style="{StaticResource SecondColumnStyle}" />
</Grid.ColumnDefinitions>
<!-- Theme Size Editor -->
<Border
Grid.Column="1"
@ -260,7 +263,8 @@
</StackPanel>
</ScrollViewer>
</Border>
<!-- Theme Preview -->
<!-- Theme preview -->
<Border
Grid.Column="0"
Background="{Binding PreviewBackground}"
@ -303,7 +307,7 @@
Width="400"
Margin="40 30 0 30"
SnapsToDevicePixels="True"
Style="{DynamicResource WindowBorderStyle}">
Style="{DynamicResource PreviewWindowBorderStyle}">
<Border BorderThickness="0" Style="{DynamicResource WindowRadius}">
<Grid>
<Grid.RowDefinitions>
@ -370,17 +374,6 @@
</DockPanel>
</Border>
</Grid>
<!-- Drop shadow effect -->
<cc:Card
Title="{DynamicResource queryWindowShadowEffect}"
Margin="0 8 0 0"
Icon="&#xeb91;"
Sub="{DynamicResource shadowEffectCPUUsage}">
<ui:ToggleSwitch
IsOn="{Binding DropShadowEffect}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<!-- Theme -->
<cc:ExCard
@ -469,22 +462,57 @@
</ListBox.Template>
</ListBox>
</cc:ExCard>
<cc:CardGroup Margin="0 10 0 0">
<!-- Backdrop effect -->
<cc:Card
Title="{DynamicResource BackdropType}"
Margin="0 0 0 0"
Icon="&#xeb42;">
<ComboBox
MinWidth="160"
VerticalAlignment="Center"
DisplayMemberPath="Display"
FontSize="14"
IsEnabled="{Binding IsBackdropEnabled}"
ItemsSource="{Binding BackdropTypesList}"
SelectedValue="{Binding BackdropType, Mode=TwoWay}"
SelectedValuePath="Value" />
<!-- ✅ 추가 -->
</cc:Card>
<!-- Drop shadow effect -->
<cc:Card
Title="{DynamicResource queryWindowShadowEffect}"
Margin="0 0 0 0"
Icon="&#xeb91;">
<ui:ToggleSwitch
IsEnabled="{Binding IsDropShadowEnabled}"
IsOn="{Binding DropShadowEffect}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
</cc:CardGroup>
<cc:HyperLink
Margin="10"
HorizontalAlignment="Right"
Text="{DynamicResource browserMoreThemes}"
Uri="{Binding LinkThemeGallery}" />
<!-- Fixed Height -->
<!-- Fixed size -->
<cc:CardGroup Margin="0 20 0 0">
<cc:Card
Title="{DynamicResource KeepMaxResults}"
Icon="&#xe8fd;"
Icon="&#xE744;"
Sub="{DynamicResource KeepMaxResultsToolTip}">
<ui:ToggleSwitch IsOn="{Binding KeepMaxResults}" />
<ui:ToggleSwitch
IsOn="{Binding KeepMaxResults}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource maxShowResults}"
Icon="&#xe8fd;"
Sub="{DynamicResource maxShowResultsToolTip}"
Visibility="{Binding KeepMaxResults, Converter={StaticResource BoolToVisibilityConverter}}">
<ComboBox
@ -506,6 +534,29 @@
OnContent="{DynamicResource enable}" />
</cc:Card>
<!-- Placeholder text -->
<cc:CardGroup Margin="0 14 0 0">
<cc:Card
Title="{DynamicResource ShowPlaceholder}"
Icon="&#xE82F;"
Sub="{DynamicResource ShowPlaceholderTip}">
<ui:ToggleSwitch
IsOn="{Binding ShowPlaceholder}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource PlaceholderText}"
Icon="&#xED59;"
Sub="{Binding PlaceholderTextTip}">
<TextBox
MinWidth="150"
Text="{Binding PlaceholderText}"
TextWrapping="NoWrap" />
</cc:Card>
</cc:CardGroup>
<!-- Time and date -->
<cc:CardGroup Margin="0 14 0 0">
<cc:Card Title="{DynamicResource Clock}" Icon="&#xec92;">
@ -667,9 +718,8 @@
DisplayMemberPath="Display"
FontSize="14"
ItemsSource="{Binding ColorSchemes}"
SelectedValue="{Binding Settings.ColorScheme}"
SelectedValuePath="Value"
SelectionChanged="Selector_OnSelectionChanged" />
SelectedValue="{Binding ColorScheme, Mode=TwoWay}"
SelectedValuePath="Value" />
</cc:Card>
<!-- Theme folder -->

View file

@ -1,4 +1,3 @@
using System.Windows.Controls;
using System.Windows.Navigation;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.SettingPages.ViewModels;
@ -23,9 +22,4 @@ public partial class SettingsPaneTheme : Page
base.OnNavigatedTo(e);
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
_viewModel.UpdateColorScheme();
}
}

View file

@ -18,6 +18,8 @@
Loaded="OnLoaded"
MouseDown="window_MouseDown"
ResizeMode="CanResize"
SnapsToDevicePixels="True"
UseLayoutRounding="True"
StateChanged="Window_StateChanged"
Top="{Binding SettingWindowTop, Mode=TwoWay}"
WindowStartupLocation="Manual"
@ -54,6 +56,7 @@
Width="16"
Height="16"
Margin="10 4 4 4"
RenderOptions.BitmapScalingMode="HighQuality"
Source="/Images/app.png" />
<TextBlock
Grid.Row="0"

View file

@ -4,7 +4,7 @@ using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Interop;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.SettingPages.Views;
@ -92,13 +92,13 @@ public partial class SettingWindow
{
if (WindowState == WindowState.Maximized)
{
MaximizeButton.Visibility = Visibility.Collapsed;
MaximizeButton.Visibility = Visibility.Hidden;
RestoreButton.Visibility = Visibility.Visible;
}
else
{
MaximizeButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Hidden;
}
}
@ -143,8 +143,8 @@ public partial class SettingWindow
private double WindowLeft()
{
var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}
@ -152,8 +152,8 @@ public partial class SettingWindow
private double WindowTop()
{
var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
var top = (dip2.Y - ActualHeight) / 2 + dip1.Y - 20;
return top;
}

View file

@ -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" />
@ -107,7 +106,7 @@
</Style>
<Style x:Key="BasePendingLineStyle" TargetType="{x:Type Line}">
<Setter Property="Stroke" Value="Blue" />
<Setter Property="Stroke" Value="{StaticResource SystemAccentColorLight1Brush}" />
</Style>
<Style x:Key="BaseClockPanelPosition" TargetType="{x:Type Canvas}" />
@ -165,29 +164,6 @@
BasedOn="{StaticResource BaseClockPanel}"
TargetType="{x:Type StackPanel}">
<Setter Property="Orientation" Value="Vertical" />
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=QueryTextBox, UpdateSourceTrigger=PropertyChanged, Path=Text.Length}" Value="0" />
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
<MultiDataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="0.0"
To="1"
Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
</MultiDataTrigger>
</Style.Triggers>
</Style>
<!-- Item Style -->
@ -204,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" />
@ -241,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" />
@ -417,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>
@ -458,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}">
@ -473,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>
@ -557,6 +531,14 @@
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style x:Key="PreviewWindowBorderStyle" TargetType="{x:Type Border}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="#2F2F2F" />
<Setter Property="Padding" Value="0 0 0 0" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="UseLayoutRounding" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
</Style>
<!-- Style for old version themes -->
<Style
x:Key="PreviewItemTitleStyle"

View file

@ -12,6 +12,9 @@
</ResourceDictionary.MergedDictionaries>
<system:Boolean x:Key="ThemeBlurEnabled">True</system:Boolean>
<system:String x:Key="SystemBG">Dark</system:String>
<Color x:Key="LightBG">#C7000000</Color>
<Color x:Key="DarkBG">#C7000000</Color>
<Style x:Key="WindowRadius" TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="0" />
@ -51,7 +54,6 @@
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="SeparatorStyle"
BasedOn="{StaticResource BaseSeparatorStyle}"
@ -139,8 +141,8 @@
BasedOn="{StaticResource BaseSearchIconStyle}"
TargetType="{x:Type Path}">
<Setter Property="Fill" Value="#ffffff" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Width" Value="30" />
<Setter Property="Height" Value="30" />
<Setter Property="Opacity" Value="0.2" />
</Style>
<Style x:Key="ItemHotkeyStyle" TargetType="{x:Type TextBlock}">

View file

@ -10,8 +10,11 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Themes/Base.xaml" />
</ResourceDictionary.MergedDictionaries>
<system:Boolean x:Key="ThemeBlurEnabled">True</system:Boolean>
<system:String x:Key="SystemBG">Dark</system:String>
<Color x:Key="LightBG">#B0000000</Color>
<Color x:Key="DarkBG">#B6000000</Color>
<Style x:Key="WindowRadius" TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="0" />
</Style>
@ -135,8 +138,8 @@
BasedOn="{StaticResource BaseSearchIconStyle}"
TargetType="{x:Type Path}">
<Setter Property="Fill" Value="#ffffff" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Width" Value="30" />
<Setter Property="Height" Value="30" />
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style x:Key="ItemHotkeyStyle" TargetType="{x:Type TextBlock}">

View file

@ -10,14 +10,24 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Themes/Base.xaml" />
</ResourceDictionary.MergedDictionaries>
<system:Boolean x:Key="ThemeBlurEnabled">True</system:Boolean>
<system:String x:Key="SystemBG">Light</system:String>
<Color x:Key="LightBG">#BFFAFAFA</Color>
<Color x:Key="DarkBG">#BFFAFAFA</Color>
<Style
x:Key="ItemGlyph"
BasedOn="{StaticResource BaseGlyphStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#FF000000" />
</Style>
<Style x:Key="ItemGlyphSelectedStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#FF000000" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Width" Value="25" />
<Setter Property="Height" Value="25" />
<Setter Property="FontSize" Value="25" />
</Style>
<Style x:Key="WindowRadius" TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="0" />
</Style>
@ -33,7 +43,9 @@
<Style
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}" />
TargetType="{x:Type TextBox}">
<Setter Property="Foreground" Value="#39000000" />
</Style>
<Style
x:Key="WindowBorderStyle"
@ -76,7 +88,7 @@
x:Key="ItemSubTitleStyle"
BasedOn="{StaticResource BaseItemSubTitleStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#818181" />
<Setter Property="Foreground" Value="#85000000" />
</Style>
<Style
x:Key="ItemTitleSelectedStyle"
@ -88,18 +100,18 @@
x:Key="ItemSubTitleSelectedStyle"
BasedOn="{StaticResource BaseItemSubTitleSelectedStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#72767d" />
<Setter Property="Foreground" Value="#85000000" />
</Style>
<SolidColorBrush
x:Key="ItemSelectedBackgroundColor"
Opacity="0.5"
Color="#ccd0d4" />
Opacity="0.6"
Color="#33000000" />
<Style
x:Key="SeparatorStyle"
BasedOn="{StaticResource BaseSeparatorStyle}"
TargetType="{x:Type Rectangle}">
<Setter Property="Fill" Value="#c6c6c6" />
<Setter Property="Fill" Value="#22000000" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="12 0 12 8" />
</Style>
@ -112,7 +124,7 @@
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border
Background="#bebebe"
Background="#3C000000"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="2"
@ -132,8 +144,8 @@
BasedOn="{StaticResource BaseSearchIconStyle}"
TargetType="{x:Type Path}">
<Setter Property="Fill" Value="#000000" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Width" Value="30" />
<Setter Property="Height" Value="30" />
<Setter Property="Opacity" Value="0.2" />
</Style>
<Style x:Key="ItemHotkeyStyle" TargetType="{x:Type TextBlock}">

View file

@ -1,7 +1,7 @@
<!--
Name: Circle
IsDark: True
HasBlur: False
HasBlur: True
-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
@ -12,6 +12,10 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Themes/Base.xaml" />
</ResourceDictionary.MergedDictionaries>
<system:Boolean x:Key="ThemeBlurEnabled">True</system:Boolean>
<system:String x:Key="SystemBG">Auto</system:String>
<Color x:Key="LightBG">#E5E6E6E6</Color>
<Color x:Key="DarkBG">#DF1F1F1F</Color>
<Style
x:Key="ItemGlyph"
BasedOn="{StaticResource BaseGlyphStyle}"
@ -23,7 +27,7 @@
BasedOn="{StaticResource BaseQueryBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Padding" Value="0 4 50 0" />
<Setter Property="Foreground" Value="{m:DynamicColor SystemControlPageTextBaseHighBrush}" />
<Setter Property="Foreground" Value="{m:DynamicColor SystemControlHighlightBaseHighBrush}" />
<Setter Property="FontSize" Value="18" />
<Setter Property="Height" Value="38" />
</Style>
@ -75,7 +79,7 @@
x:Key="SeparatorStyle"
BasedOn="{StaticResource BaseSeparatorStyle}"
TargetType="{x:Type Rectangle}">
<Setter Property="Fill" Value="{m:DynamicColor SystemControlBackgroundChromeMediumBrush}" />
<Setter Property="Fill" Value="{DynamicResource SeparatorForeground}" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="0 0 0 8" />
</Style>

View file

@ -29,7 +29,9 @@
<Style
x:Key="WindowBorderStyle"
BasedOn="{StaticResource BaseWindowBorderStyle}"
TargetType="{x:Type Border}" />
TargetType="{x:Type Border}">
<Setter Property="Background" Value="#2F2F2F" />
</Style>
<Style
x:Key="WindowStyle"
@ -89,7 +91,7 @@
TargetType="{x:Type Rectangle}">
<Setter Property="Fill" Value="#3b3b3b" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="8,0,8,8" />
<Setter Property="Margin" Value="8 0 8 8" />
</Style>
<Style
x:Key="PreviewBorderStyle"

View file

@ -28,7 +28,6 @@
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Foreground" Value="#72767d" />
<Setter Property="Background" Value="#36393f" />
<Setter Property="Height" Value="42" />
<Setter Property="FontSize" Value="24" />
<Setter Property="Padding" Value="0,0,66,0" />

View file

@ -24,7 +24,6 @@
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="#161614" />
<Setter Property="Foreground" Value="#a09b8c" />
<Setter Property="Padding" Value="0,0,66,0" />
<Setter Property="Height" Value="42" />

View file

@ -2,46 +2,44 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Themes/Base.xaml" />
</ResourceDictionary.MergedDictionaries>
<Thickness x:Key="ResultMargin">0 0 0 0</Thickness>
<Thickness x:Key="ResultMargin">0 0 0 4</Thickness>
<Style
x:Key="ItemGlyph"
BasedOn="{StaticResource BaseGlyphStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#0e172c" />
<Setter Property="Foreground" Value="#ffffff" />
</Style>
<Style
x:Key="QueryBoxStyle"
BasedOn="{StaticResource BaseQueryBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Foreground" Value="#0E172C" />
<Setter Property="CaretBrush" Value="#0E172C" />
<Setter Property="SelectionBrush" Value="#a786df" />
<Setter Property="Foreground" Value="#cc1081" />
<Setter Property="CaretBrush" Value="#cc1081" />
<Setter Property="SelectionBrush" Value="#e564b1" />
</Style>
<Style
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#E9819F" />
<Setter Property="Foreground" Value="#71114b" />
</Style>
<Style
x:Key="WindowBorderStyle"
BasedOn="{StaticResource BaseWindowBorderStyle}"
TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Background" Value="#fec7d7" />
<Setter Property="BorderThickness" Value="3" />
<Setter Property="BorderBrush" Value="#0e172c" />
<Setter Property="Background" Value="#1f1d1f" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="#000000" />
</Style>
<Style
x:Key="SeparatorStyle"
BasedOn="{StaticResource BaseSeparatorStyle}"
TargetType="{x:Type Rectangle}">
<Setter Property="Fill" Value="#0e172c" />
<Setter Property="Height" Value="3" />
<Setter Property="Margin" Value="0 0 0 0" />
<Setter Property="Fill" Value="#000000" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="12 0 12 4" />
</Style>
<Style
x:Key="WindowStyle"
@ -51,33 +49,34 @@
x:Key="PendingLineStyle"
BasedOn="{StaticResource BasePendingLineStyle}"
TargetType="{x:Type Line}">
<Setter Property="Stroke" Value="#a786df" />
<Setter Property="Stroke" Value="#cc1081" />
</Style>
<Style
x:Key="ItemTitleStyle"
BasedOn="{StaticResource BaseItemTitleStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#0e172c" />
<Setter Property="Foreground" Value="#f5f5f5" />
</Style>
<Style
x:Key="ItemSubTitleStyle"
BasedOn="{StaticResource BaseItemSubTitleStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#0e172c" />
<Setter Property="Foreground" Value="#c2c2c2" />
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style
x:Key="ItemTitleSelectedStyle"
BasedOn="{StaticResource BaseItemTitleSelectedStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#fffffe" />
<Setter Property="Foreground" Value="#f5f5f5" />
</Style>
<Style
x:Key="ItemSubTitleSelectedStyle"
BasedOn="{StaticResource BaseItemSubTitleSelectedStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#fffffe" />
<Setter Property="Foreground" Value="#ed92c9" />
</Style>
<SolidColorBrush x:Key="ItemSelectedBackgroundColor">#0e172c</SolidColorBrush>
<SolidColorBrush x:Key="ItemSelectedBackgroundColor">#cc1081</SolidColorBrush>
<Style
x:Key="ThumbStyle"
BasedOn="{StaticResource BaseThumbStyle}"
@ -86,7 +85,7 @@
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border
Background="#E9819F"
Background="#e564b1"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="2"
@ -109,56 +108,53 @@
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
<Style x:Key="SearchIconStyle" TargetType="{x:Type Path}">
<Setter Property="Fill" Value="#E9819F" />
<Setter Property="Fill" Value="#cc1081" />
<Setter Property="Width" Value="28" />
<Setter Property="Height" Value="28" />
</Style>
<Style x:Key="ItemHotkeyStyle" TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="#E9819F" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="#ed92c9" />
</Style>
<Style x:Key="ItemHotkeySelectedStyle" TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="#E9819F" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="#ed92c9" />
</Style>
<Style
x:Key="ClockBox"
BasedOn="{StaticResource BaseClockBox}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#E9819F" />
<Setter Property="Foreground" Value="#71114b" />
</Style>
<Style
x:Key="DateBox"
BasedOn="{StaticResource BaseDateBox}"
TargetType="{x:Type TextBlock}">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="#E9819F" />
<Setter Property="Foreground" Value="#71114b" />
</Style>
<Style
x:Key="PreviewBorderStyle"
BasedOn="{StaticResource BasePreviewBorderStyle}"
TargetType="{x:Type Border}">
<Setter Property="Margin" Value="0 0 10 0" />
<Setter Property="Width" Value="5" />
<Setter Property="BorderThickness" Value="3 0 0 0" />
<Setter Property="BorderBrush" Value="#0e172c" />
<Setter Property="BorderBrush" Value="#000000" />
</Style>
<Style
x:Key="PreviewItemTitleStyle"
BasedOn="{StaticResource BasePreviewItemTitleStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#0E172C" />
<Setter Property="Foreground" Value="#f5f5f5" />
</Style>
<Style
x:Key="PreviewItemSubTitleStyle"
BasedOn="{StaticResource BasePreviewItemSubTitleStyle}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#0E172C" />
<Setter Property="Foreground" Value="#c2c2c2" />
</Style>
<Style
x:Key="PreviewGlyph"
BasedOn="{StaticResource BasePreviewGlyph}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#0E172C" />
<Setter Property="Foreground" Value="#ffffff" />
</Style>
</ResourceDictionary>

View file

@ -32,7 +32,7 @@
x:Key="QueryBoxStyle"
BasedOn="{StaticResource BaseQueryBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Padding" Value="0,4,50,0" />
<Setter Property="Padding" Value="0 4 50 0" />
<Setter Property="CaretBrush" Value="#fa7941" />
<Setter Property="Foreground" Value="#ffffff" />
<Setter Property="FontSize" Value="18" />
@ -43,7 +43,7 @@
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Padding" Value="0,4,50,0" />
<Setter Property="Padding" Value="0 4 50 0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="38" />
<Setter Property="FontSize" Value="18" />
@ -56,15 +56,7 @@
TargetType="{x:Type Border}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="BorderBrush" Value="#e2e2e2" />
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Offset="0.0" Color="#383838" />
<GradientStop Offset="0.9" Color="#2e2e2e" />
<GradientStop Offset="1.0" Color="#2e2e2e" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Background" Value="#2e2e2e" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="UseLayoutRounding" Value="True" />
</Style>
@ -96,7 +88,7 @@
TargetType="{x:Type Rectangle}">
<Setter Property="Fill" Value="#3b3b3b" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="0,0,0,8" />
<Setter Property="Margin" Value="0 0 0 8" />
</Style>
<Style x:Key="HighlightStyle" />
<Style
@ -147,7 +139,7 @@
<Setter Property="Background" Value="Transparent" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Margin" Value="0,8,8,0" />
<Setter Property="Margin" Value="0 8 8 0" />
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
@ -174,7 +166,7 @@
x:Key="ClockPanel"
BasedOn="{StaticResource ClockPanel}"
TargetType="{x:Type StackPanel}">
<Setter Property="Margin" Value="0,0,54,2" />
<Setter Property="Margin" Value="0 0 54 2" />
</Style>
<Style
x:Key="ClockBox"

View file

@ -11,6 +11,10 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Themes/Base.xaml" />
</ResourceDictionary.MergedDictionaries>
<system:Boolean x:Key="ThemeBlurEnabled">False</system:Boolean>
<system:String x:Key="SystemBG">Auto</system:String>
<Color x:Key="LightBG">#FFFAFAFA</Color>
<Color x:Key="DarkBG">#FF202020</Color>
<Thickness x:Key="ResultMargin">0 0 0 8</Thickness>
<Style
x:Key="ItemGlyph"
@ -175,13 +179,13 @@
x:Key="ClockBox"
BasedOn="{StaticResource BaseClockBox}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{DynamicResource ClockDateForeground}" />
<Setter Property="Foreground" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
</Style>
<Style
x:Key="DateBox"
BasedOn="{StaticResource BaseDateBox}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{DynamicResource ClockDateForeground}" />
<Setter Property="Foreground" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
</Style>
<Style
x:Key="PreviewBorderStyle"

View file

@ -1,7 +1,7 @@
<!--
Name: Windows 11
IsDark: True
HasBlur: False
HasBlur: True
-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
@ -12,6 +12,10 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Themes/Base.xaml" />
</ResourceDictionary.MergedDictionaries>
<system:Boolean x:Key="ThemeBlurEnabled">True</system:Boolean>
<system:String x:Key="SystemBG">Auto</system:String>
<Color x:Key="LightBG">#BFFAFAFA</Color>
<Color x:Key="DarkBG">#DD202020</Color>
<Style
x:Key="BulletStyle"
BasedOn="{StaticResource BaseBulletStyle}"

File diff suppressed because it is too large Load diff

View file

@ -43,7 +43,6 @@ namespace Flow.Launcher.ViewModel
}
}
private async void LoadIconAsync()
{
Image = await ImageLoader.LoadAsync(PluginPair.Metadata.IcoPath);
@ -100,7 +99,7 @@ namespace Flow.Launcher.ViewModel
: null;
private ImageSource _image = ImageLoader.MissingImage;
public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed;
public Visibility ActionKeywordsVisibility => PluginPair.Metadata.HideActionKeywordPanel ? Visibility.Collapsed : Visibility.Visible;
public string InitilizaTime => PluginPair.Metadata.InitTime + "ms";
public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms";
public string Version => InternationalizationManager.Instance.GetTranslation("plugin_query_version") + " " + PluginPair.Metadata.Version;
@ -109,9 +108,8 @@ namespace Flow.Launcher.ViewModel
public int Priority => PluginPair.Metadata.Priority;
public Infrastructure.UserSettings.Plugin PluginSettingsObject { get; set; }
public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword)
public void OnActionKeywordsChanged()
{
PluginManager.ReplaceActionKeyword(PluginPair.Metadata.ID, oldActionKeyword, newActionKeyword);
OnPropertyChanged(nameof(ActionKeywordsText));
}
@ -150,8 +148,6 @@ namespace Flow.Launcher.ViewModel
App.API.ShowMainWindow();
}
public static bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
[RelayCommand]
private void SetActionKeywords()
{

View file

@ -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();
}
}

View file

@ -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();

View file

@ -8,77 +8,87 @@
d:DesignWidth="500"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
<Grid Margin="60,0,10,0">
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Margin="0,10,0,10" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10" Text="{DynamicResource flowlauncher_plugin_browserbookmark_loadBrowserFrom}" />
<CheckBox
Margin="0,0,15,0"
Content="Chrome"
IsChecked="{Binding LoadChromeBookmark}" />
<CheckBox
Margin="0,0,15,0"
Content="Edge"
IsChecked="{Binding LoadEdgeBookmark}" />
<CheckBox
Margin="0,0,15,0"
Content="Firefox"
IsChecked="{Binding LoadFirefoxBookmark}" />
<StackPanel
Grid.Row="0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
Orientation="Horizontal">
<TextBlock
Margin="{StaticResource SettingPanelItemRightMargin}"
VerticalAlignment="Center"
Text="{DynamicResource flowlauncher_plugin_browserbookmark_loadBrowserFrom}" />
<CheckBox
Margin="{StaticResource SettingPanelItemRightMargin}"
Content="Chrome"
IsChecked="{Binding LoadChromeBookmark}" />
<CheckBox
Margin="{StaticResource SettingPanelItemRightMargin}"
Content="Edge"
IsChecked="{Binding LoadEdgeBookmark}" />
<CheckBox
Margin="{StaticResource SettingPanelItemRightMargin}"
Content="Firefox"
IsChecked="{Binding LoadFirefoxBookmark}" />
<Button
Margin="{StaticResource SettingPanelItemRightMargin}"
Click="Others_Click"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_others}" />
</StackPanel>
<StackPanel
Name="CustomBrowsersList"
Grid.Row="1"
Visibility="Collapsed">
<ListView
Name="CustomBrowsers"
Height="auto"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
BorderBrush="DarkGray"
BorderThickness="1"
ItemsSource="{Binding Settings.CustomChromiumBrowsers}"
MouseDoubleClick="MouseDoubleClickOnSelectedCustomBrowser"
SelectedItem="{Binding SelectedCustomBrowser}"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserName}" />
<GridViewColumn DisplayMemberBinding="{Binding DataDirectoryPath, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserBookmarkDataDirectory}" />
<GridViewColumn DisplayMemberBinding="{Binding BrowserType, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserEngine}" />
</GridView>
</ListView.View>
</ListView>
<StackPanel
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,0,15,0"
Click="Others_Click"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_others}" />
</StackPanel>
<StackPanel Name="CustomBrowsersList" Visibility="Collapsed">
<ListView
Name="CustomBrowsers"
Height="auto"
Margin="10"
BorderBrush="DarkGray"
BorderThickness="1"
ItemsSource="{Binding Settings.CustomChromiumBrowsers}"
MouseDoubleClick="MouseDoubleClickOnSelectedCustomBrowser"
SelectedItem="{Binding SelectedCustomBrowser}"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserName}" />
<GridViewColumn DisplayMemberBinding="{Binding DataDirectoryPath, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserBookmarkDataDirectory}" />
<GridViewColumn DisplayMemberBinding="{Binding BrowserType, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserEngine}" />
</GridView>
</ListView.View>
</ListView>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
MinWidth="130"
Margin="10"
Click="NewCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_addBrowserBookmark}" />
<Button
MinWidth="130"
Margin="10"
Click="EditCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_editBrowserBookmark}">
<Button.Style>
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="Button">
<Setter Property="IsEnabled" Value="true" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=CustomBrowsers, Path=SelectedItems.Count}" Value="0">
<Setter Property="IsEnabled" Value="false" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button
MinWidth="120"
Margin="10"
Click="DeleteCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_removeBrowserBookmark}" />
</StackPanel>
Width="100"
Click="NewCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_addBrowserBookmark}" />
<Button
Width="100"
Margin="{StaticResource SettingPanelItemLeftMargin}"
Click="EditCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_editBrowserBookmark}">
<Button.Style>
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="Button">
<Setter Property="IsEnabled" Value="true" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=CustomBrowsers, Path=SelectedItems.Count}" Value="0">
<Setter Property="IsEnabled" Value="false" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button
Width="100"
Margin="{StaticResource SettingPanelItemLeftMargin}"
Click="DeleteCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_removeBrowserBookmark}" />
</StackPanel>
</StackPanel>
</Grid>

View file

@ -43,6 +43,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<ItemGroup>

View file

@ -155,16 +155,16 @@ namespace Flow.Launcher.Plugin.Calculator
return value.ToString(numberFormatInfo);
}
private string GetDecimalSeparator()
private static string GetDecimalSeparator()
{
string systemDecimalSeperator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
switch (_settings.DecimalSeparator)
string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
return _settings.DecimalSeparator switch
{
case DecimalSeparator.UseSystemLocale: return systemDecimalSeperator;
case DecimalSeparator.Dot: return dot;
case DecimalSeparator.Comma: return comma;
default: return systemDecimalSeperator;
}
DecimalSeparator.UseSystemLocale => systemDecimalSeparator,
DecimalSeparator.Dot => dot,
DecimalSeparator.Comma => comma,
_ => systemDecimalSeparator,
};
}
private bool IsBracketComplete(string query)

View file

@ -17,7 +17,7 @@
<core:LocalizationConverter x:Key="LocalizationConverter" />
</UserControl.Resources>
<Grid Margin="70,14,0,14">
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
@ -30,7 +30,7 @@
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,10,0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_calculator_output_decimal_seperator}" />
@ -39,8 +39,9 @@
Grid.Row="0"
Grid.Column="1"
MaxWidth="300"
Margin="10,5,0,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ItemsSource="{Binding Source={ui:EnumBindingSource {x:Type calculator:DecimalSeparator}}}"
SelectedItem="{Binding Settings.DecimalSeparator}">
<ComboBox.ItemTemplate>
@ -53,7 +54,7 @@
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,0,10,0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_calculator_max_decimal_places}" />
@ -61,8 +62,9 @@
x:Name="MaxDecimalPlaces"
Grid.Row="1"
Grid.Column="1"
Margin="10,5,0,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ItemsSource="{Binding MaxDecimalPlacesRange}"
SelectedItem="{Binding Settings.MaxDecimalPlaces}" />

View file

@ -7,6 +7,7 @@
"*",
"*"
],
"HideActionKeywordPanel": true,
"Name": "Explorer",
"Description": "Find and manage files and folders via Windows Search or Everything",
"Author": "Jeremy Wu",

View file

@ -19,7 +19,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>

View file

@ -7,7 +7,7 @@
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid Margin="70 15 18 15">
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.ColumnDefinitions />
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
@ -15,13 +15,12 @@
</Grid.RowDefinitions>
<CheckBox
Grid.Row="0"
Padding="8 0 0 0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource plugin_pluginsmanager_plugin_settings_unknown_source}"
IsChecked="{Binding WarnFromUnknownSource}" />
<CheckBox
Grid.Row="1"
Margin="0 10 0 0"
Padding="8 0 0 0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource plugin_pluginsmanager_plugin_settings_auto_restart}"
IsChecked="{Binding AutoRestartAfterChanging}" />
</Grid>

View file

@ -1,5 +1,4 @@

using Flow.Launcher.Plugin.PluginsManager.ViewModels;
using Flow.Launcher.Plugin.PluginsManager.ViewModels;
namespace Flow.Launcher.Plugin.PluginsManager.Views
{
@ -8,15 +7,11 @@ namespace Flow.Launcher.Plugin.PluginsManager.Views
/// </summary>
public partial class PluginsManagerSettings
{
private readonly SettingsViewModel viewModel;
internal PluginsManagerSettings(SettingsViewModel viewModel)
{
InitializeComponent();
this.viewModel = viewModel;
this.DataContext = viewModel;
DataContext = viewModel;
}
}
}

View file

@ -13,6 +13,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<UseWPF>true</UseWPF>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -58,7 +59,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>

View file

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="flowlauncher_plugin_processkiller_plugin_name">Process Killer</system:String>
<system:String x:Key="flowlauncher_plugin_processkiller_plugin_description">Kill running processes from Flow Launcher</system:String>
@ -9,4 +10,6 @@
<system:String x:Key="flowlauncher_plugin_processkiller_kill_all_count">kill {0} processes</system:String>
<system:String x:Key="flowlauncher_plugin_processkiller_kill_instances">kill all instances</system:String>
<system:String x:Key="flowlauncher_plugin_processkiller_put_visible_window_process_top">Put processes with visible windows on the top</system:String>
</ResourceDictionary>

View file

@ -1,18 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Flow.Launcher.Infrastructure;
using System.Windows.Controls;
using Flow.Launcher.Plugin.ProcessKiller.ViewModels;
using Flow.Launcher.Plugin.ProcessKiller.Views;
namespace Flow.Launcher.Plugin.ProcessKiller
{
public class Main : IPlugin, IPluginI18n, IContextMenu
public class Main : IPlugin, IPluginI18n, IContextMenu, ISettingProvider
{
private ProcessHelper processHelper = new ProcessHelper();
private readonly ProcessHelper processHelper = new();
private static PluginInitContext _context;
internal Settings Settings;
private SettingsViewModel _viewModel;
public void Init(PluginInitContext context)
{
_context = context;
Settings = context.API.LoadSettingJsonStorage<Settings>();
_viewModel = new SettingsViewModel(Settings);
}
public List<Result> Query(Query query)
@ -48,7 +57,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
{
foreach (var p in similarProcesses)
{
processHelper.TryKill(p);
processHelper.TryKill(_context, p);
}
return true;
@ -62,16 +71,72 @@ namespace Flow.Launcher.Plugin.ProcessKiller
private List<Result> CreateResultsFromQuery(Query query)
{
string termToSearch = query.Search;
var processlist = processHelper.GetMatchingProcesses(termToSearch);
if (!processlist.Any())
// Get all non-system processes
var allPocessList = processHelper.GetMatchingProcesses();
if (!allPocessList.Any())
{
return null;
}
var results = new List<Result>();
// Filter processes based on search term
var searchTerm = query.Search;
var processlist = new List<ProcessResult>();
var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle();
if (string.IsNullOrWhiteSpace(searchTerm))
{
foreach (var p in allPocessList)
{
var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p);
if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
{
// Add score to prioritize processes with visible windows
// And use window title for those processes
processlist.Add(new ProcessResult(p, Settings.PutVisibleWindowProcessesTop ? 200 : 0, windowTitle, null, progressNameIdTitle));
}
else
{
processlist.Add(new ProcessResult(p, 0, progressNameIdTitle, null, progressNameIdTitle));
}
}
}
else
{
foreach (var p in allPocessList)
{
var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p);
if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
{
// Get max score from searching process name, window title and process id
var windowTitleMatch = _context.API.FuzzySearch(searchTerm, windowTitle);
var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle);
var score = Math.Max(windowTitleMatch.Score, processNameIdMatch.Score);
if (score > 0)
{
// Add score to prioritize processes with visible windows
// And use window title for those processes
if (Settings.PutVisibleWindowProcessesTop)
{
score += 200;
}
processlist.Add(new ProcessResult(p, score, windowTitle,
score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle));
}
}
else
{
var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle);
var score = processNameIdMatch.Score;
if (score > 0)
{
processlist.Add(new ProcessResult(p, score, progressNameIdTitle, processNameIdMatch, progressNameIdTitle));
}
}
}
}
var results = new List<Result>();
foreach (var pr in processlist)
{
var p = pr.Process;
@ -79,28 +144,30 @@ namespace Flow.Launcher.Plugin.ProcessKiller
results.Add(new Result()
{
IcoPath = path,
Title = p.ProcessName + " - " + p.Id,
Title = pr.Title,
TitleToolTip = pr.Tooltip,
SubTitle = path,
TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData,
TitleHighlightData = pr.TitleMatch?.MatchData,
Score = pr.Score,
ContextData = p.ProcessName,
AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}",
Action = (c) =>
{
processHelper.TryKill(p);
processHelper.TryKill(_context, p);
// Re-query to refresh process list
_context.API.ChangeQuery(query.RawQuery, true);
_context.API.ReQuery();
return true;
}
});
}
// Order results by process name for processes without visible windows
var sortedResults = results.OrderBy(x => x.Title).ToList();
// When there are multiple results AND all of them are instances of the same executable
// add a quick option to kill them all at the top of the results.
var firstResult = sortedResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.SubTitle));
if (processlist.Count > 1 && !string.IsNullOrEmpty(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle))
if (processlist.Count > 1 && !string.IsNullOrEmpty(searchTerm) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle))
{
sortedResults.Insert(1, new Result()
{
@ -112,10 +179,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller
{
foreach (var p in processlist)
{
processHelper.TryKill(p.Process);
processHelper.TryKill(_context, p.Process);
}
// Re-query to refresh process list
_context.API.ChangeQuery(query.RawQuery, true);
_context.API.ReQuery();
return true;
}
});
@ -123,5 +190,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller
return sortedResults;
}
public Control CreateSettingPanel()
{
return new SettingsControl(_viewModel);
}
}
}

View file

@ -1,2 +1,7 @@
QueryFullProcessImageName
OpenProcess
OpenProcess
EnumWindows
GetWindowTextLength
GetWindowText
IsWindowVisible
GetWindowThreadProcessId

View file

@ -1,10 +1,9 @@
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.Win32.SafeHandles;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Threading;
@ -13,7 +12,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
{
internal class ProcessHelper
{
private readonly HashSet<string> _systemProcessList = new HashSet<string>()
private readonly HashSet<string> _systemProcessList = new()
{
"conhost",
"svchost",
@ -31,46 +30,96 @@ namespace Flow.Launcher.Plugin.ProcessKiller
"explorer"
};
private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower());
private const string FlowLauncherProcessName = "Flow.Launcher";
private bool IsSystemProcessOrFlowLauncher(Process p) =>
_systemProcessList.Contains(p.ProcessName.ToLower()) ||
string.Equals(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm
/// Get title based on process name and id
/// </summary>
public List<ProcessResult> GetMatchingProcesses(string searchTerm)
public static string GetProcessNameIdTitle(Process p)
{
var processlist = new List<ProcessResult>();
var sb = new StringBuilder();
sb.Append(p.ProcessName);
sb.Append(" - ");
sb.Append(p.Id);
return sb.ToString();
}
/// <summary>
/// Returns a Process for evey running non-system process
/// </summary>
public List<Process> GetMatchingProcesses()
{
var processlist = new List<Process>();
foreach (var p in Process.GetProcesses())
{
if (IsSystemProcess(p)) continue;
if (IsSystemProcessOrFlowLauncher(p)) continue;
if (string.IsNullOrWhiteSpace(searchTerm))
{
// show all non-system processes
processlist.Add(new ProcessResult(p, 0));
}
else
{
var score = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + p.Id).Score;
if (score > 0)
{
processlist.Add(new ProcessResult(p, score));
}
}
processlist.Add(p);
}
return processlist;
}
/// <summary>
/// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title.
/// </summary>
public static unsafe Dictionary<int, string> GetProcessesWithNonEmptyWindowTitle()
{
var processDict = new Dictionary<int, string>();
PInvoke.EnumWindows((hWnd, _) =>
{
var windowTitle = GetWindowTitle(hWnd);
if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd))
{
uint processId = 0;
var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId);
if (result == 0u || processId == 0u)
{
return false;
}
var process = Process.GetProcessById((int)processId);
if (!processDict.ContainsKey((int)processId))
{
processDict.Add((int)processId, windowTitle);
}
}
return true;
}, IntPtr.Zero);
return processDict;
}
private static unsafe string GetWindowTitle(HWND hwnd)
{
var capacity = PInvoke.GetWindowTextLength(hwnd) + 1;
int length;
Span<char> buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity];
fixed (char* pBuffer = buffer)
{
// If the window has no title bar or text, if the title bar is empty,
// or if the window or control handle is invalid, the return value is zero.
length = PInvoke.GetWindowText(hwnd, pBuffer, capacity);
}
return buffer[..length].ToString();
}
/// <summary>
/// Returns all non-system processes whose file path matches the given processPath
/// </summary>
public IEnumerable<Process> GetSimilarProcesses(string processPath)
{
return Process.GetProcesses().Where(p => !IsSystemProcess(p) && TryGetProcessFilename(p) == processPath);
return Process.GetProcesses().Where(p => !IsSystemProcessOrFlowLauncher(p) && TryGetProcessFilename(p) == processPath);
}
public void TryKill(Process p)
public void TryKill(PluginInitContext context, Process p)
{
try
{
@ -82,7 +131,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
}
catch (Exception e)
{
Log.Exception($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e);
context.API.LogException($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e);
}
}

View file

@ -1,17 +1,27 @@
using System.Diagnostics;
using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Plugin.ProcessKiller
{
internal class ProcessResult
{
public ProcessResult(Process process, int score)
public ProcessResult(Process process, int score, string title, MatchResult match, string tooltip)
{
Process = process;
Score = score;
Title = title;
TitleMatch = match;
Tooltip = tooltip;
}
public Process Process { get; }
public int Score { get; }
public string Title { get; }
public MatchResult TitleMatch { get; }
public string Tooltip { get; }
}
}
}

View file

@ -0,0 +1,7 @@
namespace Flow.Launcher.Plugin.ProcessKiller
{
public class Settings
{
public bool PutVisibleWindowProcessesTop { get; set; } = false;
}
}

View file

@ -0,0 +1,18 @@
namespace Flow.Launcher.Plugin.ProcessKiller.ViewModels
{
public class SettingsViewModel
{
public Settings Settings { get; set; }
public SettingsViewModel(Settings settings)
{
Settings = settings;
}
public bool PutVisibleWindowProcessesTop
{
get => Settings.PutVisibleWindowProcessesTop;
set => Settings.PutVisibleWindowProcessesTop = value;
}
}
}

View file

@ -0,0 +1,22 @@
<UserControl
x:Class="Flow.Launcher.Plugin.ProcessKiller.Views.SettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="500"
mc:Ignorable="d">
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.ColumnDefinitions />
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<CheckBox
Grid.Row="0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource flowlauncher_plugin_processkiller_put_visible_window_process_top}"
IsChecked="{Binding PutVisibleWindowProcessesTop}" />
</Grid>
</UserControl>

View file

@ -0,0 +1,17 @@
using System.Windows.Controls;
using Flow.Launcher.Plugin.ProcessKiller.ViewModels;
namespace Flow.Launcher.Plugin.ProcessKiller.Views;
public partial class SettingsControl : UserControl
{
/// <summary>
/// Interaction logic for SettingsControl.xaml
/// </summary>
public SettingsControl(SettingsViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}

View file

@ -10,124 +10,109 @@
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</UserControl.Resources>
<Grid Margin="0">
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DockPanel
Margin="70 10 0 8"
HorizontalAlignment="Stretch"
LastChildFill="True">
<DockPanel HorizontalAlignment="Stretch" LastChildFill="True">
<TextBlock
MinWidth="120"
Margin="0 5 10 0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
Text="{DynamicResource flowlauncher_plugin_program_index_source}" />
<WrapPanel
Width="Auto"
Margin="0 0 14 0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Right"
DockPanel.Dock="Right">
<CheckBox
Name="UWPEnabled"
Margin="12 0 12 0"
Margin="{StaticResource SettingPanelItemRightMargin}"
Content="{DynamicResource flowlauncher_plugin_program_index_uwp}"
IsChecked="{Binding EnableUWP}"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_uwp_tooltip}"
Visibility="{Binding ShowUWPCheckbox, Converter={StaticResource BooleanToVisibilityConverter}}" />
<CheckBox
Name="StartMenuEnabled"
Margin="12 0 12 0"
Margin="{StaticResource SettingPanelItemRightMargin}"
Content="{DynamicResource flowlauncher_plugin_program_index_start}"
IsChecked="{Binding EnableStartMenuSource}"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_start_tooltip}" />
<CheckBox
Name="RegistryEnabled"
Margin="12 0 12 0"
Margin="{StaticResource SettingPanelItemRightMargin}"
Content="{DynamicResource flowlauncher_plugin_program_index_registry}"
IsChecked="{Binding EnableRegistrySource}"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_registry_tooltip}" />
<CheckBox
Name="PATHEnabled"
Margin="12 0 12 0"
Content="{DynamicResource flowlauncher_plugin_program_index_PATH}"
IsChecked="{Binding EnablePATHSource}"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_PATH_tooltip}" />
</WrapPanel>
</DockPanel>
<StackPanel
Grid.Row="1"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<Separator
Height="1"
BorderBrush="{DynamicResource Color03B}"
BorderThickness="1" />
<DockPanel
Margin="70 10 0 8"
HorizontalAlignment="Stretch"
LastChildFill="True">
<Separator Style="{StaticResource SettingPanelSeparatorStyle}" />
<DockPanel HorizontalAlignment="Stretch" LastChildFill="True">
<TextBlock
MinWidth="120"
Margin="0 5 10 0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
Text="{DynamicResource flowlauncher_plugin_program_index_option}" />
<WrapPanel
Width="Auto"
Margin="0 0 14 0"
HorizontalAlignment="Right"
DockPanel.Dock="Right">
<CheckBox
Margin="12 0 12 0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource flowlauncher_plugin_program_enable_hidelnkpath}"
IsChecked="{Binding HideAppsPath}"
ToolTip="{DynamicResource flowlauncher_plugin_program_enable_hidelnkpath_tooltip}" />
<CheckBox
Margin="12 0 12 0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource flowlauncher_plugin_program_enable_hideuninstallers}"
IsChecked="{Binding HideUninstallers}"
ToolTip="{DynamicResource flowlauncher_plugin_program_enable_hideuninstallers_tooltip}" />
<CheckBox
Margin="12 0 12 0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource flowlauncher_plugin_program_enable_description}"
IsChecked="{Binding EnableDescription}"
ToolTip="{DynamicResource flowlauncher_plugin_program_enable_description_tooltip}" />
<CheckBox
Margin="12 0 12 0"
Content="{DynamicResource flowlauncher_plugin_program_enable_hideduplicatedwindowsapp}"
IsChecked="{Binding HideDuplicatedWindowsApp}"
ToolTip="{DynamicResource flowlauncher_plugin_program_enable_hideduplicatedwindowsapp_tooltip}" />
</WrapPanel>
</DockPanel>
<Separator
Height="1"
BorderBrush="{DynamicResource Color03B}"
BorderThickness="1" />
<Separator Style="{StaticResource SettingPanelSeparatorStyle}" />
<StackPanel
Margin="60 0 0 2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="btnLoadAllProgramSource"
MinWidth="120"
Margin="10 10 5 10"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Right"
Click="btnLoadAllProgramSource_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_all_programs}" />
<Button
x:Name="btnProgramSuffixes"
MinWidth="120"
Margin="5 10 5 10"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Right"
Click="BtnProgramSuffixes_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_suffixes}" />
<Button
x:Name="btnReindex"
MinWidth="120"
Margin="5 10 5 10"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Right"
Click="btnReindex_Click"
Content="{DynamicResource flowlauncher_plugin_program_reindex}" />
@ -140,12 +125,13 @@
x:Name="progressBarIndexing"
Width="80"
Height="20"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
IsIndeterminate="True"
Maximum="100"
Minimum="0" />
<TextBlock
Height="20"
Margin="10 0 0 0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Center"
Text="{DynamicResource flowlauncher_plugin_program_indexing}" />
</StackPanel>
@ -154,7 +140,7 @@
<ListView
x:Name="programSourceView"
Grid.Row="2"
Margin="70 0 20 0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
AllowDrop="True"
BorderBrush="DarkGray"
BorderThickness="1"
@ -203,27 +189,24 @@
</GridView>
</ListView.View>
</ListView>
<DockPanel
Grid.Row="3"
Grid.RowSpan="1"
Margin="0 0 20 0">
<DockPanel Grid.Row="3">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
x:Name="btnProgramSourceStatus"
MinWidth="100"
Margin="10"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
Click="btnProgramSourceStatus_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_disable}" />
<Button
x:Name="btnEditProgramSource"
MinWidth="100"
Margin="10"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}"
Click="btnEditProgramSource_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_edit}" />
<Button
x:Name="btnAddProgramSource"
MinWidth="100"
Margin="10 10 0 10"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}"
Click="btnAddProgramSource_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_add}" />
</StackPanel>

View file

@ -8,7 +8,7 @@
d:DesignWidth="300"
Loaded="CMDSetting_OnLoaded"
mc:Ignorable="d">
<Grid Margin="60,10,10,20" VerticalAlignment="Top">
<Grid Margin="{StaticResource SettingPanelMargin}" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
@ -21,37 +21,37 @@
<CheckBox
x:Name="ReplaceWinR"
Grid.Row="0"
Margin="10,10,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
Content="{DynamicResource flowlauncher_plugin_cmd_relace_winr}" />
<CheckBox
x:Name="CloseShellAfterPress"
Grid.Row="1"
Margin="10,5,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
Content="{DynamicResource flowlauncher_plugin_cmd_close_cmd_after_press}" />
<CheckBox
x:Name="LeaveShellOpen"
Grid.Row="2"
Margin="10,5,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
Content="{DynamicResource flowlauncher_plugin_cmd_leave_cmd_open}" />
<CheckBox
x:Name="AlwaysRunAsAdministrator"
Grid.Row="3"
Margin="10,5,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
Content="{DynamicResource flowlauncher_plugin_cmd_always_run_as_administrator}" />
<CheckBox
x:Name="UseWindowsTerminal"
Grid.Row="4"
Margin="10,5,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
Content="{DynamicResource flowlauncher_plugin_cmd_use_windows_terminal}" />
<ComboBox
x:Name="ShellComboBox"
Grid.Row="5"
Margin="10,5,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left">
<ComboBoxItem>CMD</ComboBoxItem>
<ComboBoxItem>PowerShell</ComboBoxItem>
@ -61,11 +61,11 @@
<StackPanel Grid.Row="6" Orientation="Horizontal">
<CheckBox
x:Name="ShowOnlyMostUsedCMDs"
Margin="10,5,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Content="{DynamicResource flowlauncher_plugin_cmd_history}" />
<ComboBox
x:Name="ShowOnlyMostUsedCMDsNumber"
Margin="10,5,5,5"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left" />
</StackPanel>
</Grid>

View file

@ -302,7 +302,7 @@ namespace Flow.Launcher.Plugin.Sys
new Result
{
Title = "Hibernate",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe945"),
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe8be"),
IcoPath = "Images\\hibernate.png",
Action= c =>
{
@ -325,7 +325,7 @@ namespace Flow.Launcher.Plugin.Sys
{
Title = "Empty Recycle Bin",
IcoPath = "Images\\recyclebin.png",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe74d"),
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xea99"),
Action = c =>
{
// http://www.pinvoke.net/default.aspx/shell32/SHEmptyRecycleBin.html
@ -361,6 +361,7 @@ namespace Flow.Launcher.Plugin.Sys
{
Title = "Exit",
IcoPath = "Images\\app.png",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe89f"),
Action = c =>
{
Application.Current.MainWindow.Close();
@ -370,6 +371,7 @@ namespace Flow.Launcher.Plugin.Sys
new Result
{
Title = "Save Settings",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xea35"),
IcoPath = "Images\\app.png",
Action = c =>
{
@ -381,6 +383,7 @@ namespace Flow.Launcher.Plugin.Sys
},
new Result
{
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe72c"),
Title = "Restart Flow Launcher",
IcoPath = "Images\\app.png",
Action = c =>
@ -392,6 +395,7 @@ namespace Flow.Launcher.Plugin.Sys
new Result
{
Title = "Settings",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf210"),
IcoPath = "Images\\app.png",
Action = c =>
{
@ -403,6 +407,7 @@ namespace Flow.Launcher.Plugin.Sys
{
Title = "Reload Plugin Data",
IcoPath = "Images\\app.png",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe72c"),
Action = c =>
{
// Hide the window first then show msg after done because sometimes the reload could take a while, so not to make user think it's frozen.
@ -421,6 +426,7 @@ namespace Flow.Launcher.Plugin.Sys
new Result
{
Title = "Check For Update",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xede4"),
IcoPath = "Images\\checkupdate.png",
Action = c =>
{
@ -431,6 +437,7 @@ namespace Flow.Launcher.Plugin.Sys
},
new Result
{
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"),
Title = "Open Log Location",
IcoPath = "Images\\app.png",
CopyText = logPath,
@ -444,6 +451,7 @@ namespace Flow.Launcher.Plugin.Sys
new Result
{
Title = "Flow Launcher Tips",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe897"),
IcoPath = "Images\\app.png",
CopyText = Constant.Documentation,
AutoCompleteText = Constant.Documentation,
@ -456,6 +464,7 @@ namespace Flow.Launcher.Plugin.Sys
new Result
{
Title = "Flow Launcher UserData Folder",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"),
IcoPath = "Images\\app.png",
CopyText = userDataPath,
AutoCompleteText = userDataPath,
@ -480,7 +489,7 @@ namespace Flow.Launcher.Plugin.Sys
{
Title = "Set Flow Launcher Theme",
IcoPath = "Images\\app.png",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue7fc"),
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue790"),
Action = c =>
{
_context.API.ChangeQuery($"{ThemeSelector.Keyword} ");

View file

@ -9,7 +9,8 @@
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid Margin="70 18 18 18">
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
@ -18,7 +19,7 @@
<ListView
x:Name="lbxCommands"
Grid.Row="0"
Margin="0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
BorderBrush="DarkGray"
BorderThickness="1"
ItemsSource="{Binding Settings.Commands}"
@ -57,11 +58,12 @@
<StackPanel
Grid.Row="1"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Width="100"
Margin="10"
Margin="{StaticResource SettingPanelItemLeftMargin}"
Click="OnEditCommandKeywordClick"
Content="{DynamicResource flowlauncher_plugin_sys_edit}" />
</StackPanel>

View file

@ -2,7 +2,6 @@
using System.Linq;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Resource;
using FLSettings = Flow.Launcher.Infrastructure.UserSettings.Settings;
namespace Flow.Launcher.Plugin.Sys
{
@ -10,7 +9,6 @@ namespace Flow.Launcher.Plugin.Sys
{
public const string Keyword = "fltheme";
private readonly FLSettings _settings;
private readonly Theme _theme;
private readonly PluginInitContext _context;
@ -19,19 +17,15 @@ namespace Flow.Launcher.Plugin.Sys
// Theme select codes simplified from SettingsPaneThemeViewModel.cs
private Theme.ThemeData _selectedTheme;
private Theme.ThemeData SelectedTheme
public Theme.ThemeData SelectedTheme
{
get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.CurrentTheme);
get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.GetCurrentTheme());
set
{
_selectedTheme = value;
_theme.ChangeTheme(value.FileNameWithoutExtension);
if (_theme.BlurEnabled && _settings.UseDropShadowEffect)
{
_theme.RemoveDropShadowEffectFromCurrentTheme();
_settings.UseDropShadowEffect = false;
}
_ = _theme.RefreshFrameAsync();
}
}
@ -43,7 +37,6 @@ namespace Flow.Launcher.Plugin.Sys
{
_context = context;
_theme = Ioc.Default.GetRequiredService<Theme>();
_settings = Ioc.Default.GetRequiredService<FLSettings>();
}
public List<Result> Query(Query query)

View file

@ -51,7 +51,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>

View file

@ -19,7 +19,7 @@
<system:String x:Key="flowlauncher_plugin_websearch_action_keyword">Action Keyword</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_url">URL</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_search">Search</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_enable_suggestion">Use Search Query Autocomplete:</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_enable_suggestion">Use Search Query Autocomplete</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_enable_suggestion_provider">Autocomplete Data from:</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_pls_select_web_search">Please select a web search</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_delete_warning">Are you sure you want to delete {0}?</system:String>

View file

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Windows;
using Microsoft.Win32;
using Flow.Launcher.Core.Plugin;
namespace Flow.Launcher.Plugin.WebSearch
{
@ -16,7 +15,6 @@ namespace Flow.Launcher.Plugin.WebSearch
private SearchSourceViewModel _viewModel;
private string selectedNewIconImageFullPath;
public SearchSourceSettingWindow(IList<SearchSource> sources, PluginInitContext context, SearchSource old)
{
_oldSearchSource = old;
@ -80,10 +78,10 @@ namespace Flow.Launcher.Plugin.WebSearch
private void AddSearchSource()
{
var keyword = _searchSource.ActionKeyword;
if (!PluginManager.ActionKeywordRegistered(keyword))
if (!_context.API.ActionKeywordAssigned(keyword))
{
var id = _context.CurrentPluginMetadata.ID;
PluginManager.AddActionKeyword(id, keyword);
_context.API.AddActionKeyword(id, keyword);
_searchSources.Add(_searchSource);
@ -100,10 +98,11 @@ namespace Flow.Launcher.Plugin.WebSearch
{
var newKeyword = _searchSource.ActionKeyword;
var oldKeyword = _oldSearchSource.ActionKeyword;
if (!PluginManager.ActionKeywordRegistered(newKeyword) || oldKeyword == newKeyword)
if (!_context.API.ActionKeywordAssigned(newKeyword) || oldKeyword == newKeyword)
{
var id = _context.CurrentPluginMetadata.ID;
PluginManager.ReplaceActionKeyword(id, oldKeyword, newKeyword);
_context.API.RemoveActionKeyword(id, oldKeyword);
_context.API.AddActionKeyword(id, newKeyword);
var index = _searchSources.IndexOf(_oldSearchSource);
_searchSources[index] = _searchSource;

View file

@ -9,6 +9,7 @@
d:DesignHeight="300"
d:DesignWidth="500"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="BrowserPathBoxStyle" TargetType="TextBox">
<Setter Property="Height" Value="28" />
@ -35,16 +36,19 @@
</DockPanel>
</DataTemplate>
</UserControl.Resources>
<Grid Margin="0,4,0,0">
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="56" />
<RowDefinition Height="50" />
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView
x:Name="SearchSourcesListView"
Grid.Row="0"
Margin="18,18,18,0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
BorderBrush="DarkGray"
BorderThickness="1"
GridViewColumnHeader.Click="SortByColumn"
@ -60,7 +64,7 @@
<Image
Width="20"
Height="20"
Margin="6,0,0,0"
Margin="6 0 0 0"
Source="{Binding Path=IconPath}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
@ -69,11 +73,11 @@
<GridViewColumn
Width="130"
DisplayMemberBinding="{Binding ActionKeyword}"
Header="{DynamicResource flowlauncher_plugin_websearch_action_keyword}"/>
Header="{DynamicResource flowlauncher_plugin_websearch_action_keyword}" />
<GridViewColumn
Width="350"
Width="239"
DisplayMemberBinding="{Binding Title}"
Header="{DynamicResource flowlauncher_plugin_websearch_title}"/>
Header="{DynamicResource flowlauncher_plugin_websearch_title}" />
<GridViewColumn Width="140" Header="{DynamicResource flowlauncher_plugin_websearch_enable}">
<GridViewColumn.CellTemplate>
<DataTemplate>
@ -95,59 +99,57 @@
</GridView>
</ListView.View>
</ListView>
<StackPanel
Grid.Row="1"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Width="100"
Margin="10"
Margin="{StaticResource SettingPanelItemLeftMargin}"
Click="OnDeleteSearchSearchClick"
Content="{DynamicResource flowlauncher_plugin_websearch_delete}" />
<Button
Width="100"
Margin="10"
Margin="{StaticResource SettingPanelItemLeftMargin}"
Click="OnEditSearchSourceClick"
Content="{DynamicResource flowlauncher_plugin_websearch_edit}" />
<Button
Width="100"
Margin="10,10,18,10"
Margin="{StaticResource SettingPanelItemLeftMargin}"
Click="OnAddSearchSearchClick"
Content="{DynamicResource flowlauncher_plugin_websearch_add}" />
</StackPanel>
<Border
Grid.Row="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
BorderBrush="{DynamicResource Color03B}"
BorderThickness="0,1,0,0">
<DockPanel HorizontalAlignment="Right">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Label
Margin="14,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion_provider}" />
<ComboBox
Height="30"
Margin="0,0,20,0"
VerticalAlignment="Center"
FontSize="11"
IsEnabled="{Binding ElementName=EnableSuggestion, Path=IsChecked}"
ItemsSource="{Binding Settings.Suggestions}"
SelectedItem="{Binding Settings.SelectedSuggestion}" />
<Label
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion}" />
<CheckBox
Name="EnableSuggestion"
Margin="0,0,8,0"
IsChecked="{Binding Settings.EnableSuggestion}" />
</StackPanel>
<!-- Not sure why binding IsEnabled directly to Settings.EnableWebSearchSuggestion is not working -->
</DockPanel>
</Border>
<Separator Grid.Row="2" Style="{StaticResource SettingPanelSeparatorStyle}" />
<DockPanel
Grid.Row="3"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Right">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Label
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion_provider}" />
<ComboBox
Height="30"
Margin="{StaticResource SettingPanelItemLeftMargin}"
VerticalAlignment="Center"
FontSize="11"
IsEnabled="{Binding ElementName=EnableSuggestion, Path=IsChecked}"
ItemsSource="{Binding Settings.Suggestions}"
SelectedItem="{Binding Settings.SelectedSuggestion}" />
<CheckBox
Name="EnableSuggestion"
Margin="{StaticResource SettingPanelItemLeftMargin}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion}"
IsChecked="{Binding Settings.EnableSuggestion}" />
</StackPanel>
<!-- Not sure why binding IsEnabled directly to Settings.EnableWebSearchSuggestion is not working -->
</DockPanel>
</Grid>
</UserControl>

View file

@ -1,6 +1,5 @@
using System.Windows;
using System.Windows.Controls;
using Flow.Launcher.Core.Plugin;
using System.ComponentModel;
using System.Windows.Data;
@ -40,7 +39,7 @@ namespace Flow.Launcher.Plugin.WebSearch
if (result == MessageBoxResult.Yes)
{
var id = _context.CurrentPluginMetadata.ID;
PluginManager.RemoveActionKeyword(id, selected.ActionKeyword);
_context.API.RemoveActionKeyword(id, selected.ActionKeyword);
_settings.SearchSources.Remove(selected);
}
}

View file

@ -23,6 +23,7 @@
"yahoo",
"bd"
],
"HideActionKeywordPanel": true,
"Name": "Web Searches",
"Description": "Provide the web search ability",
"Author": "qianlifeng",

View file

@ -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">