Skip to content

Commit ac0bfd6

Browse files
committed
Merge remote-tracking branch 'upstream/dev' into GlyphIcon
# Conflicts: # Flow.Launcher/ResultListBox.xaml # Flow.Launcher/ViewModel/ResultViewModel.cs
2 parents 9eeb04e + 4219dce commit ac0bfd6

File tree

15 files changed

+106
-158
lines changed

15 files changed

+106
-158
lines changed

Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs

Lines changed: 17 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,7 @@ internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu
4040
public List<Result> LoadContextMenus(Result selectedResult)
4141
{
4242
var output = ExecuteContextMenu(selectedResult);
43-
try
44-
{
45-
return DeserializedResult(output);
46-
}
47-
catch (Exception e)
48-
{
49-
Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e);
50-
return null;
51-
}
43+
return DeserializedResult(output);
5244
}
5345

5446
private static readonly JsonSerializerOptions options = new()
@@ -65,23 +57,10 @@ private async Task<List<Result>> DeserializedResultAsync(Stream output)
6557
{
6658
if (output == Stream.Null) return null;
6759

68-
try
69-
{
70-
var queryResponseModel =
71-
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
72-
73-
return ParseResults(queryResponseModel);
74-
}
75-
catch (JsonException e)
76-
{
77-
Log.Exception(GetType().FullName, "Unexpected Json Input", e);
78-
}
79-
finally
80-
{
81-
await output.DisposeAsync();
82-
}
60+
var queryResponseModel =
61+
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
8362

84-
return null;
63+
return ParseResults(queryResponseModel);
8564
}
8665

8766
private List<Result> DeserializedResult(string output)
@@ -249,7 +228,7 @@ protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, Cancellati
249228
await using var source = process.StandardOutput.BaseStream;
250229

251230
var buffer = BufferManager.GetStream();
252-
231+
253232
token.Register(() =>
254233
{
255234
// ReSharper disable once AccessToModifiedClosure
@@ -274,30 +253,27 @@ protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, Cancellati
274253

275254
token.ThrowIfCancellationRequested();
276255

256+
if (buffer.Length == 0)
257+
{
258+
var errorMessage = process.StandardError.EndOfStream ?
259+
"Empty JSONRPC Response" :
260+
await process.StandardError.ReadToEndAsync();
261+
throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}");
262+
}
263+
277264
if (!process.StandardError.EndOfStream)
278265
{
279266
using var standardError = process.StandardError;
280267
var error = await standardError.ReadToEndAsync();
281268

282269
if (!string.IsNullOrEmpty(error))
283270
{
284-
Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}");
285-
return Stream.Null;
271+
Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}");
286272
}
287-
288-
Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error.");
289-
return Stream.Null;
290273
}
291274

292275
return buffer;
293276
}
294-
catch (Exception e)
295-
{
296-
Log.Exception(
297-
$"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
298-
e);
299-
return Stream.Null;
300-
}
301277
finally
302278
{
303279
process?.Dispose();
@@ -307,20 +283,8 @@ protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, Cancellati
307283

308284
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
309285
{
310-
try
311-
{
312-
var output = await ExecuteQueryAsync(query, token);
313-
return await DeserializedResultAsync(output);
314-
}
315-
catch (OperationCanceledException)
316-
{
317-
return null;
318-
}
319-
catch (Exception e)
320-
{
321-
Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e);
322-
return null;
323-
}
286+
var output = await ExecuteQueryAsync(query, token);
287+
return await DeserializedResultAsync(output);
324288
}
325289

326290
public virtual Task InitAsync(PluginInitContext context)
@@ -329,4 +293,4 @@ public virtual Task InitAsync(PluginInitContext context)
329293
return Task.CompletedTask;
330294
}
331295
}
332-
}
296+
}

Flow.Launcher.Infrastructure/Image/ImageCache.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using System.Windows.Media;
78

@@ -26,7 +27,8 @@ public class ImageCache
2627
private const int MaxCached = 50;
2728
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
2829
private const int permissibleFactor = 2;
29-
30+
private SemaphoreSlim semaphore = new(1, 1);
31+
3032
public void Initialization(Dictionary<string, int> usage)
3133
{
3234
foreach (var key in usage.Keys)
@@ -60,20 +62,29 @@ public ImageSource this[string path]
6062
}
6163
);
6264

