Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion Flow.Launcher.Infrastructure/Constant.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO;
using System.Reflection;

Expand Down Expand Up @@ -35,6 +35,7 @@ public static class Constant
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
public static readonly string SettingsIcon = Path.Combine(ImagesDirectory, "settings.png");
public static readonly string FolderIcon = Path.Combine(ImagesDirectory, "folder.png");
public static readonly string DeleteIcon = Path.Combine(ImagesDirectory, "delete.png");

public static string PythonPath;
public static string NodePath;
Expand Down
50 changes: 50 additions & 0 deletions Flow.Launcher.Test/QueryHistoryTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Flow.Launcher.Storage;
using NUnit.Framework;

namespace Flow.Launcher.Test
{
[TestFixture]
public class QueryHistoryTest
{
[Test]
public void Remove_RemovesOnlySelectedEntryByDefault()
{
var history = new History();
var selected = CreateHistoryItem("query one");
var otherQuery = CreateHistoryItem("query two");
history.LastOpenedHistoryItems.AddRange([selected, otherQuery]);

var removedCount = history.Remove(selected);

Assert.That(removedCount, Is.EqualTo(1));
Assert.That(history.LastOpenedHistoryItems, Is.EqualTo(new[] { otherQuery }));
}

[Test]
public void Remove_WhenRemovingMatchingResults_RemovesEntriesFromAllQueries()
{
var history = new History();
var selected = CreateHistoryItem("query one");
var sameResultFromAnotherQuery = CreateHistoryItem("query two");
var differentResult = CreateHistoryItem("query three", recordKey: "different");
history.LastOpenedHistoryItems.AddRange([selected, sameResultFromAnotherQuery, differentResult]);

var removedCount = history.Remove(selected, removeAllMatchingResults: true);

Assert.That(removedCount, Is.EqualTo(2));
Assert.That(history.LastOpenedHistoryItems, Is.EqualTo(new[] { differentResult }));
}

private static LastOpenedHistoryResult CreateHistoryItem(string query, string recordKey = "same")
{
return new LastOpenedHistoryResult
{
Title = "Result",
SubTitle = "Subtitle",
PluginID = "Plugin",
RecordKey = recordKey,
Query = query
};
}
}
}
Binary file added Flow.Launcher/Images/delete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions Flow.Launcher/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,8 @@ private void OnKeyDown(object sender, KeyEventArgs e)
e.Handled = true;
break;
case Key.Right:
if (_viewModel.QueryResultsSelected()
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
if ((_viewModel.QueryResultsSelected() || _viewModel.HistorySelected())
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
_viewModel.LoadContextMenuCommand.Execute(null);
e.Handled = true;
Expand Down
25 changes: 24 additions & 1 deletion Flow.Launcher/Storage/QueryHistory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -86,6 +86,29 @@ public void Add(Result result)
}
}

/// <summary>
/// Removes an item from history.
/// </summary>
/// <param name="historyItem">The stored history item to remove.</param>
/// <param name="removeAllMatchingResults">
/// Whether to remove every entry representing the same result. This is used by the last-opened history
/// style, which displays entries with different queries as a single result.
/// </param>
/// <returns>The number of removed history entries.</returns>
public int Remove(LastOpenedHistoryResult historyItem, bool removeAllMatchingResults = false)
{
if (!removeAllMatchingResults)
{
return LastOpenedHistoryItems.Remove(historyItem) ? 1 : 0;
}

return LastOpenedHistoryItems.RemoveAll(item =>
item.Title == historyItem.Title
&& item.SubTitle == historyItem.SubTitle
&& item.PluginID == historyItem.PluginID
&& item.RecordKey == historyItem.RecordKey);
}

/// <summary>
/// Attempts to find an existing <see cref="LastOpenedHistoryResult"/> in <see cref="LastOpenedHistoryItems"/>
/// that is considered equal to the supplied <paramref name="result"/>.
Expand Down
127 changes: 110 additions & 17 deletions Flow.Launcher/ViewModel/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable, IResultUp
private readonly ConcurrentDictionary<Guid, Query> _progressQueryDict = new(); // Used for QueryResultAsync
private Query _updateQuery; // Used for ResultsUpdated
private string _queryTextBeforeLeaveResults;
private string _queryTextBeforeContextMenu;
private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results

private ResultsViewModel _contextMenuSource;
private Result _contextMenuTarget;

private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
private readonly History _history;
private int lastHistoryIndex = 1;
Expand Down Expand Up @@ -413,23 +417,49 @@ private async Task LoadContextMenuAsync()
return;
}

// For query mode, we load context menu
if (QueryResultsSelected())
// If the context menu is already open, we need to return to the previous results view
if (ContextMenuSelected())
{
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
if (SelectedResults.SelectedItem?.Result != null &&
!string.IsNullOrEmpty(SelectedResults.SelectedItem.Result.PluginID)) // Do not show context menu for history results
{
SelectedResults = ContextMenu;
}
await ReturnFromContextMenuAsync();
return;
}
else

