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;