[PTRun]Add history plugin (#19569)

* Progress!

* Progress...

* POC level.

* Added ability to delete from history using IPublicAPI

* Some sorting, works in some cases.

* Rename "Run History" back to just "History".

* Updated item from review.

* Slight change to PowerLauncher ref, set Copy Local = False

* Fixed missing history items if added to history without search term.

* Added placeholder unit test project

* Updates for new History plugin.

* Update Product.wxs, removed useless Unit Test project

* Removed actual files for "Microsoft.PowerToys.Run.Plugin.History.UnitTests"

* Added history.md, updated ESRPSigning_core.json

* Changes for review

* Removed now global CodeAnalysis/stylecop
This commit is contained in:
Jeff Lord
2022-08-23 16:27:45 -04:00
committed by GitHub
parent 8cea22aaf1
commit 4c796c0b53
21 changed files with 940 additions and 64 deletions

View File

@@ -86,6 +86,7 @@
"modules\\launcher\\Plugins\\Folder\\Microsoft.Plugin.Folder.dll",
"modules\\launcher\\Plugins\\Indexer\\Microsoft.Plugin.Indexer.dll",
"modules\\launcher\\Plugins\\OneNote\\Microsoft.PowerToys.Run.Plugin.OneNote.dll",
"modules\\launcher\\Plugins\\History\\Microsoft.PowerToys.Run.Plugin.History.dll",
"modules\\launcher\\Plugins\\Program\\Microsoft.Plugin.Program.dll",
"modules\\launcher\\Plugins\\Registry\\Microsoft.PowerToys.Run.Plugin.Registry.dll",
"modules\\launcher\\Plugins\\WindowsSettings\\Microsoft.PowerToys.Run.Plugin.WindowsSettings.dll",

View File

@@ -415,6 +415,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImageResizerContextMenu", "
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImageResizerLib", "src\modules\imageresizer\ImageResizerLib\ImageResizerLib.vcxproj", "{18B3DB45-4FFE-4D01-97D6-5223FEEE1853}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.History", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.History\Microsoft.PowerToys.Run.Plugin.History.csproj", "{212AD910-8488-4036-BE20-326931B75FB2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -1632,6 +1634,18 @@ Global
{18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x64.Build.0 = Release|x64
{18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x86.ActiveCfg = Release|x64
{18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x86.Build.0 = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{212AD910-8488-4036-BE20-326931B75FB2}.Debug|ARM64.Build.0 = Debug|ARM64
{212AD910-8488-4036-BE20-326931B75FB2}.Debug|x64.ActiveCfg = Debug|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Debug|x64.Build.0 = Debug|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Debug|x86.ActiveCfg = Debug|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Debug|x86.Build.0 = Debug|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|ARM64.ActiveCfg = Release|ARM64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|ARM64.Build.0 = Release|ARM64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x64.ActiveCfg = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x64.Build.0 = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x86.ActiveCfg = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1770,6 +1784,7 @@ Global
{5A1DB2F0-0715-4B3B-98E6-79BC41540045} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{93B72A06-C8BD-484F-A6F7-C9F280B150BF} = {6C7F47CC-2151-44A3-A546-41C70025132C}
{18B3DB45-4FFE-4D01-97D6-5223FEEE1853} = {6C7F47CC-2151-44A3-A546-41C70025132C}
{212AD910-8488-4036-BE20-326931B75FB2} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,103 @@
# History Plugin
The History Plugin allows users to search or display results they have used (selected).
## How it works
The plugin uses data that was already being captured which is, what results were clicked, and how many times. We do add a little more data to this set now.
When this plugin is queried, it creates results based on this previously selected results data.
In order to make sure selected results in the history are still valid, we re-query the plugin the relevant plug using the PluginManager. If there are no results,
this history item is not included. This usually means that the result is no longer valid. For instance, if a file was deleted, but it's still in the selected history
we don't want to show it as a selectable result.
Because the results from the History Plugin are actually created from calls to the PluginManager, they will be exactly the same is if they did not come from the History Plugin.
## Special notes
While the results returned from the plugin are from the PluginManager, they are sometimes modified before returning. One example is the Calculator plugin.
Since the Calculator plugin operates on the current query input by the user, the results from Calculator plugin don't include that in the title. However, as a history item,
the query is very important. In this case, and maybe others in the future, we modify the tile to also include the search.
### Modified title example:
This is what the Calculator plugin normally might show:
![image](https://user-images.githubusercontent.com/4396667/184661303-4f8cf0da-2956-46b9-bdc1-ed879cd0b7cc.png)
But this is how it will look returned from the History plugin
![image](https://user-images.githubusercontent.com/4396667/184661450-9ec3c416-66df-40c8-b004-da8b0cebc5c5.png)
As you can see, here and maybe other places, other non-history plugin might be able to include extra data for the History plugin to use later.
For example, in future, plugins might be able to also set a "History Title", "History Icon", etc... But for now, it's not needed.
## Duplicates from the History Plugin in global results
If the History plugin is set to show in the global results, it might return a result that is also returned from another plugin. If a match is found,
the result from the history plugin is discarded.
## Removing items from history
A new context menu item is added to each History result, which can be used to delete it from the history.
![image](https://user-images.githubusercontent.com/4396667/184656195-6d9f1a49-652c-4027-a424-535e9fb1f2a8.png)
## Context menus
Because these results are coming from the History plugin, this plugin must invoke each menu items `LoadContextMenus` method.
We then also add the "Remove this from history" context menu action.
## Results score
When the plugin is used with the activation command, the scores are configured so the results show with the more recently selected items at the top.
If the history results are shown in the global results, the scores are not modified from that the original plugin set.
## Old Data
Items selected before this plugin was created will not show in the history because they don't contain enough data.
## Important for developers
### Important plugin values (meta-data)
| Name | Value |
| --------------- | ---------------------------------------------------- |
| ActionKeyword | `!!` |
| ExecuteFileName | `Microsoft.PowerToys.Run.Plugin.History.dll` |
| ID | `C88512156BB74580AADF7252E130BA8D` |
### Interfaces used by this plugin
The plugin uses only these interfaces (all inside the `Main.cs`):
* `Wox.Plugin.IPlugin`
* `Wox.Plugin.IContextMenu`
* `Wox.Plugin.IPluginI18n`
### Program files
| File | Content |
| ------------------------------------- | ----------------------------------------------------------------------- |
| `Images\history.dark.png` | Symbol for the results for the dark theme |
| `Images\history.light.png` | Symbol for the results for the light theme |
| `Properties\Resources.Designer.resx` | File that contain all translatable keys |
| `Properties\Resources.resx` | File that contains all translatable strings in the neutral language |
| `Main.cs` | Main class, the only place that implements the WOX interfaces |
| `ErrorHandler.cs` | Class to build error result on plugin failure |
| `plugin.json` | All meta-data for this plugin |
### Important project values (*.csproj)
| Name | Value |
| --------------- | ------------------------------------------------- |
| TargetFramework | `net6.0-windows10.0.19041.0` |
### Project dependencies
#### Projects
* `Wox.Infrastructure`
* `Wox.Plugin`
* `PowerToys.PowerLauncher`
#### Build Dependency
Access to PluginManager was needed to make this plugin work. Because of this a reference to PowerToys.PowerLauncher was needed.
Since History Plugin needs a reference to PowerToys.PowerLauncher, it can not be set as a dependency reference in PowerToys.PowerLauncher project (else a circular reference would exist).
This means that if you build PowerToys.PowerLauncher only it will not build History Plugin. You will need to manually build History Plugin at least once and again manually if you change it.
### Caching
Right now, there is no caching. But since this plugin does cause more queries than expected to many plugins, the `BuildResult` method is likely to be improved with some level of caching.

View File

@@ -70,6 +70,8 @@
<?define ShellComponentFiles=plugin.json;Microsoft.Plugin.Shell.deps.json;Microsoft.Plugin.Shell.dll;PowerToys.ManagedTelemetry.dll?>
<?define HistoryPluginComponentFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.History.deps.json;Microsoft.PowerToys.Run.Plugin.History.dll?>
<?define ShellImagesComponentFiles=shell.light.png;shell.dark.png;user.light.png;user.dark.png?>
<?define IndexerComponentFiles=Microsoft.Plugin.Indexer.deps.json;Microsoft.Plugin.Indexer.dll;plugin.json;PowerToys.ManagedTelemetry.dll;Ijwhost.dll?>
@@ -487,6 +489,10 @@
<Directory Id="UriImagesFolder" Name="Images" />
<Directory Id="UriLanguagesFolder" Name="Languages" />
</Directory>
<Directory Id="HistoryPluginFolder" Name="History">
<Directory Id="HistoryImagesFolder" Name="Images" />
<Directory Id="HistoryLanguagesFolder" Name="Languages" />
</Directory>
<Directory Id="UnitConverterPluginFolder" Name="UnitConverter">
<Directory Id="UnitConverterImagesFolder" Name="Images" />
<Directory Id="UnitConverterLanguagesFolder" Name="Languages" />
@@ -1105,7 +1111,7 @@
<Fragment>
<!-- Resource directories should be added only if the installer is built on the build farm -->
<?ifdef env.IsPipeline?>
<?foreach ParentDirectory in LauncherInstallFolder;FancyZonesInstallFolder;ImageResizerInstallFolder;ColorPickerInstallFolder;FileExplorerPreviewInstallFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;TimeZonePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder?>
<?foreach ParentDirectory in LauncherInstallFolder;FancyZonesInstallFolder;ImageResizerInstallFolder;ColorPickerInstallFolder;FileExplorerPreviewInstallFolder;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;TimeZonePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder?>
<DirectoryRef Id="$(var.ParentDirectory)">
<!-- Resource file directories -->
<?foreach Language in $(var.LocLanguageList)?>
@@ -1397,6 +1403,12 @@
Guid="$(var.CompGUIDPrefix)1A">
<File Id="MonacoPreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)modules\FileExplorerPreview\$(var.Language)\PowerToys.MonacoPreviewHandler.resources.dll" />
</Component>
<Component
Id="Launcher_History_$(var.IdSafeLanguage)_Component"
Directory="Resource$(var.IdSafeLanguage)HistoryPluginFolder"
Guid="$(var.CompGUIDPrefix)1B">
<File Id="Launcher_History_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)modules\launcher\Plugins\History\$(var.Language)\Microsoft.PowerToys.Run.Plugin.History.resources.dll" />
</Component>
<?undef IdSafeLanguage?>
<?undef CompGUIDPrefix?>
<?endforeach?>
@@ -1836,6 +1848,19 @@
<File Id="WebSearchDark" Source="$(var.BinDir)modules\launcher\Plugins\WebSearch\Images\WebSearch.dark.png" />
</Component>
<!-- History Plugin -->
<?foreach File in $(var.HistoryPluginComponentFiles)?>
<Component Id="HistoryComponent_$(var.File)" Win64="yes" Directory="HistoryPluginFolder">
<File Id="HistoryComponentFile_$(var.File)" Source="$(var.BinDir)modules\launcher\Plugins\History\$(var.File)" />
</Component>
<?endforeach?>
<Component Id="HistoryImagesComponentLight" Directory="HistoryImagesFolder" >
<File Id="HistoryLightIcon" Source="$(var.BinDir)modules\launcher\Plugins\History\Images\history.light.png" />
</Component>
<Component Id="HistoryImagesComponentDark" Directory="HistoryImagesFolder" >
<File Id="HistoryDarkIcon" Source="$(var.BinDir)modules\launcher\Plugins\History\Images\history.dark.png" />
</Component>
<!-- Uri Plugin -->
<?foreach File in $(var.UriComponentFiles)?>
<Component Id="UriComponent_$(var.File)" Win64="yes" Directory="UriPluginFolder">

View File

@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using Wox.Plugin;
using Wox.Plugin.Logger;
namespace Microsoft.PowerToys.Run.Plugin.History
{
internal static class ErrorHandler
{
/// <summary>
/// Method to handles errors
/// </summary>
/// <param name="icon">Path to result icon.</param>
/// <param name="isGlobalQuery">Bool to indicate if it is a global query.</param>
/// <param name="queryInput">User input as string including the action keyword.</param>
/// <param name="errorMessage">Error message if applicable.</param>
/// <param name="exception">Exception if applicable.</param>
/// <returns>List of results to show. Either an error message or an empty list.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="errorMessage"/> and <paramref name="exception"/> are both filled with their default values.</exception>
internal static List<Result> OnError(string icon, bool isGlobalQuery, string queryInput, string errorMessage, Exception exception = default)
{
string userMessage;
if (errorMessage != default)
{
Log.Error($"Failed to handle history item <{queryInput}>: {errorMessage}", typeof(History.Main));
userMessage = errorMessage;
}
else if (exception != default)
{
Log.Exception($"Exception when query for <{queryInput}>", exception, exception.GetType());
userMessage = exception.Message;
}
else
{
throw new ArgumentException("The arguments error and exception have default values. One of them has to be filled with valid error data (error message/exception)!");
}
return isGlobalQuery ? new List<Result>() : new List<Result> { CreateErrorResult(userMessage, icon) };
}
private static Result CreateErrorResult(string errorMessage, string iconPath)
{
return new Result
{
Title = Properties.Resources.wox_plugin_history_processing_failed,
SubTitle = errorMessage,
IcoPath = iconPath,
Score = 300,
};
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,251 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using ManagedCommon;
using Microsoft.PowerToys.Run.Plugin.History.Properties;
using PowerLauncher.Plugin;
using Wox.Plugin;
namespace Microsoft.PowerToys.Run.Plugin.History
{
public class Main : IPlugin, IContextMenu, IPluginI18n, IDisposable
{
private PluginInitContext Context { get; set; }
private string IconPath { get; set; }
public string Name => Resources.wox_plugin_history_plugin_name;
public string Description => Resources.wox_plugin_history_plugin_description;
private bool _disposed;
public List<Result> Query(Query query)
{
var results = new List<Result>();
try
{
if (query.SelectedItems != null)
{
var scoreCounter = 1000;
// System.Diagnostics.Debugger.Launch();
foreach (var historyItem in query.SelectedItems.Values.OrderByDescending(sel => sel.LastSelected))
{
if (historyItem.PluginID == null)
{
continue;
}
var plugin = PluginManager.AllPlugins.FirstOrDefault(p => p.Metadata.ID == historyItem.PluginID);
if (query.Search != string.Empty && !IsRelevant(query, historyItem))
{
continue;
}
var result = BuildResult(historyItem);
if (result != null)
{
// very special case for Calculator
if (plugin.Metadata.Name == "Calculator")
{
result.HistoryTitle = result.Title;
result.Title = $"{historyItem.Search} = {historyItem.Title}";
}
if (query.RawQuery.StartsWith(query.ActionKeyword, StringComparison.InvariantCultureIgnoreCase))
{
// this is just the history view, update the scores.
result.Score = scoreCounter--;
}
results.Add(result);
}
else
{
System.Diagnostics.Debug.WriteLine("Skipping " + historyItem.Title);
}
}
return results;
}
}
catch (Exception e)
{
// System.Diagnostics.Debugger.Launch();
bool isGlobalQuery = string.IsNullOrEmpty(query.ActionKeyword);
return ErrorHandler.OnError(IconPath, isGlobalQuery, query.RawQuery, default, e);
}
return results;
}
private bool IsRelevant(Query query, UserSelectedRecord.UserSelectedRecordItem genericSelectedItem)
{
if (genericSelectedItem.Title != null && genericSelectedItem.Title.Contains(query.Search, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
else if (genericSelectedItem.SubTitle != null && genericSelectedItem.SubTitle.Contains(query.Search, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
else if (genericSelectedItem.Search != null && genericSelectedItem.Search.Contains(query.Search, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
return false;
}
private Result BuildResult(UserSelectedRecord.UserSelectedRecordItem historyItem)
{
Result result = null;
var plugin = PluginManager.AllPlugins.FirstOrDefault(x => x.Metadata.ID == historyItem.PluginID);
var searchTerm = historyItem.Search;
if (string.IsNullOrEmpty(searchTerm))
{
searchTerm = historyItem.Title;
}
var tempResults = PluginManager.QueryForPlugin(plugin, new Query(searchTerm), false);
if (tempResults != null)
{
result = tempResults.FirstOrDefault(r => r.Title == historyItem.Title && r.SubTitle == historyItem.SubTitle);
if (result == null)
{
// do less exact match, some plugins (like shell), have a dynamic SubTitle
result = tempResults.FirstOrDefault(r => r.Title == historyItem.Title);
}
}
if (result == null)
{
tempResults = PluginManager.QueryForPlugin(plugin, new Query(searchTerm), true);
if (tempResults != null)
{
result = tempResults.FirstOrDefault(r => r.Title == historyItem.Title && r.SubTitle == historyItem.SubTitle);
if (result == null)
{
// do less exact match, some plugins (like shell), have a dynamic SubTitle
result = tempResults.FirstOrDefault(r => r.Title == historyItem.Title);
}
}
}
if (result != null)
{
result.FromHistory = true;
result.HistoryPluginID = historyItem.PluginID;
}
return result;
}
public void Init(PluginInitContext context)
{
Context = context ?? throw new ArgumentNullException(paramName: nameof(context));
Context.API.ThemeChanged += OnThemeChanged;
UpdateIconPath(Context.API.GetCurrentTheme());
}
private void UpdateIconPath(Theme theme)
{
if (theme == Theme.Light || theme == Theme.HighContrastWhite)
{
IconPath = "Images/history.light.png";
}
else
{
IconPath = "Images/history.dark.png";
}
}
private void OnThemeChanged(Theme currentTheme, Theme newTheme)
{
UpdateIconPath(newTheme);
}
public string GetTranslatedPluginTitle()
{
return Resources.wox_plugin_history_plugin_name;
}
public string GetTranslatedPluginDescription()
{
return Resources.wox_plugin_history_plugin_description;
}
public List<ContextMenuResult> LoadContextMenus(Result selectedResult)
{
var pluginPair = PluginManager.AllPlugins.FirstOrDefault(x => x.Metadata.ID == selectedResult.HistoryPluginID);
if (pluginPair != null)
{
List<ContextMenuResult> menuItems = new List<ContextMenuResult>();
if (pluginPair.Plugin.GetType().GetInterface(nameof(IContextMenu)) != null)
{
var plugin = (IContextMenu)pluginPair.Plugin;
menuItems = plugin.LoadContextMenus(selectedResult);
}
menuItems.Add(new ContextMenuResult
{
// https://docs.microsoft.com/en-us/windows/apps/design/style/segoe-ui-symbol-font
FontFamily = "Segoe MDL2 Assets",
Glyph = "\xF739", // ECC9 => Symbol: RemoveFrom, or F739 => SetHistoryStatus2
Title = $"Remove this from history",
Action = _ =>
{
// very special case for Calculator
if (pluginPair.Plugin.Name == "Calculator")
{
selectedResult.Title = selectedResult.HistoryTitle;
}
PluginManager.API.RemoveUserSelectedItem(selectedResult);
return true;
},
});
return menuItems;
}
return new List<ContextMenuResult>();
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (Context != null && Context.API != null)
{
Context.API.ThemeChanged -= OnThemeChanged;
}
_disposed = true;
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
<ProjectGuid>{212AD910-8488-4036-BE20-326931B75FB2}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Microsoft.PowerToys.Run.Plugin.History</RootNamespace>
<AssemblyName>Microsoft.PowerToys.Run.Plugin.History</AssemblyName>
<Version>$(Version).0</Version>
<useWPF>true</useWPF>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<OutputPath>$(SolutionDir)\$(Platform)\$(Configuration)\modules\launcher\Plugins\History\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DebugSymbols>true</DebugSymbols>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<ItemGroup>
<None Include="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\PowerLauncher\PowerLauncher.csproj">
<Private>False</Private>
<CopyLocalSatelliteAssemblies>False</CopyLocalSatelliteAssemblies>
</ProjectReference>
<ProjectReference Include="..\..\Wox.Infrastructure\Wox.Infrastructure.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\..\Wox.Plugin\Wox.Plugin.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" />
<PackageReference Include="Mages" Version="2.0.1" />
<PackageReference Include="System.Runtime" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<None Update="Images\history.dark.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Images\history.light.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.PowerToys.Run.Plugin.History.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PowerToys.Run.Plugin.History.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Quick access to previously selected results..
/// </summary>
public static string wox_plugin_history_plugin_description {
get {
return ResourceManager.GetString("wox_plugin_history_plugin_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to History.
/// </summary>
public static string wox_plugin_history_plugin_name {
get {
return ResourceManager.GetString("wox_plugin_history_plugin_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to process the input.
/// </summary>
public static string wox_plugin_history_processing_failed {
get {
return ResourceManager.GetString("wox_plugin_history_processing_failed", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="wox_plugin_history_plugin_name" xml:space="preserve">
<value>History</value>
</data>
<data name="wox_plugin_history_plugin_description" xml:space="preserve">
<value>Quick access to previously selected results.</value>
</data>
<data name="wox_plugin_history_processing_failed" xml:space="preserve">
<value>Failed to process the input</value>
</data>
</root>

View File

@@ -0,0 +1,13 @@
{
"ID": "C88512156BB74580AADF7252E130BA8D",
"ActionKeyword": "!!",
"IsGlobal": false,
"Name": "History",
"Author": "jefflord",
"Version": "1.0.0",
"Language": "csharp",
"Website": "https://aka.ms/powertoys",
"ExecuteFileName": "Microsoft.PowerToys.Run.Plugin.History.dll",
"IcoPathDark": "Images\\history.dark.png",
"IcoPathLight": "Images\\history.light.png"
}

View File

@@ -38,6 +38,12 @@ namespace Wox
};
}
public void RemoveUserSelectedItem(Result result)
{
_mainVM.RemoveUserSelectedRecord(result);
_mainVM.ChangeQueryText(_mainVM.QueryText, true);
}
public void ChangeQuery(string query, bool requery = false)
{
_mainVM.ChangeQueryText(query, requery);

View File

@@ -1,58 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Wox.Plugin;
namespace PowerLauncher.Storage
{
public class UserSelectedRecord
{
public class UserSelectedRecordItem
{
public int SelectedCount { get; set; }
public DateTime LastSelected { get; set; }
}
[JsonInclude]
public Dictionary<string, UserSelectedRecordItem> Records { get; private set; } = new Dictionary<string, UserSelectedRecordItem>();
public void Add(Result result)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
var key = result.ToString();
if (Records.TryGetValue(key, out var value))
{
Records[key].SelectedCount = Records[key].SelectedCount + 1;
Records[key].LastSelected = DateTime.UtcNow;
}
else
{
Records.Add(key, new UserSelectedRecordItem { SelectedCount = 1, LastSelected = DateTime.UtcNow });
}
}
public UserSelectedRecordItem GetSelectedData(Result result)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (result != null && Records.TryGetValue(result.ToString(), out var value))
{
return value;
}
return new UserSelectedRecordItem { SelectedCount = 0, LastSelected = DateTime.MinValue };
}
}
}

View File

@@ -82,6 +82,11 @@ namespace PowerLauncher.ViewModel
RegisterResultsUpdatedEvent();
}
public void RemoveUserSelectedRecord(Result result)
{
_userSelectedRecord.Remove(result);
}
public void RegisterHotkey(IntPtr hwnd)
{
Log.Info("RegisterHotkey()", GetType());
@@ -566,6 +571,7 @@ namespace PowerLauncher.ViewModel
{
var plugin = pluginQueryItem.Key;
var query = pluginQueryItem.Value;
query.SelectedItems = _userSelectedRecord.GetGenericHistory();
var results = PluginManager.QueryForPlugin(plugin, query);
resultPluginPair[plugin.Metadata] = results;
currentCancellationToken.ThrowIfCancellationRequested();
@@ -586,6 +592,7 @@ namespace PowerLauncher.ViewModel
{
var plugin = pluginQueryItem.Key;
var query = pluginQueryItem.Value;
query.SelectedItems = _userSelectedRecord.GetGenericHistory();
var results = PluginManager.QueryForPlugin(plugin, query);
resultPluginPair[plugin.Metadata] = results;
currentCancellationToken.ThrowIfCancellationRequested();

View File

@@ -151,11 +151,10 @@ namespace PowerLauncher.ViewModel
{
bool hideWindow =
r.Action != null &&
r.Action(
new ActionContext
{
SpecialKeyState = KeyboardHelper.CheckModifiers(),
});
r.Action(new ActionContext
{
SpecialKeyState = KeyboardHelper.CheckModifiers(),
});
if (hideWindow)
{

View File

@@ -266,6 +266,16 @@ namespace PowerLauncher.ViewModel
sorted = Results.OrderByDescending(x => (x.Result.Metadata.WeightBoost + x.Result.Score + (x.Result.SelectedCount * 5))).ToList();
}
// remove history items in they are in the list as non-history items
foreach (var nonHistoryResult in sorted.Where(x => x.Result.Metadata.Name != "History").ToList())
{
var historyToRemove = sorted.FirstOrDefault(x => x.Result.Metadata.Name == "History" && x.Result.Title == nonHistoryResult.Result.Title && x.Result.SubTitle == nonHistoryResult.Result.SubTitle);
if (historyToRemove != null)
{
sorted.Remove(historyToRemove);
}
}
Clear();
Results.AddRange(sorted);
}

View File

@@ -28,6 +28,11 @@ namespace Wox.Plugin
/// </summary>
void RestartApp();
/// <summary>
/// Remove user selected history item and refresh/requery
/// </summary>
void RemoveUserSelectedItem(Result result);
/// <summary>
/// Get current theme
/// </summary>

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Mono.Collections.Generic;
@@ -149,6 +150,8 @@ namespace Wox.Plugin
public override string ToString() => RawQuery;
public Dictionary<string, UserSelectedRecord.UserSelectedRecordItem> SelectedItems { get; set; }
[Obsolete("Use Search instead, this method will be removed in v1.3.0")]
public string GetAllRemainingParameter() => Search;
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO.Abstractions;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Media;
@@ -42,6 +43,12 @@ namespace Wox.Plugin
}
}
public bool FromHistory { get; set; }
public string HistoryPluginID { get; set; }
public string HistoryTitle { get; set; }
public string SubTitle { get; set; }
public string Glyph { get; set; }
@@ -98,6 +105,7 @@ namespace Wox.Plugin
/// <summary>
/// Gets or sets return true to hide wox after select result
/// </summary>
[JsonIgnore]
public Func<ActionContext, bool> Action { get; set; }
public int Score { get; set; }

View File

@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Wox.Plugin
{
public class UserSelectedRecord
{
public class UserSelectedRecordItem
{
public int SelectedCount { get; set; }
public DateTime LastSelected { get; set; }
public string IconPath { get; set; }
public string Title { get; set; }
public string Search { get; set; }
public int Score { get; set; }
public string SubTitle { get; set; }
public string PluginID { get; set; }
}
[JsonInclude]
public Dictionary<string, UserSelectedRecordItem> Records { get; private set; } = new Dictionary<string, UserSelectedRecordItem>();
public void Remove(Result result)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
var key = result.ToString();
if (Records.ContainsKey(result.ToString()))
{
Records.Remove(result.ToString());
}
}
public void Add(Result result)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
var key = result.ToString();
if (Records.TryGetValue(key, out var value))
{
Records[key].SelectedCount = Records[key].SelectedCount + 1;
Records[key].LastSelected = DateTime.UtcNow;
Records[key].IconPath = result.IcoPath;
Records[key].Title = result.Title;
Records[key].SubTitle = result.SubTitle;
Records[key].Search = (result.OriginQuery.Search.Length > 0) ? result.OriginQuery.Search : Records[key].Search;
if (Records[key].PluginID == null)
{
Records[key].PluginID = result.PluginID;
}
}
else
{
Records.Add(key, new UserSelectedRecordItem
{
SelectedCount = 1,
LastSelected = DateTime.UtcNow,
Title = result.Title,
SubTitle = result.SubTitle,
IconPath = result.IcoPath,
PluginID = result.PluginID,
Search = result.OriginQuery.Search,
});
}
}
public UserSelectedRecordItem GetSelectedData(Result result)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (result != null && Records.TryGetValue(result.ToString(), out var value))
{
return value;
}
return new UserSelectedRecordItem { SelectedCount = 0, LastSelected = DateTime.MinValue };
}
public Dictionary<string, UserSelectedRecordItem> GetGenericHistory()
{
/*
var history = new List<UserSelectedRecord.UserSelectedRecordItem>();
foreach (var record in Records)
{
if (record.Value.PluginID == null)
{
continue;
}
history.Add(new UserSelectedRecordItem
{
SelectedCount = record.Value.SelectedCount,
LastSelected = record.Value.LastSelected,
IconPath = record.Value.IconPath,
Title = record.Value.Title,
Score = record.Value.Score,
SubTitle = record.Value.SubTitle,
PluginID = record.Value.PluginID,
Search = record.Value.Search,
});
}
return history;
*/
return Records;
}
}
}