// Check if the selected result is a history result or a regular result
// Regular results need a valid plugin ID. History results use Flow's built-in context menu and
// can appear in either the dedicated history view or the home-page result list.
var selected = SelectedResults.SelectedItem?.Result;
if (selected == null) return;
var isHistoryResult = selected is LastOpenedHistoryResult
&& selected.ContextData is LastOpenedHistoryResult;
if (!isHistoryResult && (!QueryResultsSelected() || string.IsNullOrEmpty(selected.PluginID))) return;

_contextMenuSource = SelectedResults;
_contextMenuTarget = selected;
_queryTextBeforeContextMenu = QueryText;

// Load context menu
SelectedResults = ContextMenu;
}

private async Task ReturnFromContextMenuAsync()
{
var source = _contextMenuSource ?? Results;
var queryText = _queryTextBeforeContextMenu;

// Return to the previous results view and restore the query text
SelectedResults = source;
if (source == History)
{
SelectedResults = Results;
PreviewSelectedItem = Results.SelectedItem;
await UpdatePreviewAsync();
await ChangeQueryTextAsync(queryText);
}

// Refresh the preview panel to show the previously selected result
PreviewSelectedItem = source.SelectedItem;
await UpdatePreviewAsync();

_contextMenuSource = null;
_contextMenuTarget = null;
_queryTextBeforeContextMenu = string.Empty;
}

[RelayCommand]
Expand Down Expand Up @@ -638,7 +668,11 @@ private void SelectNextItem()
[RelayCommand]
private async Task EscAsync()
{
if (!QueryResultsSelected())
if (ContextMenuSelected())
{
await ReturnFromContextMenuAsync();
}
else if (!QueryResultsSelected())
{
SelectedResults = Results;
PreviewSelectedItem = Results.SelectedItem;
Expand Down Expand Up @@ -871,7 +905,10 @@ private ResultsViewModel SelectedResults
ContextMenu.Visibility = Visibility.Visible;
History.Visibility = Visibility.Collapsed;
}
_queryTextBeforeLeaveResults = QueryText;
if (isReturningFromQueryResults)
{
_queryTextBeforeLeaveResults = QueryText;
}

// Because of Fody's optimization
// setter won't be called when property value is not changed.
Expand Down Expand Up @@ -1307,7 +1344,17 @@ private void QueryContextMenu()
var query = QueryText.ToLower().Trim();
ContextMenu.Clear();

var selected = Results.SelectedItem?.Result;
var selected = _contextMenuTarget;

if (selected is LastOpenedHistoryResult
&& selected.ContextData is LastOpenedHistoryResult historyItem)
{
ContextMenu.AddResults([
ContextMenuDeleteHistory(historyItem),
ContextMenuHistoryInfo(historyItem)
], id);
return;
}

if (selected != null && // SelectedItem returns null if selection is empty.
!string.IsNullOrEmpty(selected.PluginID)) // SelectedItem must have a valid PluginID, history results do not.
Expand Down Expand Up @@ -1390,6 +1437,7 @@ private List<Result> GetHistoryItems(IEnumerable<LastOpenedHistoryResult> histor
foreach (var item in historyItems)
{
var copiedItem = item.DeepCopyForHistoryStyle(Settings.HistoryStyle == HistoryStyle.LastOpened);
copiedItem.ContextData = item;

if (Settings.HistoryStyle == HistoryStyle.LastOpened)
{
Expand Down Expand Up @@ -1867,6 +1915,51 @@ private Result ContextMenuTopMost(Result result)
return menu;
}

private Result ContextMenuDeleteHistory(LastOpenedHistoryResult historyItem)
{
return new Result
{
Title = Localize.delete(),
IcoPath = Constant.DeleteIcon,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE74D"),
PluginDirectory = Constant.ProgramDirectory,
AsyncAction = async context =>
{
var source = _contextMenuSource;
var removeAllMatchingResults = Settings.HistoryStyle == HistoryStyle.LastOpened;
if (_history.Remove(historyItem, removeAllMatchingResults) > 0)
{
_historyItemsStorage.Save();
}

await ReturnFromContextMenuAsync();

// Home-page history is part of the regular result list, so refresh it after deletion.
// The dedicated history view is refreshed while returning from the context menu.
if (source == Results)
{
await QueryResultsAsync(false, isReQuery: true);
}

return false;
},
OriginQuery = historyItem.OriginQuery
};
}

private Result ContextMenuHistoryInfo(LastOpenedHistoryResult historyItem)
{
return new Result
{
Title = Settings.HistoryStyle == HistoryStyle.Query ? Localize.queryHistory() : Localize.executedHistory(),
IcoPath = Constant.HistoryIcon,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"),
PluginDirectory = Constant.ProgramDirectory,
Action = _ => false,
OriginQuery = historyItem.OriginQuery
};
}

private static Result ContextMenuPluginSettings(Result result)
{
var id = result.PluginID;
Expand Down Expand Up @@ -1929,7 +2022,7 @@ private bool ContextMenuSelected()
return selected;
}

private bool HistorySelected()
internal bool HistorySelected()
{
var selected = SelectedResults == History;
return selected;
Expand Down
Loading