63-
// To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
64-
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
65-
if (Data.Count > permissibleFactor * MaxCached)
65+
SliceExtra();
66+
67+
async void SliceExtra()
6668
{
67-
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary.
68-
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
69-
Data.TryRemove(key, out _);
69+
// To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
70+
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
71+
if (Data.Count > permissibleFactor * MaxCached)
72+
{
73+
await semaphore.WaitAsync().ConfigureAwait(false);
74+
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary
75+
// Double Check to avoid concurrent remove
76+
if (Data.Count > permissibleFactor * MaxCached)
77+
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key).ToArray())
78+
Data.TryRemove(key, out _);
79+
semaphore.Release();
80+
}
7081
}
7182
}
7283
}
7384

7485
public bool ContainsKey(string key)
7586
{
76-
return Data.ContainsKey(key) && Data[key].imageSource != null;
87+
return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null;
7788
}
7889

7990
public int CacheSize()

Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void UpdatePluginSettings(List<PluginMetadata> metadatas)
1818

1919
// TODO: Remove. This is backwards compatibility for 1.8.0 release.
2020
// Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder.
21-
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count)
21+
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count > settings.ActionKeywords.Count)
2222
{
2323
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search
2424
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search

Flow.Launcher.Infrastructure/UserSettings/Settings.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public string Language
3939
/// </summary>
4040
public bool ShouldUsePinyin { get; set; } = false;
4141

42-
internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
42+
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
43+
public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
4344

4445
[JsonIgnore]
4546
public string QuerySearchPrecisionString

Flow.Launcher/ResultListBox.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<ColumnDefinition Width="0" />
4343
</Grid.ColumnDefinitions>
4444
<Image x:Name="ImageIcon" Width="32" Height="32" HorizontalAlignment="Left"
45-
Source="{Binding Image.Value}" Visibility="{Binding ShowImage}" />
45+
Source="{Binding Image}" Visibility="{Binding ShowIcon}" />
4646
<TextBlock Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"
4747
Text="{Binding Glyph.Glyph}" FontFamily="{Binding Glyph.FontFamily}" FontSize="24"
4848
Visibility="{Binding ShowGlyph}"/>

Flow.Launcher/ViewModel/MainViewModel.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@ async Task updateAction()
108108
}
109109

110110
Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends");
111-
}
112-
113-
;
111+
};
114112

