Skip to content

Commit 6c8add0

Browse files
authored
Merge pull request #4057 from Flow-Launcher/last_history_show_result_icon
History results display actual result icon for Last Opened history style
2 parents add111f + 248da93 commit 6c8add0

File tree

10 files changed

+358
-133
lines changed

10 files changed

+358
-133
lines changed

Flow.Launcher.Plugin/Result.cs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
using System.Threading.Tasks;
55
using System.Windows.Controls;
66
using System.Windows.Media;
7+
using System.Text.Json.Serialization;
78

89
namespace Flow.Launcher.Plugin
910
{
1011
/// <summary>
11-
/// Describes a result of a <see cref="Query"/> executed by a plugin
12+
/// Describes a result of a <see cref="Query"/> executed by a plugin.
13+
/// This or its child classes is serializable.
1214
/// </summary>
1315
public class Result
1416
{
@@ -21,6 +23,8 @@ public class Result
2123

2224
private string _icoPath;
2325

26+
private string _icoPathAbsolute;
27+
2428
private string _copyText = string.Empty;
2529

2630
private string _badgeIcoPath;
@@ -64,15 +68,27 @@ public string CopyText
6468
public string AutoCompleteText { get; set; }
6569

6670
/// <summary>
67-
/// The image to be displayed for the result.
71+
/// Path or URI to the icon image for this result.
72+
/// Updates <see cref="IcoPathAbsolute"/> appropriately when set.
6873
/// </summary>
69-
/// <value>Can be a local file path or a URL.</value>
70-
/// <remarks>GlyphInfo is prioritized if not null</remarks>
74+
/// <remarks>
75+
/// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png").
76+
/// Because <see cref="IcoPath"/> is serialized, using relative paths keeps the icon reference portable
77+
/// when Flow is moved.
78+
///
79+
/// Accepted formats:
80+
/// - Relative file paths (resolved against <see cref="PluginDirectory"/> into <see cref="IcoPathAbsolute"/>)
81+
/// - Absolute file paths (left as-is)
82+
/// - HTTP/HTTPS URLs (left as-is)
83+
/// - Data URIs (left as-is)
84+
/// </remarks>
7185
public string IcoPath
7286
{
7387
get => _icoPath;
7488
set
7589
{
90+
_icoPath = value;
91+
7692
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
7793
if (!string.IsNullOrEmpty(value)
7894
&& !string.IsNullOrEmpty(PluginDirectory)
@@ -81,15 +97,23 @@ public string IcoPath
8197
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
8298
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
8399
{
84-
_icoPath = Path.Combine(PluginDirectory, value);
100+
_icoPathAbsolute = Path.Combine(PluginDirectory, value);
85101
}
86102
else
87103
{
88-
_icoPath = value;
104+
_icoPathAbsolute = value;
89105
}
90106
}
91107
}
92108

109+
/// <summary>
110+
/// Absolute path or URI which is used to load and display the result icon for Flow.
111+
/// This is populated by the <see cref="IcoPath"/> setter.
112+
/// If a relative path was provided to <see cref="IcoPath"/>, this property will contain the resolved
113+
/// absolute local path after combining with <see cref="PluginDirectory"/>.
114+
/// </summary>
115+
public string IcoPathAbsolute => _icoPathAbsolute;
116+
93117
/// <summary>
94118
/// The image to be displayed for the badge of the result.
95119
/// </summary>
@@ -131,17 +155,34 @@ public string BadgeIcoPath
131155
/// <summary>
132156
/// Delegate to load an icon for this result.
133157
/// </summary>
158+
[JsonIgnore]
134159
public IconDelegate Icon = null;
135160

136161
/// <summary>
137162
/// Delegate to load an icon for the badge of this result.
138163
/// </summary>
164+
[JsonIgnore]
139165
public IconDelegate BadgeIcon = null;
140166

167+
private GlyphInfo _glyph;
168+
141169
/// <summary>
142170
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
143171
/// </summary>
144-
public GlyphInfo Glyph { get; init; }
172+
public GlyphInfo Glyph
173+
{
174+
get => _glyph;
175+
init => _glyph = value;
176+
}
177+
178+
/// <summary>
179+
/// Set the Glyph Icon after initialization
180+
/// </summary>
181+
/// <param name="glyph"></param>
182+
public void SetGlyph(GlyphInfo glyph)
183+
{
184+
_glyph = glyph;
185+
}
145186

146187
/// <summary>
147188
/// An action to take in the form of a function call when the result has been selected.
@@ -151,6 +192,7 @@ public string BadgeIcoPath
151192
/// Its result determines what happens to Flow Launcher's query form:
152193
/// when true, the form will be hidden; when false, it will stay in focus.
153194
/// </remarks>
195+
[JsonIgnore]
154196
public Func<ActionContext, bool> Action { get; set; }
155197

156198
/// <summary>
@@ -161,6 +203,7 @@ public string BadgeIcoPath
161203
/// Its result determines what happens to Flow Launcher's query form:
162204
/// when true, the form will be hidden; when false, it will stay in focus.
163205
/// </remarks>
206+
[JsonIgnore]
164207
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
165208

166209
/// <summary>
@@ -203,11 +246,13 @@ public string PluginDirectory
203246
/// <example>
204247
/// As external information for ContextMenu
205248
/// </example>
249+
[JsonIgnore]
206250
public object ContextData { get; set; }
207251

208252
/// <summary>
209253
/// Plugin ID that generated this result
210254
/// </summary>
255+
[JsonInclude]
211256
public string PluginID { get; internal set; }
212257

213258
/// <summary>
@@ -223,6 +268,7 @@ public string PluginDirectory
223268
/// <summary>
224269
/// Customized Preview Panel
225270
/// </summary>
271+
[JsonIgnore]
226272
public Lazy<UserControl> PreviewPanel { get; set; }
227273

228274
/// <summary>
@@ -352,6 +398,7 @@ public record PreviewInfo
352398
/// <summary>
353399
/// Delegate to get the preview panel's image
354400
/// </summary>
401+
[JsonIgnore]
355402
public IconDelegate PreviewDelegate { get; set; } = null;
356403

357404
/// <summary>

Flow.Launcher/App.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
259259

260260
await PluginManager.InitializePluginsAsync(_mainVM);
261261

262+
// Refresh the history results after plugins are initialized so that we can parse the absolute icon paths
263+
_mainVM.RefreshLastOpenedHistoryResults();
264+
262265
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
263266
// And home page is created without full plugin list
264267
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))

Flow.Launcher/Helper/ResultHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Flow.Launcher.Helper;
1111

1212
public static class ResultHelper
1313
{
14-
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryItem item)
14+
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryResult item)
1515
{
1616
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
1717
}
@@ -24,7 +24,7 @@ public static class ResultHelper
2424
if (query == null) return null;
2525
try
2626
{
27-
var freshResults = await plugin.Plugin.QueryAsync(query, CancellationToken.None);
27+
var freshResults = await PluginManager.QueryForPluginAsync(plugin, query, CancellationToken.None);
2828
// Try to match by record key first if it is valid, otherwise fall back to title + subtitle match
2929
if (string.IsNullOrEmpty(recordKey))
3030
{

Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@
333333
Margin="18 24 0 0"
334334
HorizontalAlignment="Left"
335335
RenderOptions.BitmapScalingMode="Fant"
336-
Source="{Binding IcoPath, IsAsync=True}" />
336+
Source="{Binding IcoPathAbsolute, IsAsync=True}" />
337337
<Border
338338
x:Name="LabelUpdate"
339339
Height="12"

Flow.Launcher/Storage/HistoryItem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Flow.Launcher.Storage
44
{
5-
[Obsolete("Use LastOpenedHistoryItem instead. This class will be removed in future versions.")]
5+
[Obsolete("Use LastOpenedHistoryResult instead. This class will be removed in future versions.")]
66
public class HistoryItem
77
{
88
public string Query { get; set; }

Flow.Launcher/Storage/LastOpenedHistoryItem.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System;
2+
using Flow.Launcher.Infrastructure;
3+
using Flow.Launcher.Plugin;
4+
5+
namespace Flow.Launcher.Storage;
6+
7+
/// <summary>
8+
/// A serializable result used to record the last opened history for reopening results.
9+
/// Inherits common result fields from <see cref="Result"/> and adds the original query and execution time.
10+
/// </summary>
11+
public class LastOpenedHistoryResult : Result
12+
{
13+
/// <summary>
14+
/// The query string from Query.TrimmedQuery property, it is stored as a string instead of the entire Query class <see cref="Result"/>.
15+
/// This is used so results can be reopened or re-run using the serialized query string.
16+
/// </summary>
17+
public string Query { get; set; } = string.Empty;
18+
19+
/// <summary>
20+
/// The local date and time when this result was executed/opened.
21+
/// </summary>
22+
public DateTime ExecutedDateTime { get; set; }
23+
24+
/// <summary>
25+
/// Initializes a new instance of <see cref="LastOpenedHistoryResult"/>.
26+
/// </summary>
27+
public LastOpenedHistoryResult()
28+
{
29+
}
30+
31+
/// <summary>
32+
/// Creates a <see cref="LastOpenedHistoryResult"/> from an existing <see cref="Result"/>.
33+
/// Copies required fields and sets up default reopening actions.
34+
/// </summary>
35+
/// <param name="result">The original result to create history from.</param>
36+
public LastOpenedHistoryResult(Result result)
37+
{
38+
Title = result.Title;
39+
SubTitle = result.SubTitle;
40+
PluginID = result.PluginID;
41+
Query = result.OriginQuery.TrimmedQuery;
42+
OriginQuery = result.OriginQuery;
43+
RecordKey = result.RecordKey;
44+
IcoPath = result.IcoPath;
45+
PluginDirectory = result.PluginDirectory;
46+
Glyph = result.Glyph;
47+
ExecutedDateTime = DateTime.Now;
48+
// Used for Query History style reopening
49+
Action = _ =>
50+
{
51+
App.API.BackToQueryResults();
52+
App.API.ChangeQuery(result.OriginQuery.TrimmedQuery);
53+
return false;
54+
};
55+
// Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs
56+
AsyncAction = null;
57+
}
58+
59+
/// <summary>
60+
/// Selectively creates a deep copy of the required properties for <see cref="LastOpenedHistoryResult"/>
61+
/// based on the style of history- Last Opened or Query.
62+
/// This copy should be independent of original and full isolated.
63+
/// </summary>
64+
/// <returns>A new <see cref="LastOpenedHistoryResult"/> containing the same required data.</returns>
65+
public LastOpenedHistoryResult DeepCopyForHistoryStyle(bool isHistoryStyleLastOpened)
66+
{
67+
// queryValue and glyphValue are captured to ensure they are correctly referenced in the Action delegate.
68+
var queryValue = Query;
69+
var glyphValue = Glyph;
70+
71+
var title = string.Empty;
72+
var showBadge = false;
73+
var badgeIcoPath = string.Empty;
74+
var icoPath = string.Empty;
75+
var glyph = null as GlyphInfo;
76+
77+
if (isHistoryStyleLastOpened)
78+
{
79+
title = Title;
80+
icoPath = IcoPath;
81+
glyph = glyphValue != null
82+
? new GlyphInfo(glyphValue.FontFamily, glyphValue.Glyph)
83+
: null;
84+
showBadge = true;
85+
badgeIcoPath = Constant.HistoryIcon;
86+
}
87+
else
88+
{
89+
title = Localize.executeQuery(Query);
90+
icoPath = Constant.HistoryIcon;
91+
glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C");
92+
showBadge = false;
93+
}
94+
95+
return new LastOpenedHistoryResult
96+
{
97+
Title = title,
98+
// Subtitle has datetime which can cause duplicates when saving.
99+
SubTitle = Localize.lastExecuteTime(ExecutedDateTime),
100+
// Empty PluginID so the source of last opened history results won't be updated, this copy is meant to be temporary.
101+
PluginID = string.Empty,
102+
Query = Query,
103+
OriginQuery = new Query { TrimmedQuery = Query },
104+
RecordKey = RecordKey,
105+
IcoPath = icoPath,
106+
ShowBadge = showBadge,
107+
BadgeIcoPath = badgeIcoPath,
108+
PluginDirectory = PluginDirectory,
109+
// Used for Query History style reopening
110+
Action = _ =>
111+
{
112+
App.API.BackToQueryResults();
113+
App.API.ChangeQuery(queryValue);
114+
return false;
115+
},
116+
// Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs
117+
AsyncAction = null,
118+
Glyph = glyph,
119+
ExecutedDateTime = ExecutedDateTime
120+
// Note: Other properties are left as default — copy if needed.
121+
};
122+
}
123+
124+
/// <summary>
125+
/// Determines whether the specified <see cref="Result"/> is equivalent to this history result.
126+
/// Comparison uses <see cref="Result.RecordKey"/> when available; otherwise falls back to title/subtitle/plugin id and query.
127+
/// </summary>
128+
/// <param name="r">The result to compare to.</param>
129+
/// <returns><c>true</c> if the results are considered equal; otherwise <c>false</c>.</returns>
130+
public bool Equals(Result r)
131+
{
132+
if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey))
133+
{
134+
return Title == r.Title
135+
&& SubTitle == r.SubTitle
136+
&& PluginID == r.PluginID
137+
&& Query == r.OriginQuery.TrimmedQuery;
138+
}
139+
else
140+
{
141+
return RecordKey == r.RecordKey
142+
&& PluginID == r.PluginID
143+
&& Query == r.OriginQuery.TrimmedQuery;
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)