diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index 5232e46daee..cf75e4aa3e3 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -43,15 +43,19 @@ public class JsonRPCQueryResponseModel : JsonRPCResponseModel [JsonPropertyName("result")] public new List Result { get; set; } + public Dictionary SettingsChange { get; set; } + public string DebugMessage { get; set; } } - + public class JsonRPCRequestModel { public string Method { get; set; } public object[] Parameters { get; set; } + public Dictionary Settings { get; set; } + private static readonly JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase @@ -86,5 +90,7 @@ public class JsonRPCClientRequestModel : JsonRPCRequestModel public class JsonRPCResult : Result { public JsonRPCClientRequestModel JsonRPCAction { get; set; } + + public Dictionary SettingsChange { get; set; } } } \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/JsonRPCConfigurationModel.cs b/Flow.Launcher.Core/Plugin/JsonRPCConfigurationModel.cs new file mode 100644 index 00000000000..1f63f85a806 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCConfigurationModel.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Core.Plugin +{ + public class JsonRpcConfigurationModel + { + public List Body { get; set; } + public void Deconstruct(out List Body) + { + Body = this.Body; + } + } + + public class SettingField + { + public string Type { get; set; } + public FieldAttributes Attributes { get; set; } + public void Deconstruct(out string Type, out FieldAttributes attributes) + { + Type = this.Type; + attributes = this.Attributes; + } + } + public class FieldAttributes + { + public string Name { get; set; } + public string Label { get; set; } + public string Description { get; set; } + public bool Validation { get; set; } + public List Options { get; set; } + public string DefaultValue { get; set; } + public char passwordChar { get; set; } + public void Deconstruct(out string Name, out string Label, out string Description, out bool Validation, out List Options, out string DefaultValue) + { + Name = this.Name; + Label = this.Label; + Description = this.Description; + Validation = this.Validation; + Options = this.Options; + DefaultValue = this.DefaultValue; + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 65977219d8f..384418db974 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -1,4 +1,6 @@ -using Flow.Launcher.Core.Resource; +using Accessibility; +using Flow.Launcher.Core.Resource; +using Flow.Launcher.Infrastructure; using System; using System.Collections.Generic; using System.Diagnostics; @@ -8,12 +10,24 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using ICSharpCode.SharpZipLib.Zip; using JetBrains.Annotations; using Microsoft.IO; +using System.Text.Json.Serialization; +using System.Windows; +using System.Windows.Controls; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using CheckBox = System.Windows.Controls.CheckBox; +using Control = System.Windows.Controls.Control; +using Label = System.Windows.Controls.Label; +using Orientation = System.Windows.Controls.Orientation; +using TextBox = System.Windows.Controls.TextBox; +using UserControl = System.Windows.Controls.UserControl; +using System.Windows.Data; namespace Flow.Launcher.Core.Plugin { @@ -21,7 +35,7 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu + internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable { protected PluginInitContext context; public const string JsonRPC = "JsonRPC"; @@ -35,6 +49,9 @@ internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu private static readonly RecyclableMemoryStreamManager BufferManager = new(); + private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json"); + public List LoadContextMenus(Result selectedResult) { var request = new JsonRPCRequestModel @@ -59,6 +76,14 @@ public List LoadContextMenus(Result selectedResult) } }; + private static readonly JsonSerializerOptions settingSerializeOption = new() + { + WriteIndented = true + }; + private Dictionary Settings { get; set; } + + private Dictionary _settingControls = new(); + private async Task> DeserializedResultAsync(Stream output) { if (output == Stream.Null) return null; @@ -92,6 +117,8 @@ private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) { result.Action = c => { + UpdateSettings(result.SettingsChange); + if (result.JsonRPCAction == null) return false; if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) @@ -131,6 +158,8 @@ private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) results.AddRange(queryResponseModel.Result); + UpdateSettings(queryResponseModel.SettingsChange); + return results; } @@ -283,19 +312,222 @@ public async Task> QueryAsync(Query query, CancellationToken token) var request = new JsonRPCRequestModel { Method = "query", - Parameters = new[] + Parameters = new object[] { query.Search - } + }, + Settings = Settings }; var output = await RequestAsync(request, token); return await DeserializedResultAsync(output); } - public virtual Task InitAsync(PluginInitContext context) + public async Task InitSettingAsync() + { + if (!File.Exists(SettingConfigurationPath)) + return; + + if (File.Exists(SettingPath)) + { + await using var fileStream = File.OpenRead(SettingPath); + Settings = await JsonSerializer.DeserializeAsync>(fileStream, options); + } + + var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); + _settingsTemplate = deserializer.Deserialize(await File.ReadAllTextAsync(SettingConfigurationPath)); + + Settings ??= new Dictionary(); + + foreach (var (type, attribute) in _settingsTemplate.Body) + { + if (type == "textBlock") + continue; + if (!Settings.ContainsKey(attribute.Name)) + { + Settings[attribute.Name] = attribute.DefaultValue; + } + } + } + + public virtual async Task InitAsync(PluginInitContext context) { this.context = context; - return Task.CompletedTask; + await InitSettingAsync(); + } + private static readonly Thickness settingControlMargin = new(10); + private JsonRpcConfigurationModel _settingsTemplate; + public Control CreateSettingPanel() + { + if (Settings == null) + return new(); + var settingWindow = new UserControl(); + var mainPanel = new StackPanel + { + Margin = settingControlMargin, + Orientation = Orientation.Vertical + }; + settingWindow.Content = mainPanel; + + foreach (var (type, attribute) in _settingsTemplate.Body) + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = settingControlMargin + }; + var name = new Label() + { + Content = attribute.Label, + Margin = settingControlMargin + }; + + FrameworkElement contentControl; + + switch (type) + { + case "textBlock": + { + contentControl = new TextBlock + { + Text = attribute.Description.Replace("\\r\\n", "\r\n"), + Margin = settingControlMargin, + MaxWidth = 400, + TextWrapping = TextWrapping.WrapWithOverflow + }; + break; + } + case "input": + { + var textBox = new TextBox() + { + Width = 300, + Text = Settings[attribute.Name] as string ?? string.Empty, + Margin = settingControlMargin, + ToolTip = attribute.Description + }; + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + contentControl = textBox; + break; + } + case "textarea": + { + var textBox = new TextBox() + { + Width = 300, + Height = 120, + Margin = settingControlMargin, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + Text = Settings[attribute.Name] as string ?? string.Empty, + ToolTip = attribute.Description + }; + textBox.TextChanged += (sender, _) => + { + Settings[attribute.Name] = ((TextBox)sender).Text; + }; + contentControl = textBox; + break; + } + case "passwordBox": + { + var passwordBox = new PasswordBox() + { + Width = 300, + Margin = settingControlMargin, + Password = Settings[attribute.Name] as string ?? string.Empty, + PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, + ToolTip = attribute.Description + }; + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attribute.Name] = ((PasswordBox)sender).Password; + }; + contentControl = passwordBox; + break; + } + case "dropdown": + { + var comboBox = new ComboBox() + { + ItemsSource = attribute.Options, + SelectedItem = Settings[attribute.Name], + Margin = settingControlMargin, + ToolTip = attribute.Description + }; + comboBox.SelectionChanged += (sender, _) => + { + Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem; + }; + contentControl = comboBox; + break; + } + case "checkbox": + var checkBox = new CheckBox + { + IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue), + Margin = settingControlMargin, + ToolTip = attribute.Description + }; + checkBox.Click += (sender, _) => + { + Settings[attribute.Name] = ((CheckBox)sender).IsChecked; + }; + contentControl = checkBox; + break; + default: + continue; + } + if (type != "textBlock") + _settingControls[attribute.Name] = contentControl; + panel.Children.Add(name); + panel.Children.Add(contentControl); + mainPanel.Children.Add(panel); + } + return settingWindow; + } + public void Save() + { + if (Settings != null) + { + Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name)); + File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption)); + } + } + + public void UpdateSettings(Dictionary settings) + { + if (settings == null || settings.Count == 0) + return; + + foreach (var (key, value) in settings) + { + if (Settings.ContainsKey(key)) + { + Settings[key] = value; + } + if (_settingControls.ContainsKey(key)) + { + + switch (_settingControls[key]) + { + case TextBox textBox: + textBox.Dispatcher.Invoke(() => textBox.Text = value as string); + break; + case PasswordBox passwordBox: + passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string); + 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)); + break; + } + } + } } } } \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 855e5c90172..8f7e5760af4 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -51,15 +51,12 @@ protected override string Request(JsonRPCRequestModel rpcRequest, CancellationTo // TODO: Async Action return Execute(_startInfo); } - public override Task InitAsync(PluginInitContext context) + public override async Task InitAsync(PluginInitContext context) { - this.context = context; _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); _startInfo.ArgumentList.Add(""); - + await base.InitAsync(context); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; - - return Task.CompletedTask; } } }