115113
void continueAction(Task t)
116114
{

Flow.Launcher/ViewModel/ResultViewModel.cs

Lines changed: 46 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,71 +12,23 @@ namespace Flow.Launcher.ViewModel
1212
{
1313
public class ResultViewModel : BaseModel
1414
{
15-
public class LazyAsync<T> : Lazy<ValueTask<T>>
16-
{
17-
private readonly T defaultValue;
18-
19-
private readonly Action _updateCallback;
20-
public new T Value
21-
{
22-
get
23-
{
24-
if (!IsValueCreated)
25-
{
26-
_ = Exercute(); // manually use callback strategy
27-
28-
return defaultValue;
29-
}
30-
31-
if (!base.Value.IsCompletedSuccessfully)
32-
return defaultValue;
33-
34-
return base.Value.Result;
35-
36-
// If none of the variables captured by the local function are captured by other lambdas,
37-
// the compiler can avoid heap allocations.
38-
async ValueTask Exercute()
39-
{
40-
await base.Value.ConfigureAwait(false);
41-
_updateCallback();
42-
}
43-
44-
}
45-
}
46-
public LazyAsync(Func<ValueTask<T>> factory, T defaultValue, Action updateCallback) : base(factory)
47-
{
48-
if (defaultValue != null)
49-
{
50-
this.defaultValue = defaultValue;
51-
}
52-
53-
_updateCallback = updateCallback;
54-
}
55-
}
56-
5715
public ResultViewModel(Result result, Settings settings)
5816
{
5917
if (result != null)
6018
{
6119
Result = result;
6220

63-
Image = new LazyAsync<ImageSource>(
64-
SetImage,
65-
ImageLoader.DefaultImage,
66-
() =>
67-
{
68-
OnPropertyChanged(nameof(Image));
69-
});
70-
7121
if (Result.Glyph is { FontFamily: not null } glyph)
7222
{
73-
if (glyph.FontFamily.Contains('/'))
23+
if (glyph.FontFamily.EndsWith(".ttf") || glyph.FontFamily.EndsWith(".otf"))
7424
{
7525
var fontPath = Result.Glyph.FontFamily;
76-
Glyph = Path.IsPathRooted(fontPath) ? Result.Glyph : Result.Glyph with
77-
{
78-
FontFamily = Path.Combine(Result.PluginDirectory, fontPath)
79-
};
26+
Glyph = Path.IsPathRooted(fontPath)
27+
? Result.Glyph
28+
: Result.Glyph with
29+
{
30+
FontFamily = Path.Combine(Result.PluginDirectory, fontPath)
31+
};
8032
}
8133
else
8234
{
@@ -88,10 +40,15 @@ public ResultViewModel(Result result, Settings settings)
8840
Settings = settings;
8941
}
9042

91-
public Settings Settings { get; private set; }
43+
private Settings Settings { get; }
44+
45+
public Visibility ShowOpenResultHotkey =>
46+
Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Hidden;
47+
48+
public Visibility ShowIcon => Result.IcoPath != null || Result.Icon is not null || Glyph == null
49+
? Visibility.Visible
50+
: Visibility.Hidden;
9251

93-
public Visibility ShowOpenResultHotkey => Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Hidden;
94-
public Visibility ShowIcon => Result.IcoPath != null || Result.Icon is not null || Glyph == null ? Visibility.Visible : Visibility.Hidden;
9552
public Visibility ShowGlyph => Glyph is not null ? Visibility.Visible : Visibility.Hidden;
9653
public string OpenResultModifiers => Settings.OpenResultModifiers;
9754

@@ -103,46 +60,61 @@ public ResultViewModel(Result result, Settings settings)
10360
? Result.SubTitle
10461
: Result.SubTitleToolTip;
10562

106-
public LazyAsync<ImageSource> Image { get; set; }
63+
private volatile bool ImageLoaded;
64+
65+
private ImageSource image = ImageLoader.DefaultImage;
66+
67+
public ImageSource Image
68+
{
69+
get
70+
{
71+
if (!ImageLoaded)
72+
{
73+
ImageLoaded = true;
74+
_ = LoadImageAsync();
75+
}
76+
77+
return image;
78+
}
79+
private set => image = value;
80+
}
10781

10882
public GlyphInfo Glyph { get; set; }
10983

110-
private async ValueTask<ImageSource> SetImage()
84+
private async ValueTask LoadImageAsync()
11185
{
11286
var imagePath = Result.IcoPath;
11387
if (string.IsNullOrEmpty(imagePath) && Result.Icon != null)
11488
{
11589
try
11690
{
117-
return Result.Icon();
91+
image = Result.Icon();
92+
return;
11893
}
11994
catch (Exception e)
12095
{
121-
Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e);
122-
return ImageLoader.DefaultImage;
96+
Log.Exception(
97+
$"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>",
98+
e);
12399
}
124100
}
125101

126102
if (ImageLoader.CacheContainImage(imagePath))
103+
{
127104
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate
128-
return ImageLoader.Load(imagePath);
105+
image = ImageLoader.Load(imagePath);
106+
return;
107+
}
129108

130-
return await Task.Run(() => ImageLoader.Load(imagePath));
109+
// We need to modify the property not field here to trigger the OnPropertyChanged event
110+
Image = await Task.Run(() => ImageLoader.Load(imagePath)).ConfigureAwait(false);
131111
}
132112

133113
public Result Result { get; }
134114

135115
public override bool Equals(object obj)
136116
{
137-
var r = obj as ResultViewModel;
138-
if (r != null)
139-
{
140-
return Result.Equals(r.Result);
141-
}
142-
else
143-
{
144-
return false;
145-
}
117+
return obj is ResultViewModel r && Result.Equals(r.Result);
146118
}
147119

148120
public override int GetHashCode()

0 commit comments

Comments
 (0)