Merge pull request #657 from Flow-Launcher/JsonRPCPluginSettingControl

Json rpc plugin setting control
This commit is contained in:
Kevin Zhang 2021-12-08 21:49:08 -06:00 committed by GitHub
commit f86dd9d287
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 291 additions and 13 deletions

View file

@ -43,15 +43,19 @@ namespace Flow.Launcher.Core.Plugin
[JsonPropertyName("result")]
public new List<JsonRPCResult> Result { get; set; }
public Dictionary<string, object> SettingsChange { get; set; }
public string DebugMessage { get; set; }
}
public class JsonRPCRequestModel
{
public string Method { get; set; }
public object[] Parameters { get; set; }
public Dictionary<string, object> Settings { get; set; }
private static readonly JsonSerializerOptions options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@ -86,5 +90,7 @@ namespace Flow.Launcher.Core.Plugin
public class JsonRPCResult : Result
{
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
public Dictionary<string, object> SettingsChange { get; set; }
}
}

View file

@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace Flow.Launcher.Core.Plugin
{
public class JsonRpcConfigurationModel
{
public List<SettingField> Body { get; set; }
public void Deconstruct(out List<SettingField> 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<string> 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<string> Options, out string DefaultValue)
{
Name = this.Name;
Label = this.Label;
Description = this.Description;
Validation = this.Validation;
Options = this.Options;
DefaultValue = this.DefaultValue;
}
}
}

View file

@ -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.Reflection;
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
/// </summary>
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 @@ namespace Flow.Launcher.Core.Plugin
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<Result> LoadContextMenus(Result selectedResult)
{
var request = new JsonRPCRequestModel
@ -59,6 +76,14 @@ namespace Flow.Launcher.Core.Plugin
}
};
private static readonly JsonSerializerOptions settingSerializeOption = new()
{
WriteIndented = true
};
private Dictionary<string, object> Settings { get; set; }
private Dictionary<string, FrameworkElement> _settingControls = new();
private async Task<List<Result>> DeserializedResultAsync(Stream output)
{
if (output == Stream.Null) return null;
@ -92,6 +117,8 @@ namespace Flow.Launcher.Core.Plugin
{
result.Action = c =>
{
UpdateSettings(result.SettingsChange);
if (result.JsonRPCAction == null) return false;
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
@ -131,6 +158,8 @@ namespace Flow.Launcher.Core.Plugin
results.AddRange(queryResponseModel.Result);
UpdateSettings(queryResponseModel.SettingsChange);
return results;
}
@ -283,19 +312,222 @@ namespace Flow.Launcher.Core.Plugin
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<Dictionary<string, object>>(fileStream, options);
}
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
_settingsTemplate = deserializer.Deserialize<JsonRpcConfigurationModel>(await File.ReadAllTextAsync(SettingConfigurationPath));
Settings ??= new Dictionary<string, object>();
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<string, object> 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;
}
}
}
}
}
}

View file

@ -51,15 +51,12 @@ namespace Flow.Launcher.Core.Plugin
// 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;
}
}
}