Skip to content

Commit f86dd9d

Browse files
authored
Merge pull request #657 from Flow-Launcher/JsonRPCPluginSettingControl
Json rpc plugin setting control
2 parents 2855c6c + 238d4df commit f86dd9d

File tree

4 files changed

+291
-13
lines changed

4 files changed

+291
-13
lines changed

Flow.Launcher.Core/Plugin/JsonPRCModel.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,19 @@ public class JsonRPCQueryResponseModel : JsonRPCResponseModel
4343
[JsonPropertyName("result")]
4444
public new List<JsonRPCResult> Result { get; set; }
4545

46+
public Dictionary<string, object> SettingsChange { get; set; }
47+
4648
public string DebugMessage { get; set; }
4749
}
48-
50+
4951
public class JsonRPCRequestModel
5052
{
5153
public string Method { get; set; }
5254

5355
public object[] Parameters { get; set; }
5456

57+
public Dictionary<string, object> Settings { get; set; }
58+
5559
private static readonly JsonSerializerOptions options = new()
5660
{
5761
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
@@ -86,5 +90,7 @@ public class JsonRPCClientRequestModel : JsonRPCRequestModel
8690
public class JsonRPCResult : Result
8791
{
8892
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
93+
94+
public Dictionary<string, object> SettingsChange { get; set; }
8995
}
9096
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Collections.Generic;
2+
3+
namespace Flow.Launcher.Core.Plugin
4+
{
5+
public class JsonRpcConfigurationModel
6+
{
7+
public List<SettingField> Body { get; set; }
8+
public void Deconstruct(out List<SettingField> Body)
9+
{
10+
Body = this.Body;
11+
}
12+
}
13+
14+
public class SettingField
15+
{
16+
public string Type { get; set; }
17+
public FieldAttributes Attributes { get; set; }
18+
public void Deconstruct(out string Type, out FieldAttributes attributes)
19+
{
20+
Type = this.Type;
21+
attributes = this.Attributes;
22+
}
23+
}
24+
public class FieldAttributes
25+
{
26+
public string Name { get; set; }
27+
public string Label { get; set; }
28+
public string Description { get; set; }
29+
public bool Validation { get; set; }
30+
public List<string> Options { get; set; }
31+
public string DefaultValue { get; set; }
32+
public char passwordChar { get; set; }
33+
public void Deconstruct(out string Name, out string Label, out string Description, out bool Validation, out List<string> Options, out string DefaultValue)
34+
{
35+
Name = this.Name;
36+
Label = this.Label;
37+
Description = this.Description;
38+
Validation = this.Validation;
39+
Options = this.Options;
40+
DefaultValue = this.DefaultValue;
41+
}
42+
}
43+
}

Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs

Lines changed: 239 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Flow.Launcher.Core.Resource;
1+
using Accessibility;
2+
using Flow.Launcher.Core.Resource;
3+
using Flow.Launcher.Infrastructure;
24
using System;
35
using System.Collections.Generic;
46
using System.Diagnostics;
@@ -8,20 +10,32 @@
810
using System.Text.Json;
911
using System.Threading;
1012
using System.Threading.Tasks;
11-
using System.Windows.Forms;
1213
using Flow.Launcher.Infrastructure.Logger;
14+
using Flow.Launcher.Infrastructure.UserSettings;
1315
using Flow.Launcher.Plugin;
1416
using ICSharpCode.SharpZipLib.Zip;
1517
using JetBrains.Annotations;
1618
using Microsoft.IO;
19+
using System.Text.Json.Serialization;
20+
using System.Windows;
21+
using System.Windows.Controls;
22+
using YamlDotNet.Serialization;
23+
using YamlDotNet.Serialization.NamingConventions;
24+
using CheckBox = System.Windows.Controls.CheckBox;
25+
using Control = System.Windows.Controls.Control;
26+
using Label = System.Windows.Controls.Label;
27+
using Orientation = System.Windows.Controls.Orientation;
28+
using TextBox = System.Windows.Controls.TextBox;
29+
using UserControl = System.Windows.Controls.UserControl;
30+
using System.Windows.Data;
1731

1832
namespace Flow.Launcher.Core.Plugin
1933
{
2034
/// <summary>
2135
/// Represent the plugin that using JsonPRC
2236
/// every JsonRPC plugin should has its own plugin instance
2337
/// </summary>
24-
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu
38+
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
2539
{
2640
protected PluginInitContext context;
2741
public const string JsonRPC = "JsonRPC";
@@ -35,6 +49,9 @@ internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu
3549

3650
private static readonly RecyclableMemoryStreamManager BufferManager = new();
3751

52+
private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
53+
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json");
54+
3855
public List<Result> LoadContextMenus(Result selectedResult)
3956
{
4057
var request = new JsonRPCRequestModel
@@ -59,6 +76,14 @@ public List<Result> LoadContextMenus(Result selectedResult)
5976
}
6077
};
6178

79+
private static readonly JsonSerializerOptions settingSerializeOption = new()
80+
{
81+
WriteIndented = true
82+
};
83+
private Dictionary<string, object> Settings { get; set; }
84+
85+
private Dictionary<string, FrameworkElement> _settingControls = new();
86+
6287
private async Task<List<Result>> DeserializedResultAsync(Stream output)
6388
{
6489
if (output == Stream.Null) return null;
@@ -92,6 +117,8 @@ private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
92117
{
93118
result.Action = c =>
94119
{
120+
UpdateSettings(result.SettingsChange);
121+
95122
if (result.JsonRPCAction == null) return false;
96123

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

132159
results.AddRange(queryResponseModel.Result);
133160

161+
UpdateSettings(queryResponseModel.SettingsChange);
162+
134163
return results;
135164
}
136165

@@ -283,19 +312,222 @@ public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
283312
var request = new JsonRPCRequestModel
284313
{
285314
Method = "query",
286-
Parameters = new[]
315+
Parameters = new object[]
287316
{
288317
query.Search
289-
}
318+
},
319+
Settings = Settings
290320
};
291321
var output = await RequestAsync(request, token);
292322
return await DeserializedResultAsync(output);
293323
}
294324

295-
public virtual Task InitAsync(PluginInitContext context)
325+
public async Task InitSettingAsync()
326+
{
327+
if (!File.Exists(SettingConfigurationPath))
328+
return;
329+
330+
if (File.Exists(SettingPath))
331+
{
332+
await using var fileStream = File.OpenRead(SettingPath);
333+
Settings = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(fileStream, options);
334+
}
335+
336+
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
337+
_settingsTemplate = deserializer.Deserialize<JsonRpcConfigurationModel>(await File.ReadAllTextAsync(SettingConfigurationPath));
338+
339+
Settings ??= new Dictionary<string, object>();
340+
341+
foreach (var (type, attribute) in _settingsTemplate.Body)
342+
{
343+
if (type == "textBlock")
344+
continue;
345+
if (!Settings.ContainsKey(attribute.Name))
346+
{
347+
Settings[attribute.Name] = attribute.DefaultValue;
348+
}
349+
}
350+
}
351+
352+
public virtual async Task InitAsync(PluginInitContext context)
296353
{
297354
this.context = context;
298-
return Task.CompletedTask;
355+
await InitSettingAsync();
356+
}
357+
private static readonly Thickness settingControlMargin = new(10);
358+
private JsonRpcConfigurationModel _settingsTemplate;
359+
public Control CreateSettingPanel()
360+
{
361+
if (Settings == null)
362+
return new();
363+
var settingWindow = new UserControl();
364+
var mainPanel = new StackPanel
365+
{
366+
Margin = settingControlMargin,
367+
Orientation = Orientation.Vertical
368+
};
369+
settingWindow.Content = mainPanel;
370+
371+
foreach (var (type, attribute) in _settingsTemplate.Body)
372+
{
373+
var panel = new StackPanel
374+
{
375+
Orientation = Orientation.Horizontal,
376+
Margin = settingControlMargin
377+
};
378+
var name = new Label()
379+
{
380+
Content = attribute.Label,
381+
Margin = settingControlMargin
382+
};
383+
384+
FrameworkElement contentControl;
385+
386+
switch (type)
387+
{
388+
case "textBlock":
389+
{
390+
contentControl = new TextBlock
391+
{
392+
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
393+
Margin = settingControlMargin,
394+
MaxWidth = 400,
395+
TextWrapping = TextWrapping.WrapWithOverflow
396+
};
397+
break;
398+
}
399+
case "input":
400+
{
401+
var textBox = new TextBox()
402+
{
403+
Width = 300,
404+
Text = Settings[attribute.Name] as string ?? string.Empty,
405+
Margin = settingControlMargin,
406+
ToolTip = attribute.Description
407+
};
408+
textBox.TextChanged += (_, _) =>
409+
{
410+
Settings[attribute.Name] = textBox.Text;
411+
};
412+
contentControl = textBox;
413+
break;
414+
}
415+
case "textarea":
416+
{
417+
var textBox = new TextBox()
418+
{
419+
Width = 300,
420+
Height = 120,
421+
Margin = settingControlMargin,
422+
TextWrapping = TextWrapping.WrapWithOverflow,
423+
AcceptsReturn = true,
424+
Text = Settings[attribute.Name] as string ?? string.Empty,
425+
ToolTip = attribute.Description
426+
};
427+
textBox.TextChanged += (sender, _) =>
428+
{
429+
Settings[attribute.Name] = ((TextBox)sender).Text;
430+
};
431+
contentControl = textBox;
432+
break;
433+
}
434+
case "passwordBox":
435+
{
436+
var passwordBox = new PasswordBox()
437+
{
438+
Width = 300,
439+
Margin = settingControlMargin,
440+
Password = Settings[attribute.Name] as string ?? string.Empty,
441+
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
442+
ToolTip = attribute.Description
443+
};
444+
passwordBox.PasswordChanged += (sender, _) =>
445+
{
446+
Settings[attribute.Name] = ((PasswordBox)sender).Password;
447+
};
448+
contentControl = passwordBox;
449+
break;
450+
}
451+
case "dropdown":
452+
{
453+
var comboBox = new ComboBox()
454+
{
455+
ItemsSource = attribute.Options,
456+
SelectedItem = Settings[attribute.Name],
457+
Margin = settingControlMargin,
458+
ToolTip = attribute.Description
459+
};
460+
comboBox.SelectionChanged += (sender, _) =>
461+
{
462+
Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem;
463+
};
464+
contentControl = comboBox;
465+
break;
466+
}
467+
case "checkbox":
468+
var checkBox = new CheckBox
469+
{
470+
IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
471+
Margin = settingControlMargin,
472+
ToolTip = attribute.Description
473+
};
474+
checkBox.Click += (sender, _) =>
475+
{
476+
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
477+
};
478+
contentControl = checkBox;
479+
break;
480+
default:
481+
continue;
482+
}
483+
if (type != "textBlock")
484+
_settingControls[attribute.Name] = contentControl;
485+
panel.Children.Add(name);
486+
panel.Children.Add(contentControl);
487+
mainPanel.Children.Add(panel);
488+
}
489+
return settingWindow;
490+
}
491+
public void Save()
492+
{
493+
if (Settings != null)
494+
{
495+
Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name));
496+
File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption));
497+
}
498+
}
499+
500+
public void UpdateSettings(Dictionary<string, object> settings)
501+
{
502+
if (settings == null || settings.Count == 0)
503+
return;
504+
505+
foreach (var (key, value) in settings)
506+
{
507+
if (Settings.ContainsKey(key))
508+
{
509+
Settings[key] = value;
510+
}
511+
if (_settingControls.ContainsKey(key))
512+
{
513+
514+
switch (_settingControls[key])
515+
{
516+
case TextBox textBox:
517+
textBox.Dispatcher.Invoke(() => textBox.Text = value as string);
518+
break;
519+
case PasswordBox passwordBox:
520+
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string);
521+
break;
522+
case ComboBox comboBox:
523+
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
524+
break;
525+
case CheckBox checkBox:
526+
checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string));
527+
break;
528+
}
529+
}
530+
}
299531
}
300532
}
301533
}

0 commit comments

Comments
 (0)