1- using Flow . Launcher . Core . Resource ;
1+ using Accessibility ;
2+ using Flow . Launcher . Core . Resource ;
3+ using Flow . Launcher . Infrastructure ;
24using System ;
35using System . Collections . Generic ;
46using System . Diagnostics ;
810using System . Text . Json ;
911using System . Threading ;
1012using System . Threading . Tasks ;
11- using System . Windows . Forms ;
1213using Flow . Launcher . Infrastructure . Logger ;
14+ using Flow . Launcher . Infrastructure . UserSettings ;
1315using Flow . Launcher . Plugin ;
1416using ICSharpCode . SharpZipLib . Zip ;
1517using JetBrains . Annotations ;
1618using 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
1832namespace 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