Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Flow.Launcher.Core/Plugin/JsonPRCModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,19 @@ public class JsonRPCQueryResponseModel : JsonRPCResponseModel
[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
Expand Down Expand Up @@ -86,5 +90,7 @@ public class JsonRPCClientRequestModel : JsonRPCRequestModel
public class JsonRPCResult : Result
{
public JsonRPCClientRequestModel JsonRPCAction { get; set; }

public Dictionary<string, object> SettingsChange { get; set; }
}
}
43 changes: 43 additions & 0 deletions Flow.Launcher.Core/Plugin/JsonRPCConfigurationModel.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
245 changes: 237 additions & 8 deletions Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -8,25 +10,37 @@
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
{
/// <summary>
/// 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";

/// <summary>
/// <summary=
/// The language this JsonRPCPlugin support
/// </summary>
public abstract string SupportedLanguage { get; set; }
Expand All @@ -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<Result> LoadContextMenus(Result selectedResult)
{
var request = new JsonRPCRequestModel
Expand All @@ -59,6 +76,14 @@ public List<Result> LoadContextMenus(Result selectedResult)
}
};

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;
Expand Down Expand Up @@ -92,6 +117,8 @@ private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
{
result.Action = c =>
{
UpdateSettings(result.SettingsChange);

if (result.JsonRPCAction == null) return false;

if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
Expand Down Expand Up @@ -131,6 +158,8 @@ private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)

results.AddRange(queryResponseModel.Result);

UpdateSettings(queryResponseModel.SettingsChange);

return results;
}

Expand Down Expand Up @@ -283,19 +312,219 @@ public async Task<List<Result>> 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))
Settings = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(File.OpenRead(SettingPath), 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;
}
}
}
}
}
}
Loading