diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
index 4d988b837b6..9d6174eb7ee 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
@@ -94,6 +94,11 @@ public void OpenSettingDialog()
_api.OpenSettingDialog();
}
+ public bool OpenPluginSettingsWindow(string pluginId)
+ {
+ return _api.OpenPluginSettingsWindow(pluginId);
+ }
+
public string GetTranslation(string key)
{
return _api.GetTranslation(key);
diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs
index 13da9f79f3b..3b32d067911 100644
--- a/Flow.Launcher.Infrastructure/Constant.cs
+++ b/Flow.Launcher.Infrastructure/Constant.cs
@@ -33,6 +33,7 @@ public static class Constant
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 readonly string SettingsIcon = Path.Combine(ImagesDirectory, "settings.png");
public static string PythonPath;
public static string NodePath;
diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index 0af37d9d59b..c7d3e49869f 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -162,6 +162,15 @@ public interface IPublicAPI
///
void OpenSettingDialog();
+
+ ///
+ /// Open plugin setting window for a specific plugin.
+ /// Reuses the existing window when that plugin's settings window is already open.
+ ///
+ /// ID of the plugin whose settings window should be opened
+ /// True if the plugin settings window was successfully opened or reused; false otherwise
+ bool OpenPluginSettingsWindow(string pluginId);
+
///
/// Get translation of current language
/// You need to implement IPluginI18n if you want to support multiple languages for your plugin
diff --git a/Flow.Launcher/Helper/SingletonWindowOpener.cs b/Flow.Launcher/Helper/SingletonWindowOpener.cs
index 5282b61f97e..cb9408769c3 100644
--- a/Flow.Launcher/Helper/SingletonWindowOpener.cs
+++ b/Flow.Launcher/Helper/SingletonWindowOpener.cs
@@ -11,30 +11,6 @@ public static T Open(params object[] args) where T : Window
var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.GetType() == typeof(T))
?? (T)Activator.CreateInstance(typeof(T), args);
- // Fix UI bug
- // Add `window.WindowState = WindowState.Normal`
- // If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
- // Not sure why this works tho
- // Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
- // https://stackoverflow.com/a/59719760/4230390
- // Ensure the window is not minimized before showing it
- if (window.WindowState == WindowState.Minimized)
- {
- window.WindowState = WindowState.Normal;
- }
-
- // Ensure the window is visible
- if (!window.IsVisible)
- {
- window.Show();
- }
- else
- {
- window.Activate(); // Bring the window to the foreground if already open
- }
-
- window.Focus();
-
- return (T)window;
+ return WindowVisibilityHelper.ShowOrActivate((T)window);
}
}
diff --git a/Flow.Launcher/Helper/WindowVisibilityHelper.cs b/Flow.Launcher/Helper/WindowVisibilityHelper.cs
new file mode 100644
index 00000000000..4421a0b4869
--- /dev/null
+++ b/Flow.Launcher/Helper/WindowVisibilityHelper.cs
@@ -0,0 +1,35 @@
+using System.Windows;
+
+namespace Flow.Launcher.Helper;
+
+public static class WindowVisibilityHelper
+{
+ public static T ShowOrActivate(T window) where T : Window
+ {
+ // Fix UI bug
+ // Add `window.WindowState = WindowState.Normal`
+ // If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
+ // Not sure why this works tho
+ // Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
+ // https://stackoverflow.com/a/59719760/4230390
+ // Ensure the window is not minimized before showing it
+ if (window.WindowState == WindowState.Minimized)
+ {
+ window.WindowState = WindowState.Normal;
+ }
+
+ // Ensure the window is visible
+ if (!window.IsVisible)
+ {
+ window.Show();
+ }
+ else
+ {
+ window.Activate(); // Bring the window to the foreground if already open
+ }
+
+ window.Focus();
+
+ return (T)window;
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 1b2b01ef892..02c7becacca 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -484,6 +484,10 @@
Debug
Setting Window Font
+
+ Settings: {0}
+ Failed to open plugin settings window
+
See more release notes on GitHub
Failed to fetch release notes
diff --git a/Flow.Launcher/PluginSettingsWindow.xaml b/Flow.Launcher/PluginSettingsWindow.xaml
new file mode 100644
index 00000000000..75b6c570bc0
--- /dev/null
+++ b/Flow.Launcher/PluginSettingsWindow.xaml
@@ -0,0 +1,351 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/PluginSettingsWindow.xaml.cs b/Flow.Launcher/PluginSettingsWindow.xaml.cs
new file mode 100644
index 00000000000..33a9fde8341
--- /dev/null
+++ b/Flow.Launcher/PluginSettingsWindow.xaml.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Windows;
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.ViewModel;
+using iNKORE.UI.WPF.Modern.Controls;
+
+namespace Flow.Launcher;
+
+public partial class PluginSettingsWindow
+{
+ private readonly Settings _settings;
+
+ public string PluginId { get; }
+
+ public PluginSettingsWindow(string pluginId)
+ {
+ _settings = Ioc.Default.GetRequiredService();
+
+ if (string.IsNullOrWhiteSpace(pluginId)){
+ throw new ArgumentException("Plugin ID cannot be null or whitespace.", nameof(pluginId));
+ }
+
+ PluginId = pluginId;
+
+ var pluginPair = PluginManager.GetPluginForId(pluginId);
+ if (pluginPair == null)
+ {
+ throw new InvalidOperationException($"Unable to find plugin: {pluginId}");
+ }
+
+ var pluginSettings = _settings.PluginSettings.GetPluginSettings(pluginId);
+ if (pluginSettings == null)
+ {
+ throw new InvalidOperationException($"Unable to load settings for plugin: {pluginPair.Metadata.Name}");
+ }
+
+ var pluginViewModel = new PluginViewModel
+ {
+ PluginPair = pluginPair,
+ PluginSettingsObject = pluginSettings,
+ IsExpanded = true,
+ };
+
+ DataContext = pluginViewModel;
+ Title = Localize.pluginSettingsWindowTitle(pluginPair.Metadata.Name);
+ InitializeComponent();
+ }
+
+ // This is used for Priority control to force its value to be 0 when the user clears the value.
+ private void NumberBox_OnValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
+ {
+ if (double.IsNaN(args.NewValue))
+ {
+ sender.Value = 0;
+ }
+ }
+
+ private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState.Minimized;
+ }
+
+ private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState switch
+ {
+ WindowState.Maximized => WindowState.Normal,
+ _ => WindowState.Maximized
+ };
+ }
+
+ private void OnCloseButtonClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ RefreshMaximizeRestoreButton();
+ }
+
+ private void Window_StateChanged(object sender, EventArgs e)
+ {
+ RefreshMaximizeRestoreButton();
+ }
+
+ private void RefreshMaximizeRestoreButton()
+ {
+ if (WindowState == WindowState.Maximized)
+ {
+ MaximizeButton.Visibility = Visibility.Hidden;
+ RestoreButton.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ MaximizeButton.Visibility = Visibility.Visible;
+ RestoreButton.Visibility = Visibility.Hidden;
+ }
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ if (!App.LoadingOrExiting)
+ {
+ _settings.Save();
+ App.API.SavePluginSettings();
+ }
+
+ base.OnClosed(e);
+ }
+}
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index ef3b76a289f..b939cfb5a5e 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -5,6 +5,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -147,6 +148,30 @@ public void OpenSettingDialog()
});
}
+ public bool OpenPluginSettingsWindow(string pluginId)
+ {
+ return Application.Current.Dispatcher.Invoke(() =>
+ {
+ try
+ {
+ // get existing settings window for this plugin, or else create a new one
+ var window = Application.Current.Windows
+ .OfType()
+ .FirstOrDefault(existing => existing.PluginId == pluginId)
+ ?? new PluginSettingsWindow(pluginId);
+
+ WindowVisibilityHelper.ShowOrActivate(window);
+ return true;
+ }
+ catch (Exception e)
+ {
+ LogException(ClassName, $"Failed to open plugin settings window for plugin id '{pluginId}'", e);
+ ShowMsgError(Localize.pluginSettingsWindowOpenFailed());
+ return false;
+ }
+ });
+ }
+
public void ShellRun(string cmd, string filename = "cmd.exe")
{
var args = filename == "cmd.exe" ? $"/C {cmd}" : $"{cmd}";
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index 6a4d54f27ab..057fb4610a1 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -1275,6 +1275,7 @@ private void QueryContextMenu()
{
List results = PluginManager.GetContextMenusForPlugin(selected);
results.Add(ContextMenuTopMost(selected));
+ results.Add(ContextMenuPluginSettings(selected));
results.Add(ContextMenuPluginInfo(selected));
if (!string.IsNullOrEmpty(query))
@@ -1827,6 +1828,26 @@ private Result ContextMenuTopMost(Result result)
return menu;
}
+ private static Result ContextMenuPluginSettings(Result result)
+ {
+ var id = result.PluginID;
+ var metadata = PluginManager.GetPluginForId(id).Metadata;
+
+ var menu = new Result
+ {
+ Title = Localize.pluginSettingsWindowTitle(metadata.Name),
+ IcoPath = Constant.SettingsIcon,
+ Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE713"),
+ PluginDirectory = Constant.ProgramDirectory,
+ Action = _ =>
+ {
+ return App.API.OpenPluginSettingsWindow(id);
+ },
+ OriginQuery = result.OriginQuery
+ };
+ return menu;
+ }
+
private static Result ContextMenuPluginInfo(Result result)
{
var id = result.PluginID;