mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge remote-tracking branch 'origin/dev' into quicklook
This commit is contained in:
commit
f902eaed0d
430 changed files with 22935 additions and 14245 deletions
|
|
@ -58,7 +58,7 @@ dotnet_style_prefer_conditional_expression_over_return = true:silent
|
|||
###############################
|
||||
# Style Definitions
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
# Use PascalCase for constant fields
|
||||
# Use PascalCase for constant fields
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
|
||||
|
|
@ -134,7 +134,7 @@ csharp_preserve_single_line_statements = true
|
|||
csharp_preserve_single_line_blocks = true
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
|
|
|
|||
3
.github/actions/spelling/allow.txt
vendored
3
.github/actions/spelling/allow.txt
vendored
|
|
@ -3,3 +3,6 @@ https
|
|||
ssh
|
||||
ubuntu
|
||||
runcount
|
||||
Firefox
|
||||
Português
|
||||
Português (Brasil)
|
||||
|
|
|
|||
10
.github/actions/spelling/expect.txt
vendored
10
.github/actions/spelling/expect.txt
vendored
|
|
@ -1,7 +1,6 @@
|
|||
crowdin
|
||||
DWM
|
||||
workflows
|
||||
Wpf
|
||||
wpf
|
||||
actionkeyword
|
||||
stackoverflow
|
||||
|
|
@ -20,9 +19,7 @@ Prioritise
|
|||
Segoe
|
||||
Google
|
||||
Customise
|
||||
UWP
|
||||
uwp
|
||||
Uwp
|
||||
Bokmal
|
||||
Bokm
|
||||
uninstallation
|
||||
|
|
@ -61,7 +58,6 @@ popup
|
|||
ptr
|
||||
pluginindicator
|
||||
TobiasSekan
|
||||
Img
|
||||
img
|
||||
resx
|
||||
bak
|
||||
|
|
@ -87,7 +83,6 @@ txb
|
|||
btn
|
||||
otf
|
||||
searchplugin
|
||||
Noresult
|
||||
wpftk
|
||||
mkv
|
||||
flac
|
||||
|
|
@ -104,9 +99,14 @@ Português (Brasil)
|
|||
Italiano
|
||||
Slovenský
|
||||
quicklook
|
||||
Tiếng Việt
|
||||
Droplex
|
||||
Preinstalled
|
||||
errormetadatafile
|
||||
noresult
|
||||
pluginsmanager
|
||||
alreadyexists
|
||||
JsonRPC
|
||||
JsonRPCV2
|
||||
Softpedia
|
||||
img
|
||||
|
|
|
|||
3
.github/actions/spelling/patterns.txt
vendored
3
.github/actions/spelling/patterns.txt
vendored
|
|
@ -118,3 +118,6 @@
|
|||
|
||||
# UWP
|
||||
[Uu][Ww][Pp]
|
||||
|
||||
# version suffix <word>v#
|
||||
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
|
||||
|
|
|
|||
28
.github/workflows/default_plugins.yml
vendored
28
.github/workflows/default_plugins.yml
vendored
|
|
@ -13,12 +13,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Determine New Plugin Updates
|
||||
uses: dorny/paths-filter@v2
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
|
||||
- name: Publish BrowserBookmark
|
||||
if: steps.changes.outputs.browserbookmark == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.BrowserBookmark"
|
||||
files: "Flow.Launcher.Plugin.BrowserBookmark.zip"
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
|
||||
- name: Publish Calculator
|
||||
if: steps.changes.outputs.calculator == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Calculator"
|
||||
files: "Flow.Launcher.Plugin.Calculator.zip"
|
||||
|
|
@ -119,7 +119,7 @@ jobs:
|
|||
|
||||
- name: Publish Explorer
|
||||
if: steps.changes.outputs.explorer == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Explorer"
|
||||
files: "Flow.Launcher.Plugin.Explorer.zip"
|
||||
|
|
@ -146,7 +146,7 @@ jobs:
|
|||
|
||||
- name: Publish PluginIndicator
|
||||
if: steps.changes.outputs.pluginindicator == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginIndicator"
|
||||
files: "Flow.Launcher.Plugin.PluginIndicator.zip"
|
||||
|
|
@ -173,7 +173,7 @@ jobs:
|
|||
|
||||
- name: Publish PluginsManager
|
||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginsManager"
|
||||
files: "Flow.Launcher.Plugin.PluginsManager.zip"
|
||||
|
|
@ -200,7 +200,7 @@ jobs:
|
|||
|
||||
- name: Publish ProcessKiller
|
||||
if: steps.changes.outputs.processkiller == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller"
|
||||
files: "Flow.Launcher.Plugin.ProcessKiller.zip"
|
||||
|
|
@ -227,7 +227,7 @@ jobs:
|
|||
|
||||
- name: Publish Program
|
||||
if: steps.changes.outputs.program == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Program"
|
||||
files: "Flow.Launcher.Plugin.Program.zip"
|
||||
|
|
@ -254,7 +254,7 @@ jobs:
|
|||
|
||||
- name: Publish Shell
|
||||
if: steps.changes.outputs.shell == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Shell"
|
||||
files: "Flow.Launcher.Plugin.Shell.zip"
|
||||
|
|
@ -281,7 +281,7 @@ jobs:
|
|||
|
||||
- name: Publish Sys
|
||||
if: steps.changes.outputs.sys == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Sys"
|
||||
files: "Flow.Launcher.Plugin.Sys.zip"
|
||||
|
|
@ -308,7 +308,7 @@ jobs:
|
|||
|
||||
- name: Publish Url
|
||||
if: steps.changes.outputs.url == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Url"
|
||||
files: "Flow.Launcher.Plugin.Url.zip"
|
||||
|
|
@ -335,7 +335,7 @@ jobs:
|
|||
|
||||
- name: Publish WebSearch
|
||||
if: steps.changes.outputs.websearch == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WebSearch"
|
||||
files: "Flow.Launcher.Plugin.WebSearch.zip"
|
||||
|
|
@ -362,7 +362,7 @@ jobs:
|
|||
|
||||
- name: Publish WindowsSettings
|
||||
if: steps.changes.outputs.windowssettings == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WindowsSettings"
|
||||
files: "Flow.Launcher.Plugin.WindowsSettings.zip"
|
||||
|
|
|
|||
7
.github/workflows/spelling.yml
vendored
7
.github/workflows/spelling.yml
vendored
|
|
@ -73,7 +73,7 @@ jobs:
|
|||
steps:
|
||||
- name: check-spelling
|
||||
id: spelling
|
||||
uses: check-spelling/check-spelling@v0.0.22
|
||||
uses: check-spelling/check-spelling@prerelease
|
||||
with:
|
||||
suppress_push_for_open_pull_request: 1
|
||||
checkout: true
|
||||
|
|
@ -91,10 +91,9 @@ jobs:
|
|||
extra_dictionaries:
|
||||
cspell:software-terms/dict/softwareTerms.txt
|
||||
cspell:win32/src/win32.txt
|
||||
cspell:php/src/php.txt
|
||||
cspell:filetypes/filetypes.txt
|
||||
cspell:csharp/csharp.txt
|
||||
cspell:dotnet/src/dotnet.txt
|
||||
cspell:dotnet/dict/dotnet.txt
|
||||
cspell:python/src/common/extra.txt
|
||||
cspell:python/src/python/python-lib.txt
|
||||
cspell:aws/aws.txt
|
||||
|
|
@ -130,7 +129,7 @@ jobs:
|
|||
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
|
||||
steps:
|
||||
- name: comment
|
||||
uses: check-spelling/check-spelling@v0.0.22
|
||||
uses: check-spelling/check-spelling@prerelease
|
||||
with:
|
||||
checkout: true
|
||||
spell_check_this: check-spelling/spell-check-this@main
|
||||
|
|
|
|||
15
.github/workflows/stale.yml
vendored
15
.github/workflows/stale.yml
vendored
|
|
@ -6,6 +6,11 @@ on:
|
|||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
env:
|
||||
days-before-stale: 60
|
||||
days-before-close: 7
|
||||
exempt-issue-labels: 'keep-fresh'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -13,14 +18,14 @@ jobs:
|
|||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
days-before-stale: 45
|
||||
days-before-close: 7
|
||||
stale-issue-message: 'This issue is stale because it has been open ${{ env.days-before-stale }} days with no activity. Remove stale label or comment or this will be closed in ${{ env.days-before-stale }} days.\n\nAlternatively this issue can be kept open by adding one of the following labels:\n${{ env.exempt-issue-labels }}'
|
||||
days-before-stale: ${{ env.days-before-stale }}
|
||||
days-before-close: ${{ env.days-before-close }}
|
||||
days-before-pr-close: -1
|
||||
exempt-all-milestones: true
|
||||
close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity. If you feel this issue still needs attention please feel free to reopen.'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-issue-labels: 'keep-fresh'
|
||||
exempt-issue-labels: ${{ env.exempt-issue-labels }}
|
||||
exempt-pr-labels: 'keep-fresh,awaiting-approval,work-in-progress'
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using System.Collections.Generic;
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -16,6 +18,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
|
||||
private List<UserPlugin> plugins = new();
|
||||
|
||||
private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fetch and deserialize the contents of a plugins.json file found at <see cref="ManifestFileUrl"/>.
|
||||
/// We use conditional http requests to keep repeat requests fast.
|
||||
|
|
@ -32,12 +39,15 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
|
||||
request.Headers.Add("If-None-Match", latestEtag);
|
||||
|
||||
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
||||
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
this.plugins = await response.Content.ReadFromJsonAsync<List<UserPlugin>>(cancellationToken: token).ConfigureAwait(false);
|
||||
this.latestEtag = response.Headers.ETag.Tag;
|
||||
this.plugins = await response.Content
|
||||
.ReadFromJsonAsync<List<UserPlugin>>(PluginStoreItemSerializationOption, cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
this.latestEtag = response.Headers.ETag?.Tag;
|
||||
|
||||
Log.Info(nameof(CommunityPluginSource), $"Loaded {this.plugins.Count} plugins from {ManifestFileUrl}");
|
||||
return this.plugins;
|
||||
|
|
@ -49,7 +59,8 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
}
|
||||
else
|
||||
{
|
||||
Log.Warn(nameof(CommunityPluginSource), $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||
Log.Warn(nameof(CommunityPluginSource),
|
||||
$"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||
throw new Exception($"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
|
||||
{
|
||||
var msg = $"Please select the {EnvName} executable";
|
||||
var selectedFile = string.Empty;
|
||||
string selectedFile;
|
||||
|
||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
|
||||
|
|
@ -131,14 +131,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
};
|
||||
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == DialogResult.OK)
|
||||
{
|
||||
return dlg.FileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
|
||||
internal class JavaScriptV2Environment : TypeScriptV2Environment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.JavaScriptV2;
|
||||
|
||||
internal JavaScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
internal class PythonV2Environment : PythonEnvironment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.PythonV2;
|
||||
|
||||
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||
{
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new PythonPluginV2(filePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
|
||||
internal PythonV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.IO;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
internal class TypeScriptV2Environment : AbstractPluginEnvironment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.TypeScriptV2;
|
||||
|
||||
internal override string EnvName => DataLocation.NodeEnvironmentName;
|
||||
|
||||
internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);
|
||||
|
||||
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
|
||||
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
|
||||
|
||||
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; }
|
||||
|
||||
internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
|
||||
internal override void InstallEnvironment()
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
||||
|
||||
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
||||
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
}
|
||||
|
||||
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||
{
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new NodePluginV2(filePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
@ -13,9 +13,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
public string Website { get; set; }
|
||||
public string UrlDownload { get; set; }
|
||||
public string UrlSourceCode { get; set; }
|
||||
public string LocalInstallPath { get; set; }
|
||||
public string IcoPath { get; set; }
|
||||
public DateTime LatestReleaseDate { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
public DateTime? LatestReleaseDate { get; set; }
|
||||
public DateTime? DateAdded { get; set; }
|
||||
|
||||
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,9 +54,11 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||
<PackageReference Include="FSharp.Core" Version="7.0.400" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageReference Include="FSharp.Core" Version="7.0.401" />
|
||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.2.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
||||
<PackageReference Include="StreamJsonRpc" Version="2.17.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -27,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||
{
|
||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||
_startInfo.ArgumentList[0] = request.ToString();
|
||||
_startInfo.ArgumentList[0] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||
return ExecuteAsync(_startInfo, token);
|
||||
}
|
||||
|
||||
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
||||
{
|
||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||
_startInfo.ArgumentList[0] = rpcRequest.ToString();
|
||||
_startInfo.ArgumentList[0] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs
Normal file
20
Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
internal sealed class ExecutablePluginV2 : ProcessStreamPluginV2
|
||||
{
|
||||
protected override ProcessStartInfo StartInfo { get; set; }
|
||||
|
||||
public ExecutablePluginV2(string filename)
|
||||
{
|
||||
StartInfo = new ProcessStartInfo { FileName = filename };
|
||||
}
|
||||
|
||||
protected override MessageHandlerType MessageHandler { get; } = MessageHandlerType.NewLineDelimited;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,67 +19,35 @@ using System.Text.Json;
|
|||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public class JsonRPCErrorModel
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default);
|
||||
public record JsonRPCErrorModel(int Code, string Message, string Data);
|
||||
|
||||
public string Message { get; set; }
|
||||
public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
||||
public record JsonRPCQueryResponseModel(int Id,
|
||||
[property: JsonPropertyName("result")] List<JsonRPCResult> Result,
|
||||
IReadOnlyDictionary<string, object> SettingsChange = null,
|
||||
string DebugMessage = "",
|
||||
JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error);
|
||||
|
||||
public string Data { get; set; }
|
||||
}
|
||||
public record JsonRPCRequestModel(int Id,
|
||||
string Method,
|
||||
object[] Parameters,
|
||||
IReadOnlyDictionary<string, object> Settings = default,
|
||||
JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
||||
|
||||
|
||||
public class JsonRPCResponseModel
|
||||
{
|
||||
public string Result { get; set; }
|
||||
|
||||
public JsonRPCErrorModel Error { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCQueryResponseModel : JsonRPCResponseModel
|
||||
{
|
||||
[JsonPropertyName("result")]
|
||||
public new List<JsonRPCResult> Result { get; set; }
|
||||
|
||||
public Dictionary<string, object> SettingsChange { get; set; }
|
||||
|
||||
public string DebugMessage { get; set; }
|
||||
}
|
||||
|
||||
public class JsonRPCRequestModel
|
||||
{
|
||||
public string Method { get; set; }
|
||||
|
||||
public object[] Parameters { get; set; }
|
||||
|
||||
public Dictionary<string, object> Settings { get; set; }
|
||||
|
||||
private static readonly JsonSerializerOptions options = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json RPC Request that Flow Launcher sent to client
|
||||
/// </summary>
|
||||
public class JsonRPCServerRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json RPC Request(in query response) that client sent to Flow Launcher
|
||||
/// </summary>
|
||||
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
public bool DontHideAfterAction { get; set; }
|
||||
}
|
||||
|
||||
public record JsonRPCClientRequestModel(
|
||||
int Id,
|
||||
string Method,
|
||||
object[] Parameters,
|
||||
IReadOnlyDictionary<string, object> Settings,
|
||||
bool DontHideAfterAction = false,
|
||||
JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represent the json-rpc result item that client send to Flow Launcher
|
||||
/// Typically, we will send back this request model to client after user select the result item
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// Represent the plugin that using JsonPRC
|
||||
/// every JsonRPC plugin should has its own plugin instance
|
||||
/// </summary>
|
||||
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||
internal abstract class JsonRPCPlugin : JsonRPCPluginBase
|
||||
{
|
||||
protected PluginInitContext context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
||||
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
||||
|
|
@ -40,19 +39,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
private static readonly RecyclableMemoryStreamManager BufferManager = new();
|
||||
|
||||
private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json");
|
||||
private int RequestId { get; set; }
|
||||
|
||||
public List<Result> LoadContextMenus(Result selectedResult)
|
||||
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
|
||||
|
||||
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
var request = new JsonRPCRequestModel
|
||||
{
|
||||
Method = "context_menu",
|
||||
Parameters = new[]
|
||||
{
|
||||
selectedResult.ContextData
|
||||
}
|
||||
};
|
||||
var request = new JsonRPCRequestModel(RequestId++,
|
||||
"context_menu",
|
||||
new[] { selectedResult.ContextData });
|
||||
var output = Request(request);
|
||||
return DeserializedResult(output);
|
||||
}
|
||||
|
|
@ -77,7 +73,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
private Dictionary<string, object> Settings { get; set; }
|
||||
|
||||
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
||||
|
||||
|
|
@ -103,85 +98,42 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return ParseResults(queryResponseModel);
|
||||
}
|
||||
|
||||
|
||||
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
||||
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||
{
|
||||
if (queryResponseModel.Result == null) return null;
|
||||
if (result.JsonRPCAction == null) return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
|
||||
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
|
||||
{
|
||||
context.API.ShowMsg(queryResponseModel.DebugMessage);
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
|
||||
foreach (var result in queryResponseModel.Result)
|
||||
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
|
||||
{
|
||||
result.AsyncAction = async c =>
|
||||
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
|
||||
result.JsonRPCAction.Parameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
|
||||
|
||||
if (actionResponse.Length == 0)
|
||||
{
|
||||
UpdateSettings(result.SettingsChange);
|
||||
|
||||
if (result.JsonRPCAction == null) return false;
|
||||
|
||||
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
|
||||
{
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
|
||||
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
|
||||
{
|
||||
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
|
||||
result.JsonRPCAction.Parameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
|
||||
|
||||
if (actionResponse.Length == 0)
|
||||
{
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
|
||||
var jsonRpcRequestModel = await
|
||||
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
|
||||
|
||||
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
||||
{
|
||||
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
|
||||
jsonRpcRequestModel.Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
};
|
||||
}
|
||||
|
||||
var jsonRpcRequestModel = await
|
||||
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
|
||||
|
||||
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
||||
{
|
||||
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
|
||||
jsonRpcRequestModel.Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<Result>();
|
||||
|
||||
results.AddRange(queryResponseModel.Result);
|
||||
|
||||
UpdateSettings(queryResponseModel.SettingsChange);
|
||||
|
||||
return results;
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
|
||||
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
|
||||
{
|
||||
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
|
||||
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
|
||||
if (methodInfo == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
methodInfo.Invoke(PluginManager.API, parameters);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
#if (DEBUG)
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute external program and return the output
|
||||
|
|
@ -296,379 +248,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return sourceBuffer;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
var request = new JsonRPCRequestModel
|
||||
{
|
||||
Method = "query",
|
||||
Parameters = new object[]
|
||||
var request = new JsonRPCRequestModel(RequestId++,
|
||||
"query",
|
||||
new object[]
|
||||
{
|
||||
query.Search
|
||||
},
|
||||
Settings = Settings
|
||||
};
|
||||
Settings?.Inner);
|
||||
|
||||
var output = await RequestAsync(request, token);
|
||||
|
||||
return await DeserializedResultAsync(output);
|
||||
}
|
||||
|
||||
public async Task InitSettingAsync()
|
||||
{
|
||||
if (!File.Exists(SettingConfigurationPath))
|
||||
return;
|
||||
|
||||
if (File.Exists(SettingPath))
|
||||
{
|
||||
await using var fileStream = File.OpenRead(SettingPath);
|
||||
Settings = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(fileStream, options);
|
||||
}
|
||||
|
||||
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
|
||||
_settingsTemplate = deserializer.Deserialize<JsonRpcConfigurationModel>(await File.ReadAllTextAsync(SettingConfigurationPath));
|
||||
|
||||
Settings ??= new Dictionary<string, object>();
|
||||
|
||||
foreach (var (type, attribute) in _settingsTemplate.Body)
|
||||
{
|
||||
if (type == "textBlock")
|
||||
continue;
|
||||
if (!Settings.ContainsKey(attribute.Name))
|
||||
{
|
||||
Settings[attribute.Name] = attribute.DefaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
this.context = context;
|
||||
await InitSettingAsync();
|
||||
}
|
||||
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
|
||||
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
|
||||
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
|
||||
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
|
||||
private JsonRpcConfigurationModel _settingsTemplate;
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
if (Settings == null)
|
||||
return new();
|
||||
var settingWindow = new UserControl();
|
||||
var mainPanel = new Grid
|
||||
{
|
||||
Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
ColumnDefinition gridCol1 = new ColumnDefinition();
|
||||
ColumnDefinition gridCol2 = new ColumnDefinition();
|
||||
|
||||
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
||||
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol1);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol2);
|
||||
settingWindow.Content = mainPanel;
|
||||
int rowCount = 0;
|
||||
foreach (var (type, attribute) in _settingsTemplate.Body)
|
||||
{
|
||||
Separator sep = new Separator();
|
||||
sep.VerticalAlignment = VerticalAlignment.Top;
|
||||
sep.Margin = settingSepMargin;
|
||||
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelPanelMargin
|
||||
};
|
||||
RowDefinition gridRow = new RowDefinition();
|
||||
mainPanel.RowDefinitions.Add(gridRow);
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attribute.Label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
var desc = new TextBlock()
|
||||
{
|
||||
Text = attribute.Description,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingDescMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
||||
|
||||
if (attribute.Description == null) /* if no description, hide */
|
||||
desc.Visibility = Visibility.Collapsed;
|
||||
|
||||
|
||||
if (type != "textBlock") /* if textBlock, hide desc */
|
||||
{
|
||||
panel.Children.Add(name);
|
||||
panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
|
||||
FrameworkElement contentControl;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "textBlock":
|
||||
{
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
Padding = new Thickness(0, 0, 0, 0),
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "input":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "inputWithFileBtn":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
var Btn = new System.Windows.Controls.Button()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
|
||||
};
|
||||
var dockPanel = new DockPanel()
|
||||
{
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
DockPanel.SetDock(Btn, Dock.Right);
|
||||
dockPanel.Children.Add(Btn);
|
||||
dockPanel.Children.Add(textBox);
|
||||
contentControl = dockPanel;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "textarea":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Height = 120,
|
||||
Margin = settingControlMargin,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "passwordBox":
|
||||
{
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
Margin = settingControlMargin,
|
||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
contentControl = passwordBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "dropdown":
|
||||
{
|
||||
var comboBox = new System.Windows.Controls.ComboBox()
|
||||
{
|
||||
ItemsSource = attribute.Options,
|
||||
SelectedItem = Settings[attribute.Name],
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
|
||||
};
|
||||
contentControl = comboBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "checkbox":
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
|
||||
Margin = settingCheckboxMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
checkBox.Click += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
|
||||
};
|
||||
contentControl = checkBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
case "hyperlink":
|
||||
var hyperlink = new Hyperlink
|
||||
{
|
||||
ToolTip = attribute.Description, NavigateUri = attribute.url
|
||||
};
|
||||
var linkbtn = new System.Windows.Controls.Button
|
||||
{
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin
|
||||
};
|
||||
linkbtn.Content = attribute.urlLabel;
|
||||
|
||||
contentControl = linkbtn;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
if (type != "textBlock")
|
||||
_settingControls[attribute.Name] = contentControl;
|
||||
mainPanel.Children.Add(panel);
|
||||
mainPanel.Children.Add(contentControl);
|
||||
rowCount++;
|
||||
|
||||
}
|
||||
return settingWindow;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
if (Settings != null)
|
||||
{
|
||||
Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name));
|
||||
File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption));
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateSettings(Dictionary<string, object> settings)
|
||||
{
|
||||
if (settings == null || settings.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var (key, value) in settings)
|
||||
{
|
||||
if (Settings.ContainsKey(key))
|
||||
{
|
||||
Settings[key] = value;
|
||||
}
|
||||
if (_settingControls.ContainsKey(key))
|
||||
{
|
||||
|
||||
switch (_settingControls[key])
|
||||
{
|
||||
case TextBox textBox:
|
||||
textBox.Dispatcher.Invoke(() => textBox.Text = value as string);
|
||||
break;
|
||||
case PasswordBox passwordBox:
|
||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string);
|
||||
break;
|
||||
case System.Windows.Controls.ComboBox comboBox:
|
||||
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
||||
break;
|
||||
case CheckBox checkBox:
|
||||
checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
163
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
163
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using CheckBox = System.Windows.Controls.CheckBox;
|
||||
using Control = System.Windows.Controls.Control;
|
||||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using System.Windows.Documents;
|
||||
using static System.Windows.Forms.LinkLabel;
|
||||
using Droplex;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent the plugin that using JsonPRC
|
||||
/// every JsonRPC plugin should has its own plugin instance
|
||||
/// </summary>
|
||||
internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||
{
|
||||
protected PluginInitContext Context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
||||
private int RequestId { get; set; }
|
||||
|
||||
private string SettingConfigurationPath =>
|
||||
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
|
||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory,
|
||||
Context.CurrentPluginMetadata.Name, "Settings.json");
|
||||
|
||||
public abstract List<Result> LoadContextMenus(Result selectedResult);
|
||||
|
||||
protected static readonly JsonSerializerOptions DeserializeOption = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
#pragma warning disable SYSLIB0020
|
||||
// IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still
|
||||
// deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour
|
||||
// to accept null and fallback to a default etc, or just keep IgnoreNullValues for now
|
||||
// see: https://github.com/dotnet/runtime/issues/39152
|
||||
IgnoreNullValues = true,
|
||||
#pragma warning restore SYSLIB0020 // Type or member is obsolete
|
||||
Converters = { new JsonObjectConverter() }
|
||||
};
|
||||
|
||||
protected static readonly JsonSerializerOptions RequestSerializeOption = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
protected abstract Task<bool> ExecuteResultAsync(JsonRPCResult result);
|
||||
|
||||
protected JsonRPCPluginSettings Settings { get; set; }
|
||||
|
||||
protected List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
||||
{
|
||||
if (queryResponseModel.Result == null) return null;
|
||||
|
||||
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
|
||||
{
|
||||
Context.API.ShowMsg(queryResponseModel.DebugMessage);
|
||||
}
|
||||
|
||||
foreach (var result in queryResponseModel.Result)
|
||||
{
|
||||
result.AsyncAction = async _ =>
|
||||
{
|
||||
Settings?.UpdateSettings(result.SettingsChange);
|
||||
|
||||
return await ExecuteResultAsync(result);
|
||||
};
|
||||
}
|
||||
|
||||
var results = new List<Result>();
|
||||
|
||||
results.AddRange(queryResponseModel.Result);
|
||||
|
||||
Settings?.UpdateSettings(queryResponseModel.SettingsChange);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
protected void ExecuteFlowLauncherAPI(string method, object[] parameters)
|
||||
{
|
||||
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
|
||||
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
|
||||
|
||||
if (methodInfo == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
methodInfo.Invoke(Context.API, parameters);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
#if (DEBUG)
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task<List<Result>> QueryAsync(Query query, CancellationToken token);
|
||||
|
||||
|
||||
private async Task InitSettingAsync()
|
||||
{
|
||||
JsonRpcConfigurationModel configuration = null;
|
||||
if (File.Exists(SettingConfigurationPath))
|
||||
{
|
||||
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
|
||||
configuration =
|
||||
deserializer.Deserialize<JsonRpcConfigurationModel>(
|
||||
await File.ReadAllTextAsync(SettingConfigurationPath));
|
||||
}
|
||||
|
||||
|
||||
Settings ??= new JsonRPCPluginSettings
|
||||
{
|
||||
Configuration = configuration, SettingPath = SettingPath, API = Context.API
|
||||
};
|
||||
|
||||
await Settings.InitializeAsync();
|
||||
}
|
||||
|
||||
public virtual async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
this.Context = context;
|
||||
await InitSettingAsync();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
Settings?.Save();
|
||||
}
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
return Settings.CreateSettingPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
415
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
415
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public class JsonRPCPluginSettings
|
||||
{
|
||||
public required JsonRpcConfigurationModel? Configuration { get; init; }
|
||||
|
||||
public required string SettingPath { get; init; }
|
||||
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
|
||||
|
||||
public IReadOnlyDictionary<string, object> Inner => Settings;
|
||||
protected ConcurrentDictionary<string, object> Settings { get; set; }
|
||||
public required IPublicAPI API { get; init; }
|
||||
|
||||
private JsonStorage<ConcurrentDictionary<string, object>> _storage;
|
||||
|
||||
// maybe move to resource?
|
||||
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
|
||||
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
|
||||
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
|
||||
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_storage = new JsonStorage<ConcurrentDictionary<string, object>>(SettingPath);
|
||||
Settings = await _storage.LoadAsync();
|
||||
|
||||
if (Configuration == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (type, attributes) in Configuration.Body)
|
||||
{
|
||||
if (attributes.Name == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Settings.ContainsKey(attributes.Name))
|
||||
{
|
||||
Settings[attributes.Name] = attributes.DefaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
|
||||
{
|
||||
if (settings == null || settings.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var (key, value) in settings)
|
||||
{
|
||||
Settings[key] = value;
|
||||
|
||||
if (SettingControls.TryGetValue(key, out var control))
|
||||
{
|
||||
switch (control)
|
||||
{
|
||||
case TextBox textBox:
|
||||
textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty);
|
||||
break;
|
||||
case PasswordBox passwordBox:
|
||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty);
|
||||
break;
|
||||
case ComboBox comboBox:
|
||||
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
||||
break;
|
||||
case CheckBox checkBox:
|
||||
checkBox.Dispatcher.Invoke(() =>
|
||||
checkBox.IsChecked = value is bool isChecked
|
||||
? isChecked
|
||||
: bool.Parse(value as string ?? string.Empty));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
await _storage.SaveAsync();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_storage.Save();
|
||||
}
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
if (Settings == null || Settings.Count == 0)
|
||||
return new();
|
||||
|
||||
var settingWindow = new UserControl();
|
||||
var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||
|
||||
ColumnDefinition gridCol1 = new ColumnDefinition();
|
||||
ColumnDefinition gridCol2 = new ColumnDefinition();
|
||||
|
||||
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
||||
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol1);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol2);
|
||||
settingWindow.Content = mainPanel;
|
||||
int rowCount = 0;
|
||||
|
||||
foreach (var (type, attribute) in Configuration.Body)
|
||||
{
|
||||
Separator sep = new Separator();
|
||||
sep.VerticalAlignment = VerticalAlignment.Top;
|
||||
sep.Margin = settingSepMargin;
|
||||
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelPanelMargin
|
||||
};
|
||||
|
||||
RowDefinition gridRow = new RowDefinition();
|
||||
mainPanel.RowDefinitions.Add(gridRow);
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attribute.Label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
var desc = new TextBlock()
|
||||
{
|
||||
Text = attribute.Description,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingDescMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
||||
|
||||
if (attribute.Description == null) /* if no description, hide */
|
||||
desc.Visibility = Visibility.Collapsed;
|
||||
|
||||
|
||||
if (type != "textBlock") /* if textBlock, hide desc */
|
||||
{
|
||||
panel.Children.Add(name);
|
||||
panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
|
||||
FrameworkElement contentControl;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "textBlock":
|
||||
{
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
Padding = new Thickness(0, 0, 0, 0),
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
case "input":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
case "inputWithFileBtn":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
|
||||
var Btn = new System.Windows.Controls.Button()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
|
||||
};
|
||||
|
||||
var dockPanel = new DockPanel() { Margin = settingControlMargin };
|
||||
|
||||
DockPanel.SetDock(Btn, Dock.Right);
|
||||
dockPanel.Children.Add(Btn);
|
||||
dockPanel.Children.Add(textBox);
|
||||
contentControl = dockPanel;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
case "textarea":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Height = 120,
|
||||
Margin = settingControlMargin,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
case "passwordBox":
|
||||
{
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
Margin = settingControlMargin,
|
||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
|
||||
contentControl = passwordBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
case "dropdown":
|
||||
{
|
||||
var comboBox = new System.Windows.Controls.ComboBox()
|
||||
{
|
||||
ItemsSource = attribute.Options,
|
||||
SelectedItem = Settings[attribute.Name],
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
|
||||
};
|
||||
|
||||
contentControl = comboBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
case "checkbox":
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked =
|
||||
Settings[attribute.Name] is bool isChecked
|
||||
? isChecked
|
||||
: bool.Parse(attribute.DefaultValue),
|
||||
Margin = settingCheckboxMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
|
||||
checkBox.Click += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
|
||||
};
|
||||
|
||||
contentControl = checkBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
case "hyperlink":
|
||||
var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url };
|
||||
|
||||
var linkbtn = new System.Windows.Controls.Button
|
||||
{
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
|
||||
linkbtn.Content = attribute.urlLabel;
|
||||
|
||||
contentControl = linkbtn;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type != "textBlock")
|
||||
SettingControls[attribute.Name] = contentControl;
|
||||
|
||||
mainPanel.Children.Add(panel);
|
||||
mainPanel.Children.Add(contentControl);
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
return settingWindow;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
158
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Core.Plugin.JsonRPCV2Models;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using StreamJsonRpc;
|
||||
using IAsyncDisposable = System.IAsyncDisposable;
|
||||
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated
|
||||
{
|
||||
public const string JsonRpc = "JsonRPC";
|
||||
|
||||
protected abstract IDuplexPipe ClientPipe { get; set; }
|
||||
|
||||
protected StreamReader ErrorStream { get; set; }
|
||||
|
||||
private JsonRpc RPC { get; set; }
|
||||
|
||||
|
||||
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
|
||||
argument: result.JsonRPCAction.Parameters);
|
||||
|
||||
return res.Hide;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
|
||||
|
||||
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = JTF.Run(() => RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("context_menu",
|
||||
new object[] { selectedResult.ContextData }));
|
||||
|
||||
var results = ParseResults(res);
|
||||
|
||||
return results;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
||||
new object[] { query, Settings.Inner },
|
||||
token);
|
||||
|
||||
var results = ParseResults(res);
|
||||
|
||||
return results;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
await base.InitAsync(context);
|
||||
|
||||
SetupJsonRPC();
|
||||
|
||||
_ = ReadErrorAsync();
|
||||
|
||||
await RPC.InvokeAsync("initialize", context);
|
||||
|
||||
async Task ReadErrorAsync()
|
||||
{
|
||||
var error = await ErrorStream.ReadToEndAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
throw new Exception(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event ResultUpdatedEventHandler ResultsUpdated;
|
||||
|
||||
protected enum MessageHandlerType
|
||||
{
|
||||
HeaderDelimited,
|
||||
LengthHeaderDelimited,
|
||||
NewLineDelimited
|
||||
}
|
||||
|
||||
protected abstract MessageHandlerType MessageHandler { get; }
|
||||
|
||||
|
||||
private void SetupJsonRPC()
|
||||
{
|
||||
var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RequestSerializeOption };
|
||||
IJsonRpcMessageHandler handler = MessageHandler switch
|
||||
{
|
||||
MessageHandlerType.HeaderDelimited => new HeaderDelimitedMessageHandler(ClientPipe, formatter),
|
||||
MessageHandlerType.LengthHeaderDelimited => new LengthHeaderMessageHandler(ClientPipe, formatter),
|
||||
MessageHandlerType.NewLineDelimited => new NewLineDelimitedMessageHandler(ClientPipe, formatter),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API));
|
||||
|
||||
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((rawQuery, response) =>
|
||||
{
|
||||
var results = ParseResults(response);
|
||||
ResultsUpdated?.Invoke(this,
|
||||
new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results });
|
||||
}));
|
||||
RPC.SynchronizationContext = null;
|
||||
RPC.StartListening();
|
||||
}
|
||||
|
||||
public virtual Task ReloadDataAsync()
|
||||
{
|
||||
SetupJsonRPC();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await RPC.InvokeAsync("close");
|
||||
}
|
||||
catch (RemoteMethodNotFoundException e)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
RPC?.Dispose();
|
||||
ErrorStream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||
{
|
||||
public record JsonRPCExecuteResponse(bool Hide = true);
|
||||
}
|
||||
177
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
177
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||
{
|
||||
public class JsonRPCPublicAPI
|
||||
{
|
||||
private IPublicAPI _api;
|
||||
|
||||
public JsonRPCPublicAPI(IPublicAPI api)
|
||||
{
|
||||
_api = api;
|
||||
}
|
||||
|
||||
public void ChangeQuery(string query, bool requery = false)
|
||||
{
|
||||
_api.ChangeQuery(query, requery);
|
||||
}
|
||||
|
||||
public void RestartApp()
|
||||
{
|
||||
_api.RestartApp();
|
||||
}
|
||||
|
||||
public void ShellRun(string cmd, string filename = "cmd.exe")
|
||||
{
|
||||
_api.ShellRun(cmd, filename);
|
||||
}
|
||||
|
||||
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true)
|
||||
{
|
||||
_api.CopyToClipboard(text, directCopy, showDefaultNotification);
|
||||
}
|
||||
|
||||
public void SaveAppAllSettings()
|
||||
{
|
||||
_api.SaveAppAllSettings();
|
||||
}
|
||||
|
||||
public void SavePluginSettings()
|
||||
{
|
||||
_api.SavePluginSettings();
|
||||
}
|
||||
|
||||
public Task ReloadAllPluginDataAsync()
|
||||
{
|
||||
return _api.ReloadAllPluginData();
|
||||
}
|
||||
|
||||
public void CheckForNewUpdate()
|
||||
{
|
||||
_api.CheckForNewUpdate();
|
||||
}
|
||||
|
||||
public void ShowMsgError(string title, string subTitle = "")
|
||||
{
|
||||
_api.ShowMsgError(title, subTitle);
|
||||
}
|
||||
|
||||
public void ShowMainWindow()
|
||||
{
|
||||
_api.ShowMainWindow();
|
||||
}
|
||||
|
||||
public void HideMainWindow()
|
||||
{
|
||||
_api.HideMainWindow();
|
||||
}
|
||||
|
||||
public bool IsMainWindowVisible()
|
||||
{
|
||||
return _api.IsMainWindowVisible();
|
||||
}
|
||||
|
||||
public void ShowMsg(string title, string subTitle = "", string iconPath = "")
|
||||
{
|
||||
_api.ShowMsg(title, subTitle, iconPath);
|
||||
}
|
||||
|
||||
public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
|
||||
{
|
||||
_api.ShowMsg(title, subTitle, iconPath, useMainWindowAsOwner);
|
||||
}
|
||||
|
||||
public void OpenSettingDialog()
|
||||
{
|
||||
_api.OpenSettingDialog();
|
||||
}
|
||||
|
||||
public string GetTranslation(string key)
|
||||
{
|
||||
return _api.GetTranslation(key);
|
||||
}
|
||||
|
||||
public List<PluginPair> GetAllPlugins()
|
||||
{
|
||||
return _api.GetAllPlugins();
|
||||
}
|
||||
|
||||
|
||||
public MatchResult FuzzySearch(string query, string stringToCompare)
|
||||
{
|
||||
return _api.FuzzySearch(query, stringToCompare);
|
||||
}
|
||||
|
||||
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default)
|
||||
{
|
||||
return _api.HttpGetStringAsync(url, token);
|
||||
}
|
||||
|
||||
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default)
|
||||
{
|
||||
return _api.HttpGetStreamAsync(url, token);
|
||||
}
|
||||
|
||||
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _api.HttpDownloadAsync(url, filePath, token);
|
||||
}
|
||||
|
||||
public void AddActionKeyword(string pluginId, string newActionKeyword)
|
||||
{
|
||||
_api.AddActionKeyword(pluginId, newActionKeyword);
|
||||
}
|
||||
|
||||
public void RemoveActionKeyword(string pluginId, string oldActionKeyword)
|
||||
{
|
||||
_api.RemoveActionKeyword(pluginId, oldActionKeyword);
|
||||
}
|
||||
|
||||
public bool ActionKeywordAssigned(string actionKeyword)
|
||||
{
|
||||
return _api.ActionKeywordAssigned(actionKeyword);
|
||||
}
|
||||
|
||||
public void LogDebug(string className, string message, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
_api.LogDebug(className, message, methodName);
|
||||
}
|
||||
|
||||
public void LogInfo(string className, string message, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
_api.LogInfo(className, message, methodName);
|
||||
}
|
||||
|
||||
public void LogWarn(string className, string message, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
_api.LogWarn(className, message, methodName);
|
||||
}
|
||||
|
||||
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
|
||||
{
|
||||
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
|
||||
}
|
||||
|
||||
|
||||
public void OpenUrl(string url, bool? inPrivate = null)
|
||||
{
|
||||
_api.OpenUrl(url, inPrivate);
|
||||
}
|
||||
|
||||
|
||||
public void OpenAppUri(string appUri)
|
||||
{
|
||||
_api.OpenAppUri(appUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||
{
|
||||
public record JsonRPCQueryRequest(
|
||||
List<JsonRPCResult> Results
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
|
@ -27,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||
{
|
||||
_startInfo.ArgumentList[1] = request.ToString();
|
||||
_startInfo.ArgumentList[1] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||
return ExecuteAsync(_startInfo, token);
|
||||
}
|
||||
|
||||
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
||||
{
|
||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||
_startInfo.ArgumentList[1] = rpcRequest.ToString();
|
||||
_startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +43,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
_startInfo.ArgumentList.Add(string.Empty);
|
||||
await base.InitAsync(context);
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
await base.InitAsync(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
Flow.Launcher.Core/Plugin/NodePluginV2.cs
Normal file
30
Flow.Launcher.Core/Plugin/NodePluginV2.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Execution of JavaScript & TypeScript plugins
|
||||
/// </summary>
|
||||
internal sealed class NodePluginV2 : ProcessStreamPluginV2
|
||||
{
|
||||
public NodePluginV2(string filename)
|
||||
{
|
||||
StartInfo = new ProcessStartInfo { FileName = filename, };
|
||||
}
|
||||
|
||||
protected override ProcessStartInfo StartInfo { get; set; }
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
await base.InitAsync(context);
|
||||
}
|
||||
|
||||
protected override MessageHandlerType MessageHandler { get; } = MessageHandlerType.HeaderDelimited;
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,17 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
return existAssembly ?? (assemblyPath == null ? null : LoadFromAssemblyPath(assemblyPath));
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
var path = dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return LoadUnmanagedDllFromPath(path);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type)
|
||||
{
|
||||
|
|
@ -41,4 +52,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ using Flow.Launcher.Infrastructure.Logger;
|
|||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -27,9 +29,9 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public static IPublicAPI API { private set; get; }
|
||||
|
||||
// todo happlebao, this should not be public, the indicator function should be embeded
|
||||
public static PluginsSettings Settings;
|
||||
private static PluginsSettings Settings;
|
||||
private static List<PluginMetadata> _metadatas;
|
||||
private static List<string> _modifiedPlugins = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Directories that will hold Flow Launcher plugin directory
|
||||
|
|
@ -242,7 +244,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new FlowPluginException(metadata, e);
|
||||
Result r = new()
|
||||
{
|
||||
Title = $"{metadata.Name}: Failed to respond!",
|
||||
SubTitle = "Select this result for more info",
|
||||
IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ => { throw new FlowPluginException(metadata, e);},
|
||||
Score = -100
|
||||
};
|
||||
results.Add(r);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
@ -363,5 +377,159 @@ namespace Flow.Launcher.Core.Plugin
|
|||
RemoveActionKeyword(id, oldActionKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
|
||||
{
|
||||
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
|
||||
var unzippedFilesCount = Directory.GetFiles(unzippedParentFolderPath).Length;
|
||||
|
||||
// adjust path depending on how the plugin is zipped up
|
||||
// the recommended should be to zip up the folder not the contents
|
||||
if (unzippedFolderCount == 1 && unzippedFilesCount == 0)
|
||||
// folder is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/pluginFolderName/
|
||||
return Directory.GetDirectories(unzippedParentFolderPath)[0];
|
||||
|
||||
if (unzippedFilesCount > 1)
|
||||
// content is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/
|
||||
return unzippedParentFolderPath;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static bool SameOrLesserPluginVersionExists(string metadataPath)
|
||||
{
|
||||
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
|
||||
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
|
||||
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
|
||||
}
|
||||
|
||||
#region Public functions
|
||||
|
||||
public static bool PluginModified(string uuid)
|
||||
{
|
||||
return _modifiedPlugins.Contains(uuid);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
||||
UninstallPlugin(existingVersion, removeSettings:false, checkModified:false);
|
||||
_modifiedPlugins.Add(existingVersion.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
|
||||
/// </summary>
|
||||
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin.
|
||||
/// </summary>
|
||||
public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true)
|
||||
{
|
||||
UninstallPlugin(plugin, removeSettings, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal functions
|
||||
|
||||
internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
|
||||
{
|
||||
if (checkModified && PluginModified(plugin.ID))
|
||||
{
|
||||
// Distinguish exception from installing same or less version
|
||||
throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin));
|
||||
}
|
||||
|
||||
// Unzip plugin files to temp folder
|
||||
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
|
||||
|
||||
if(!plugin.IsFromLocalInstallPath)
|
||||
File.Delete(zipFilePath);
|
||||
|
||||
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
|
||||
|
||||
var metadataJsonFilePath = string.Empty;
|
||||
if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
|
||||
metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);
|
||||
|
||||
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist");
|
||||
}
|
||||
|
||||
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}");
|
||||
}
|
||||
|
||||
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
|
||||
|
||||
var defaultPluginIDs = new List<string>
|
||||
{
|
||||
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
||||
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
||||
"572be03c74c642baae319fc283e561a8", // Explorer
|
||||
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
||||
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
||||
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
||||
"791FC278BA414111B8D1886DFE447410", // Program
|
||||
"D409510CD0D2481F853690A07E6DC426", // Shell
|
||||
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
||||
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
||||
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
||||
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
||||
};
|
||||
|
||||
// Treat default plugin differently, it needs to be removable along with each flow release
|
||||
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
|
||||
? DataLocation.PluginsDirectory
|
||||
: Constant.PreinstalledDirectory;
|
||||
|
||||
var newPluginPath = Path.Combine(installDirectory, folderName);
|
||||
|
||||
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
|
||||
|
||||
Directory.Delete(tempFolderPluginPath, true);
|
||||
|
||||
if (checkModified)
|
||||
{
|
||||
_modifiedPlugins.Add(plugin.ID);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void UninstallPlugin(PluginMetadata plugin, bool removeSettings, bool checkModified)
|
||||
{
|
||||
if (checkModified && PluginModified(plugin.ID))
|
||||
{
|
||||
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
|
||||
}
|
||||
|
||||
if (removeSettings)
|
||||
{
|
||||
Settings.Plugins.Remove(plugin.ID);
|
||||
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||
}
|
||||
|
||||
// Marked for deletion. Will be deleted on next start up
|
||||
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
|
||||
|
||||
if (checkModified)
|
||||
{
|
||||
_modifiedPlugins.Add(plugin.ID);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,22 +19,33 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
{
|
||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||
|
||||
|
||||
var pythonEnv = new PythonEnvironment(metadatas, settings);
|
||||
var pythonV2Env = new PythonV2Environment(metadatas, settings);
|
||||
var tsEnv = new TypeScriptEnvironment(metadatas, settings);
|
||||
var jsEnv = new JavaScriptEnvironment(metadatas, settings);
|
||||
var tsV2Env = new TypeScriptV2Environment(metadatas, settings);
|
||||
var jsV2Env = new JavaScriptV2Environment(metadatas, settings);
|
||||
var pythonPlugins = pythonEnv.Setup();
|
||||
var pythonV2Plugins = pythonV2Env.Setup();
|
||||
var tsPlugins = tsEnv.Setup();
|
||||
var jsPlugins = jsEnv.Setup();
|
||||
|
||||
var tsV2Plugins = tsV2Env.Setup();
|
||||
var jsV2Plugins = jsV2Env.Setup();
|
||||
|
||||
var executablePlugins = ExecutablePlugins(metadatas);
|
||||
|
||||
var executableV2Plugins = ExecutableV2Plugins(metadatas);
|
||||
|
||||
var plugins = dotnetPlugins
|
||||
.Concat(pythonPlugins)
|
||||
.Concat(tsPlugins)
|
||||
.Concat(jsPlugins)
|
||||
.Concat(executablePlugins)
|
||||
.ToList();
|
||||
.Concat(pythonPlugins)
|
||||
.Concat(pythonV2Plugins)
|
||||
.Concat(tsPlugins)
|
||||
.Concat(jsPlugins)
|
||||
.Concat(tsV2Plugins)
|
||||
.Concat(jsV2Plugins)
|
||||
.Concat(executablePlugins)
|
||||
.Concat(executableV2Plugins)
|
||||
.ToList();
|
||||
return plugins;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +104,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return;
|
||||
}
|
||||
|
||||
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
metadata.InitTime += milliseconds;
|
||||
}
|
||||
|
|
@ -118,7 +129,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
|
||||
|
|
@ -127,5 +138,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
|
||||
});
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
91
Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
Normal file
91
Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Meziantou.Framework.Win32;
|
||||
using Microsoft.VisualBasic.ApplicationServices;
|
||||
using Nerdbank.Streams;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
internal abstract class ProcessStreamPluginV2 : JsonRPCPluginV2
|
||||
{
|
||||
private static JobObject _jobObject = new JobObject();
|
||||
|
||||
static ProcessStreamPluginV2()
|
||||
{
|
||||
_jobObject.SetLimits(new JobObjectLimits()
|
||||
{
|
||||
Flags = JobObjectLimitFlags.KillOnJobClose | JobObjectLimitFlags.DieOnUnhandledException |
|
||||
JobObjectLimitFlags.SilentBreakawayOk
|
||||
});
|
||||
|
||||
_jobObject.AssignProcess(Process.GetCurrentProcess());
|
||||
}
|
||||
|
||||
protected sealed override IDuplexPipe ClientPipe { get; set; } = null!;
|
||||
|
||||
protected abstract ProcessStartInfo StartInfo { get; set; }
|
||||
|
||||
protected Process ClientProcess { get; set; } = null!;
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
StartInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
||||
StartInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
||||
StartInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
||||
|
||||
StartInfo.RedirectStandardError = true;
|
||||
StartInfo.RedirectStandardInput = true;
|
||||
StartInfo.RedirectStandardOutput = true;
|
||||
StartInfo.CreateNoWindow = true;
|
||||
StartInfo.UseShellExecute = false;
|
||||
StartInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
|
||||
var process = Process.Start(StartInfo);
|
||||
ArgumentNullException.ThrowIfNull(process);
|
||||
ClientProcess = process;
|
||||
_jobObject.AssignProcess(ClientProcess);
|
||||
|
||||
SetupPipe(ClientProcess);
|
||||
|
||||
ErrorStream = ClientProcess.StandardError;
|
||||
|
||||
await base.InitAsync(context);
|
||||
}
|
||||
|
||||
private void SetupPipe(Process process)
|
||||
{
|
||||
var (reader, writer) = (PipeReader.Create(process.StandardOutput.BaseStream),
|
||||
PipeWriter.Create(process.StandardInput.BaseStream));
|
||||
ClientPipe = new DuplexPipe(reader, writer);
|
||||
}
|
||||
|
||||
|
||||
public override async Task ReloadDataAsync()
|
||||
{
|
||||
var oldProcess = ClientProcess;
|
||||
ClientProcess = Process.Start(StartInfo);
|
||||
ArgumentNullException.ThrowIfNull(ClientProcess);
|
||||
SetupPipe(ClientProcess);
|
||||
await base.ReloadDataAsync();
|
||||
oldProcess.Kill(true);
|
||||
await oldProcess.WaitForExitAsync();
|
||||
oldProcess.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
ClientProcess.Kill(true);
|
||||
await ClientProcess.WaitForExitAsync();
|
||||
ClientProcess.Dispose();
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
|
|
@ -22,7 +23,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
// temp fix for issue #667
|
||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||
{
|
||||
_startInfo.ArgumentList[2] = request.ToString();
|
||||
_startInfo.ArgumentList[2] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||
|
||||
return ExecuteAsync(_startInfo, token);
|
||||
}
|
||||
|
|
@ -45,8 +45,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
||||
{
|
||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||
_startInfo.ArgumentList[2] = rpcRequest.ToString();
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
_startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||
_startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory;
|
||||
// TODO: Async Action
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
|
|
|||
42
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
42
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Flow.Launcher.Core.Plugin.JsonRPCV2Models;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Nerdbank.Streams;
|
||||
using StreamJsonRpc;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
internal sealed class PythonPluginV2 : ProcessStreamPluginV2
|
||||
{
|
||||
protected override ProcessStartInfo StartInfo { get; set; }
|
||||
|
||||
public PythonPluginV2(string filename)
|
||||
{
|
||||
StartInfo = new ProcessStartInfo { FileName = filename, };
|
||||
|
||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
|
||||
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||
|
||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||
StartInfo.ArgumentList.Add("-B");
|
||||
}
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
await base.InitAsync(context);
|
||||
}
|
||||
|
||||
protected override MessageHandlerType MessageHandler { get; } = MessageHandlerType.NewLineDelimited;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
|
|
@ -27,6 +27,8 @@ namespace Flow.Launcher.Core.Resource
|
|||
public static Language Turkish = new Language("tr", "Türkçe");
|
||||
public static Language Czech = new Language("cs", "čeština");
|
||||
public static Language Arabic = new Language("ar", "اللغة العربية");
|
||||
public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt");
|
||||
|
||||
|
||||
public static List<Language> GetAvailableLanguages()
|
||||
{
|
||||
|
|
@ -54,7 +56,8 @@ namespace Flow.Launcher.Core.Resource
|
|||
Slovak,
|
||||
Turkish,
|
||||
Czech,
|
||||
Arabic
|
||||
Arabic,
|
||||
Vietnamese
|
||||
};
|
||||
return languages;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,13 +96,10 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
// Culture of this thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows
|
||||
// Culture of main thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||
// App domain
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.DefaultThreadCurrentCulture;
|
||||
|
||||
// Raise event after culture is set
|
||||
Settings.Language = language.LanguageCode;
|
||||
|
|
@ -193,7 +190,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.DefaultThreadCurrentCulture);
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -176,8 +176,6 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
|
||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle &&
|
||||
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
||||
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
||||
|
|
@ -189,9 +187,25 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(
|
||||
new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
|
||||
new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
|
||||
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||
}
|
||||
|
||||
if (
|
||||
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||
{
|
||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultSubFont));
|
||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultSubFontStyle));
|
||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultSubFontWeight));
|
||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultSubFontStretch));
|
||||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(
|
||||
new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o
|
||||
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||
}
|
||||
|
||||
/* Ignore Theme Window Width and use setting */
|
||||
var windowStyle = dict["WindowStyle"] as Style;
|
||||
var width = Settings.WindowSize;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
public static class Constant
|
||||
{
|
||||
public const string FlowLauncher = "Flow.Launcher";
|
||||
public const string FlowLauncherFullName = "Flow Launcher";
|
||||
public const string Plugins = "Plugins";
|
||||
public const string PluginMetadataFileName = "plugin.json";
|
||||
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<PackageReference Include="FastCache.Cached" Version="1.8.2" />
|
||||
<PackageReference Include="Fody" Version="6.5.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MemoryPack" Version="1.21.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.7.30" />
|
||||
<PackageReference Include="NLog" Version="4.7.10" />
|
||||
<PackageReference Include="NLog.Schema" Version="4.7.10" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.13.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using System.Windows.Input;
|
|||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||
{
|
||||
public class HotkeyModel
|
||||
public record struct HotkeyModel
|
||||
{
|
||||
public bool Alt { get; set; }
|
||||
public bool Shift { get; set; }
|
||||
|
|
@ -17,8 +17,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
|
||||
private static readonly Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
|
||||
{
|
||||
{Key.Space, "Space"},
|
||||
{Key.Oem3, "~"}
|
||||
{ Key.Space, "Space" }, { Key.Oem3, "~" }
|
||||
};
|
||||
|
||||
public ModifierKeys ModifierKeys
|
||||
|
|
@ -30,18 +29,22 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
{
|
||||
modifierKeys |= ModifierKeys.Alt;
|
||||
}
|
||||
|
||||
if (Shift)
|
||||
{
|
||||
modifierKeys |= ModifierKeys.Shift;
|
||||
}
|
||||
|
||||
if (Win)
|
||||
{
|
||||
modifierKeys |= ModifierKeys.Windows;
|
||||
}
|
||||
|
||||
if (Ctrl)
|
||||
{
|
||||
modifierKeys |= ModifierKeys.Control;
|
||||
}
|
||||
|
||||
return modifierKeys;
|
||||
}
|
||||
}
|
||||
|
|
@ -66,31 +69,37 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> keys = hotkeyString.Replace(" ", "").Split('+').ToList();
|
||||
if (keys.Contains("Alt"))
|
||||
{
|
||||
Alt = true;
|
||||
keys.Remove("Alt");
|
||||
}
|
||||
|
||||
if (keys.Contains("Shift"))
|
||||
{
|
||||
Shift = true;
|
||||
keys.Remove("Shift");
|
||||
}
|
||||
|
||||
if (keys.Contains("Win"))
|
||||
{
|
||||
Win = true;
|
||||
keys.Remove("Win");
|
||||
}
|
||||
|
||||
if (keys.Contains("Ctrl"))
|
||||
{
|
||||
Ctrl = true;
|
||||
keys.Remove("Ctrl");
|
||||
}
|
||||
|
||||
if (keys.Count == 1)
|
||||
{
|
||||
string charKey = keys[0];
|
||||
KeyValuePair<Key, string>? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey);
|
||||
KeyValuePair<Key, string>? specialSymbolPair =
|
||||
specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey);
|
||||
if (specialSymbolPair.Value.Value != null)
|
||||
{
|
||||
CharKey = specialSymbolPair.Value.Key;
|
||||
|
|
@ -103,7 +112,6 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,33 +119,39 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
List<string> keys = new List<string>();
|
||||
if (Ctrl)
|
||||
return string.Join(" + ", EnumerateDisplayKeys());
|
||||
}
|
||||
|
||||
public IEnumerable<string> EnumerateDisplayKeys()
|
||||
{
|
||||
if (Ctrl && CharKey is not (Key.LeftCtrl or Key.RightCtrl))
|
||||
{
|
||||
keys.Add("Ctrl");
|
||||
yield return "Ctrl";
|
||||
}
|
||||
if (Alt)
|
||||
|
||||
if (Alt && CharKey is not (Key.LeftAlt or Key.RightAlt))
|
||||
{
|
||||
keys.Add("Alt");
|
||||
yield return "Alt";
|
||||
}
|
||||
if (Shift)
|
||||
|
||||
if (Shift && CharKey is not (Key.LeftShift or Key.RightShift))
|
||||
{
|
||||
keys.Add("Shift");
|
||||
yield return "Shift";
|
||||
}
|
||||
if (Win)
|
||||
|
||||
if (Win && CharKey is not (Key.LWin or Key.RWin))
|
||||
{
|
||||
keys.Add("Win");
|
||||
yield return "Win";
|
||||
}
|
||||
|
||||
if (CharKey != Key.None)
|
||||
{
|
||||
keys.Add(specialSymbolDictionary.ContainsKey(CharKey)
|
||||
? specialSymbolDictionary[CharKey]
|
||||
: CharKey.ToString());
|
||||
yield return specialSymbolDictionary.TryGetValue(CharKey, out var value)
|
||||
? value
|
||||
: CharKey.ToString();
|
||||
}
|
||||
return string.Join(" + ", keys);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validate hotkey
|
||||
/// </summary>
|
||||
|
|
@ -164,11 +178,13 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
{
|
||||
KeyGesture keyGesture = new KeyGesture(CharKey, ModifierKeys);
|
||||
}
|
||||
catch (System.Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException)
|
||||
catch (System.Exception e) when
|
||||
(e is NotSupportedException || e is InvalidEnumArgumentException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ModifierKeys == ModifierKeys.None)
|
||||
{
|
||||
return !IsPrintableCharacter(CharKey);
|
||||
|
|
@ -206,18 +222,6 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
key == Key.Decimal;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is HotkeyModel other)
|
||||
{
|
||||
return ModifierKeys == other.ModifierKeys && CharKey == other.CharKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(ModifierKeys, CharKey);
|
||||
|
|
|
|||
17
Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs
Normal file
17
Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey;
|
||||
|
||||
/// <summary>
|
||||
/// Interface that you should implement in your settings class to be able to pass it to
|
||||
/// <c>Flow.Launcher.HotkeyControlDialog</c>. It allows the dialog to display the hotkeys that have already been
|
||||
/// registered, and optionally provide a way to unregister them.
|
||||
/// </summary>
|
||||
public interface IHotkeySettings
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of hotkeys that have already been registered. The dialog will display these hotkeys and provide a way to
|
||||
/// unregister them.
|
||||
/// </summary>
|
||||
public List<RegisteredHotkeyData> RegisteredHotkeys { get; }
|
||||
}
|
||||
119
Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs
Normal file
119
Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Represents a hotkey that has been registered. Used in <c>Flow.Launcher.HotkeyControlDialog</c> via
|
||||
/// <see cref="UserSettings"/> and <see cref="IHotkeySettings"/> to display errors if user tries to register a hotkey
|
||||
/// that has already been registered, and optionally provides a way to unregister the hotkey.
|
||||
/// </summary>
|
||||
public record RegisteredHotkeyData
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="HotkeyModel"/> representation of this hotkey.
|
||||
/// </summary>
|
||||
public HotkeyModel Hotkey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// String key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||
/// </summary>
|
||||
public string DescriptionResourceKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Array of values that will replace <c>{0}</c>, <c>{1}</c>, <c>{2}</c>, etc. in the localized string found via
|
||||
/// <see cref="DescriptionResourceKey"/>.
|
||||
/// </summary>
|
||||
public object?[] DescriptionFormatVariables { get; } = Array.Empty<object?>();
|
||||
|
||||
/// <summary>
|
||||
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that
|
||||
/// this hotkey can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||
/// </summary>
|
||||
public Action? RemoveHotkey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
|
||||
/// <c>descriptionResourceKey</c> doesn't need any arguments for <c>string.Format</c>. If it does,
|
||||
/// use one of the other constructors.
|
||||
/// </summary>
|
||||
/// <param name="hotkey">
|
||||
/// The hotkey this class will represent.
|
||||
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
|
||||
/// </param>
|
||||
/// <param name="descriptionResourceKey">
|
||||
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||
/// </param>
|
||||
/// <param name="removeHotkey">
|
||||
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
|
||||
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||
/// </param>
|
||||
public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action? removeHotkey = null)
|
||||
{
|
||||
Hotkey = new HotkeyModel(hotkey);
|
||||
DescriptionResourceKey = descriptionResourceKey;
|
||||
RemoveHotkey = removeHotkey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
|
||||
/// <c>descriptionResourceKey</c> needs exactly one argument for <c>string.Format</c>.
|
||||
/// </summary>
|
||||
/// <param name="hotkey">
|
||||
/// The hotkey this class will represent.
|
||||
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
|
||||
/// </param>
|
||||
/// <param name="descriptionResourceKey">
|
||||
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||
/// </param>
|
||||
/// <param name="descriptionFormatVariable">
|
||||
/// The value that will replace <c>{0}</c> in the localized string found via <c>description</c>.
|
||||
/// </param>
|
||||
/// <param name="removeHotkey">
|
||||
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
|
||||
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||
/// </param>
|
||||
public RegisteredHotkeyData(
|
||||
string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, Action? removeHotkey = null
|
||||
)
|
||||
{
|
||||
Hotkey = new HotkeyModel(hotkey);
|
||||
DescriptionResourceKey = descriptionResourceKey;
|
||||
DescriptionFormatVariables = new[] { descriptionFormatVariable };
|
||||
RemoveHotkey = removeHotkey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
|
||||
/// <paramref name="descriptionResourceKey"/> needs multiple arguments for <c>string.Format</c>.
|
||||
/// </summary>
|
||||
/// <param name="hotkey">
|
||||
/// The hotkey this class will represent.
|
||||
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
|
||||
/// </param>
|
||||
/// <param name="descriptionResourceKey">
|
||||
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||
/// </param>
|
||||
/// <param name="descriptionFormatVariables">
|
||||
/// Array of values that will replace <c>{0}</c>, <c>{1}</c>, <c>{2}</c>, etc.
|
||||
/// in the localized string found via <c>description</c>.
|
||||
/// </param>
|
||||
/// <param name="removeHotkey">
|
||||
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
|
||||
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||
/// </param>
|
||||
public RegisteredHotkeyData(
|
||||
string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, Action? removeHotkey = null
|
||||
)
|
||||
{
|
||||
Hotkey = new HotkeyModel(hotkey);
|
||||
DescriptionResourceKey = descriptionResourceKey;
|
||||
DescriptionFormatVariables = descriptionFormatVariables;
|
||||
RemoveHotkey = removeHotkey;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Media;
|
||||
using FastCache;
|
||||
using FastCache.Services;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
[Serializable]
|
||||
public class ImageUsage
|
||||
{
|
||||
|
||||
public int usage;
|
||||
public ImageSource imageSource;
|
||||
|
||||
|
|
@ -23,16 +23,13 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
public class ImageCache
|
||||
{
|
||||
private const int MaxCached = 50;
|
||||
public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new();
|
||||
private const int permissibleFactor = 2;
|
||||
private SemaphoreSlim semaphore = new(1, 1);
|
||||
private const int MaxCached = 150;
|
||||
|
||||
public void Initialize(Dictionary<(string, bool), int> usage)
|
||||
{
|
||||
foreach (var key in usage.Keys)
|
||||
{
|
||||
Data[key] = new ImageUsage(usage[key], null);
|
||||
Cached<ImageUsage>.Save(key, new ImageUsage(usage[key], null), TimeSpan.MaxValue, MaxCached);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,70 +37,48 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
get
|
||||
{
|
||||
if (!Data.TryGetValue((path, isFullImage), out var value))
|
||||
if (!Cached<ImageUsage>.TryGet((path, isFullImage), out var value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
value.usage++;
|
||||
return value.imageSource;
|
||||
|
||||
value.Value.usage++;
|
||||
return value.Value.imageSource;
|
||||
}
|
||||
set
|
||||
{
|
||||
Data.AddOrUpdate(
|
||||
(path, isFullImage),
|
||||
new ImageUsage(0, value),
|
||||
(k, v) =>
|
||||
{
|
||||
v.imageSource = value;
|
||||
v.usage++;
|
||||
return v;
|
||||
}
|
||||
);
|
||||
|
||||
SliceExtra();
|
||||
|
||||
async void SliceExtra()
|
||||
if (Cached<ImageUsage>.TryGet((path, isFullImage), out var cached))
|
||||
{
|
||||
// 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
|
||||
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
|
||||
if (Data.Count > permissibleFactor * MaxCached)
|
||||
{
|
||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary
|
||||
// Double Check to avoid concurrent remove
|
||||
if (Data.Count > permissibleFactor * MaxCached)
|
||||
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
|
||||
Data.TryRemove(key, out _);
|
||||
semaphore.Release();
|
||||
}
|
||||
cached.Value.imageSource = value;
|
||||
cached.Value.usage++;
|
||||
}
|
||||
|
||||
Cached<ImageUsage>.Save((path, isFullImage), new ImageUsage(0, value), TimeSpan.MaxValue,
|
||||
MaxCached);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key, bool isFullImage)
|
||||
{
|
||||
return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null;
|
||||
return Cached<ImageUsage>.TryGet((key, isFullImage), out _);
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, bool isFullImage, out ImageSource image)
|
||||
{
|
||||
if (key is not null)
|
||||
if (Cached<ImageUsage>.TryGet((key, isFullImage), out var value))
|
||||
{
|
||||
bool hasKey = Data.TryGetValue((key, isFullImage), out var imageUsage);
|
||||
image = hasKey ? imageUsage.imageSource : null;
|
||||
return hasKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
image = null;
|
||||
return false;
|
||||
image = value.Value.imageSource;
|
||||
value.Value.usage++;
|
||||
return image != null;
|
||||
}
|
||||
|
||||
image = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int CacheSize()
|
||||
{
|
||||
return Data.Count;
|
||||
return CacheManager.TotalCount<(string, bool), ImageUsage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -111,7 +86,14 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
/// </summary>
|
||||
public int UniqueImagesInCache()
|
||||
{
|
||||
return Data.Values.Select(x => x.imageSource).Distinct().Count();
|
||||
return CacheManager.EnumerateEntries<(string, bool), ImageUsage>().Select(x => x.Value.imageSource)
|
||||
.Distinct()
|
||||
.Count();
|
||||
}
|
||||
|
||||
public IEnumerable<Cached<(string, bool), ImageUsage>> EnumerateEntries()
|
||||
{
|
||||
return CacheManager.EnumerateEntries<(string, bool), ImageUsage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
|
@ -15,6 +16,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
public static class ImageLoader
|
||||
{
|
||||
private static readonly ImageCache ImageCache = new();
|
||||
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
|
||||
private static BinaryStorage<Dictionary<(string, bool), int>> _storage;
|
||||
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
||||
private static IImageHashGenerator _hashGenerator;
|
||||
|
|
@ -25,24 +27,18 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
public const int FullIconSize = 256;
|
||||
|
||||
|
||||
private static readonly string[] ImageExtensions =
|
||||
{
|
||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"
|
||||
};
|
||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||
|
||||
public static void Initialize()
|
||||
public static async Task InitializeAsync()
|
||||
{
|
||||
_storage = new BinaryStorage<Dictionary<(string, bool), int>>("Image");
|
||||
_hashGenerator = new ImageHashGenerator();
|
||||
|
||||
var usage = LoadStorageToConcurrentDictionary();
|
||||
var usage = await LoadStorageToConcurrentDictionaryAsync();
|
||||
|
||||
ImageCache.Initialize(usage.ToDictionary(x => x.Key, x => x.Value));
|
||||
|
||||
foreach (var icon in new[]
|
||||
{
|
||||
Constant.DefaultIcon, Constant.MissingImgIcon
|
||||
})
|
||||
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
|
||||
{
|
||||
ImageSource img = new BitmapImage(new Uri(icon));
|
||||
img.Freeze();
|
||||
|
|
@ -53,34 +49,46 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
|
||||
{
|
||||
foreach (var ((path, isFullImage), _) in ImageCache.Data)
|
||||
foreach (var ((path, isFullImage), _) in usage)
|
||||
{
|
||||
await LoadAsync(path, isFullImage);
|
||||
}
|
||||
});
|
||||
Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
||||
Log.Info(
|
||||
$"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
||||
});
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
public static async Task Save()
|
||||
{
|
||||
lock (_storage)
|
||||
await storageLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
_storage.Save(ImageCache.Data
|
||||
await _storage.SaveAsync(ImageCache.EnumerateEntries()
|
||||
.ToDictionary(
|
||||
x => x.Key,
|
||||
x => x.Value.usage));
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary()
|
||||
private static async Task<ConcurrentDictionary<(string, bool), int>> LoadStorageToConcurrentDictionaryAsync()
|
||||
{
|
||||
lock (_storage)
|
||||
await storageLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>());
|
||||
var loaded = await _storage.TryLoadAsync(new Dictionary<(string, bool), int>());
|
||||
|
||||
return new ConcurrentDictionary<(string, bool), int>(loaded);
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private class ImageResult
|
||||
|
|
@ -117,9 +125,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return new ImageResult(MissingImage, ImageType.Error);
|
||||
}
|
||||
|
||||
if (ImageCache.ContainsKey(path, loadFullImage))
|
||||
// extra scope for use of same variable name
|
||||
{
|
||||
return new ImageResult(ImageCache[path, loadFullImage], ImageType.Cache);
|
||||
if (ImageCache.TryGetValue(path, loadFullImage, out var imageSource))
|
||||
{
|
||||
return new ImageResult(imageSource, ImageType.Cache);
|
||||
}
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult)
|
||||
|
|
@ -129,6 +140,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
ImageCache[path, loadFullImage] = image;
|
||||
return new ImageResult(image, ImageType.ImageFile);
|
||||
}
|
||||
|
||||
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var imageSource = new BitmapImage(new Uri(path));
|
||||
|
|
@ -158,6 +170,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
return imageResult;
|
||||
}
|
||||
|
||||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||
{
|
||||
// Download image from url
|
||||
|
|
@ -173,6 +186,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
image.DecodePixelHeight = SmallIconSize;
|
||||
image.DecodePixelWidth = SmallIconSize;
|
||||
}
|
||||
|
||||
image.StreamSource = buffer;
|
||||
image.EndInit();
|
||||
image.StreamSource = null;
|
||||
|
|
@ -188,8 +202,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
if (Directory.Exists(path))
|
||||
{
|
||||
/* Directories can also have thumbnails instead of shell icons.
|
||||
* Generating thumbnails for a bunch of folder results while scrolling
|
||||
* could have a big impact on performance and Flow.Launcher responsibility.
|
||||
* Generating thumbnails for a bunch of folder results while scrolling
|
||||
* could have a big impact on performance and Flow.Launcher responsibility.
|
||||
* - Solution: just load the icon
|
||||
*/
|
||||
type = ImageType.Folder;
|
||||
|
|
@ -208,9 +222,9 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
else
|
||||
{
|
||||
/* Although the documentation for GetImage on MSDN indicates that
|
||||
/* Although the documentation for GetImage on MSDN indicates that
|
||||
* if a thumbnail is available it will return one, this has proved to not
|
||||
* be the case in many situations while testing.
|
||||
* be the case in many situations while testing.
|
||||
* - Solution: explicitly pass the ThumbnailOnly flag
|
||||
*/
|
||||
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
|
||||
|
|
@ -236,7 +250,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return new ImageResult(image, type);
|
||||
}
|
||||
|
||||
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize)
|
||||
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly,
|
||||
int size = SmallIconSize)
|
||||
{
|
||||
return WindowsThumbnailProvider.GetThumbnail(
|
||||
path,
|
||||
|
|
@ -261,17 +276,19 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
var img = imageResult.ImageSource;
|
||||
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
|
||||
{ // we need to get image hash
|
||||
{
|
||||
// we need to get image hash
|
||||
string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null;
|
||||
if (hash != null)
|
||||
{
|
||||
|
||||
if (GuidToKey.TryGetValue(hash, out string key))
|
||||
{ // image already exists
|
||||
{
|
||||
// image already exists
|
||||
img = ImageCache[key, loadFullImage] ?? img;
|
||||
}
|
||||
else
|
||||
{ // new guid
|
||||
{
|
||||
// new guid
|
||||
|
||||
GuidToKey[hash] = path;
|
||||
}
|
||||
|
|
@ -289,7 +306,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
BitmapImage image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.UriSource = new Uri(path);
|
||||
image.UriSource = new Uri(path);
|
||||
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
image.EndInit();
|
||||
|
||||
|
|
@ -314,8 +331,10 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
resizedHeight.EndInit();
|
||||
return resizedHeight;
|
||||
}
|
||||
|
||||
return resizedWidth;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,67 +4,65 @@ using System.Reflection;
|
|||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using MemoryPack;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete.
|
||||
/// <summary>
|
||||
/// Stroage object using binary data
|
||||
/// Normally, it has better performance, but not readable
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable
|
||||
/// https://github.com/Cysharp/MemoryPack
|
||||
/// </remarks>
|
||||
public class BinaryStorage<T>
|
||||
{
|
||||
const string DirectoryName = "Cache";
|
||||
|
||||
const string FileSuffix = ".cache";
|
||||
|
||||
public BinaryStorage(string filename)
|
||||
{
|
||||
const string directoryName = "Cache";
|
||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), directoryName);
|
||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||
Helper.ValidateDirectory(directoryPath);
|
||||
|
||||
const string fileSuffix = ".cache";
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{fileSuffix}");
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||
}
|
||||
|
||||
public string FilePath { get; }
|
||||
|
||||
public T TryLoad(T defaultData)
|
||||
public async ValueTask<T> TryLoadAsync(T defaultData)
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
if (new FileInfo(FilePath).Length == 0)
|
||||
{
|
||||
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
|
||||
Save(defaultData);
|
||||
await SaveAsync(defaultData);
|
||||
return defaultData;
|
||||
}
|
||||
|
||||
using (var stream = new FileStream(FilePath, FileMode.Open))
|
||||
{
|
||||
var d = Deserialize(stream, defaultData);
|
||||
return d;
|
||||
}
|
||||
await using var stream = new FileStream(FilePath, FileMode.Open);
|
||||
var d = await DeserializeAsync(stream, defaultData);
|
||||
return d;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
|
||||
Save(defaultData);
|
||||
await SaveAsync(defaultData);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
|
||||
private T Deserialize(FileStream stream, T defaultData)
|
||||
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
||||
{
|
||||
//http://stackoverflow.com/questions/2120055/binaryformatter-deserialize-gives-serializationexception
|
||||
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
|
||||
BinaryFormatter binaryFormatter = new BinaryFormatter
|
||||
{
|
||||
AssemblyFormat = FormatterAssemblyStyle.Simple
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var t = ((T)binaryFormatter.Deserialize(stream)).NonNull();
|
||||
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
||||
return t;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
|
|
@ -72,47 +70,12 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
||||
return defaultData;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
|
||||
public async ValueTask SaveAsync(T data)
|
||||
{
|
||||
Assembly ayResult = null;
|
||||
string sShortAssemblyName = args.Name.Split(',')[0];
|
||||
Assembly[] ayAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
foreach (Assembly ayAssembly in ayAssemblies)
|
||||
{
|
||||
if (sShortAssemblyName == ayAssembly.FullName.Split(',')[0])
|
||||
{
|
||||
ayResult = ayAssembly;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ayResult;
|
||||
}
|
||||
|
||||
public void Save(T data)
|
||||
{
|
||||
using (var stream = new FileStream(FilePath, FileMode.Create))
|
||||
{
|
||||
BinaryFormatter binaryFormatter = new BinaryFormatter
|
||||
{
|
||||
AssemblyFormat = FormatterAssemblyStyle.Simple
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
binaryFormatter.Serialize(stream, data);
|
||||
}
|
||||
catch (SerializationException e)
|
||||
{
|
||||
Log.Exception($"|BinaryStorage.Save|serialize error for file <{FilePath}>", e);
|
||||
}
|
||||
}
|
||||
await using var stream = new FileStream(FilePath, FileMode.Create);
|
||||
await MemoryPackSerializer.SerializeAsync(stream, data);
|
||||
}
|
||||
}
|
||||
#pragma warning restore SYSLIB0011
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
|
|
@ -26,6 +27,85 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
|
||||
protected string DirectoryPath { get; init; } = null!;
|
||||
|
||||
// Let the derived class to set the file path
|
||||
protected JsonStorage()
|
||||
{
|
||||
}
|
||||
public JsonStorage(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
||||
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
}
|
||||
|
||||
public async Task<T> LoadAsync()
|
||||
{
|
||||
if (Data != null)
|
||||
return Data;
|
||||
|
||||
string? serialized = null;
|
||||
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
serialized = await File.ReadAllTextAsync(FilePath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(serialized))
|
||||
{
|
||||
try
|
||||
{
|
||||
Data = JsonSerializer.Deserialize<T>(serialized) ?? await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Data = await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Data = await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
|
||||
return Data.NonNull();
|
||||
}
|
||||
|
||||
private async ValueTask<T> LoadBackupOrDefaultAsync()
|
||||
{
|
||||
var backup = await TryLoadBackupAsync();
|
||||
|
||||
return backup ?? LoadDefault();
|
||||
}
|
||||
|
||||
private async ValueTask<T?> TryLoadBackupAsync()
|
||||
{
|
||||
if (!File.Exists(BackupFilePath))
|
||||
return default;
|
||||
|
||||
try
|
||||
{
|
||||
await using var source = File.OpenRead(BackupFilePath);
|
||||
var data = await JsonSerializer.DeserializeAsync<T>(source) ?? default;
|
||||
|
||||
if (data != null)
|
||||
RestoreBackup();
|
||||
|
||||
return data;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
private void RestoreBackup()
|
||||
{
|
||||
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
|
||||
|
||||
if (File.Exists(FilePath))
|
||||
File.Replace(BackupFilePath, FilePath, null);
|
||||
else
|
||||
File.Move(BackupFilePath, FilePath);
|
||||
}
|
||||
|
||||
public T Load()
|
||||
{
|
||||
|
|
@ -75,18 +155,9 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
|
||||
|
||||
if(File.Exists(FilePath))
|
||||
File.Replace(BackupFilePath, FilePath, null);
|
||||
else
|
||||
File.Move(BackupFilePath, FilePath);
|
||||
RestoreBackup();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return default;
|
||||
return data;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
|
|
@ -115,6 +186,20 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
|
||||
File.WriteAllText(TempFilePath, serialized);
|
||||
|
||||
AtomicWriteSetting();
|
||||
}
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
var tempOutput = File.OpenWrite(TempFilePath);
|
||||
await JsonSerializer.SerializeAsync(tempOutput, Data,
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
AtomicWriteSetting();
|
||||
}
|
||||
private void AtomicWriteSetting()
|
||||
{
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
File.Move(TempFilePath, FilePath);
|
||||
|
|
@ -124,5 +209,6 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
File.Replace(TempFilePath, FilePath, BackupFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ using System.Collections.ObjectModel;
|
|||
using System.Drawing;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
public class Settings : BaseModel
|
||||
public class Settings : BaseModel, IHotkeySettings
|
||||
{
|
||||
private string language = "en";
|
||||
private string _theme = Constant.DefaultTheme;
|
||||
|
|
@ -20,6 +21,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public bool ShowOpenResultHotkey { get; set; } = true;
|
||||
public double WindowSize { get; set; } = 580;
|
||||
public string PreviewHotkey { get; set; } = $"F1";
|
||||
public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab";
|
||||
public string AutoCompleteHotkey2 { get; set; } = $"";
|
||||
public string SelectNextItemHotkey { get; set; } = $"Tab";
|
||||
public string SelectNextItemHotkey2 { get; set; } = $"";
|
||||
public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab";
|
||||
public string SelectPrevItemHotkey2 { get; set; } = $"";
|
||||
public string SelectNextPageHotkey { get; set; } = $"PageUp";
|
||||
public string SelectPrevPageHotkey { get; set; } = $"PageDown";
|
||||
public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O";
|
||||
public string SettingWindowHotkey { get; set; } = $"Ctrl+I";
|
||||
public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
|
||||
public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
|
||||
|
||||
public string Language
|
||||
{
|
||||
|
|
@ -43,6 +56,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
}
|
||||
public bool UseDropShadowEffect { get; set; } = false;
|
||||
|
||||
/* Appearance Settings. It should be separated from the setting later.*/
|
||||
public double WindowHeightSize { get; set; } = 42;
|
||||
public double ItemHeightSize { get; set; } = 58;
|
||||
public double QueryBoxFontSize { get; set; } = 20;
|
||||
public double ResultItemFontSize { get; set; } = 16;
|
||||
public double ResultSubItemFontSize { get; set; } = 13;
|
||||
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
||||
public string QueryBoxFontStyle { get; set; }
|
||||
public string QueryBoxFontWeight { get; set; }
|
||||
|
|
@ -51,9 +71,15 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public string ResultFontStyle { get; set; }
|
||||
public string ResultFontWeight { get; set; }
|
||||
public string ResultFontStretch { get; set; }
|
||||
public string ResultSubFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
||||
public string ResultSubFontStyle { get; set; }
|
||||
public string ResultSubFontWeight { get; set; }
|
||||
public string ResultSubFontStretch { get; set; }
|
||||
public bool UseGlyphIcons { get; set; } = true;
|
||||
public bool UseAnimation { get; set; } = true;
|
||||
public bool UseSound { get; set; } = true;
|
||||
public double SoundVolume { get; set; } = 50;
|
||||
|
||||
public bool UseClock { get; set; } = true;
|
||||
public bool UseDate { get; set; } = false;
|
||||
public string TimeFormat { get; set; } = "hh:mm tt";
|
||||
|
|
@ -166,32 +192,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public bool AlwaysStartEn { get; set; } = false;
|
||||
|
||||
private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular;
|
||||
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
|
||||
|
||||
[JsonIgnore]
|
||||
public string QuerySearchPrecisionString
|
||||
public SearchPrecisionScore QuerySearchPrecision
|
||||
{
|
||||
get { return QuerySearchPrecision.ToString(); }
|
||||
get => _querySearchPrecision;
|
||||
set
|
||||
{
|
||||
try
|
||||
{
|
||||
var precisionScore = (SearchPrecisionScore)Enum
|
||||
.Parse(typeof(SearchPrecisionScore), value);
|
||||
|
||||
QuerySearchPrecision = precisionScore;
|
||||
StringMatcher.Instance.UserSettingSearchPrecision = precisionScore;
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e);
|
||||
|
||||
QuerySearchPrecision = SearchPrecisionScore.Regular;
|
||||
StringMatcher.Instance.UserSettingSearchPrecision = SearchPrecisionScore.Regular;
|
||||
|
||||
throw;
|
||||
}
|
||||
_querySearchPrecision = value;
|
||||
if (StringMatcher.Instance != null)
|
||||
StringMatcher.Instance.UserSettingSearchPrecision = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,17 +209,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public double WindowLeft { get; set; }
|
||||
public double WindowTop { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Custom left position on selected monitor
|
||||
/// </summary>
|
||||
public double CustomWindowLeft { get; set; } = 0;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Custom top position on selected monitor
|
||||
/// </summary>
|
||||
public double CustomWindowTop { get; set; } = 0;
|
||||
|
||||
|
||||
public bool KeepMaxResults { get; set; } = false;
|
||||
public int MaxResultsToShow { get; set; } = 5;
|
||||
public int ActivateTimes { get; set; }
|
||||
|
||||
|
|
@ -221,7 +232,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
[JsonIgnore]
|
||||
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
|
||||
{
|
||||
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
|
||||
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
|
||||
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
|
||||
};
|
||||
|
||||
|
|
@ -245,7 +256,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
|
||||
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center;
|
||||
|
||||
|
|
@ -262,9 +273,99 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
|
||||
public int CustomAnimationLength { get; set; } = 360;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool WMPInstalled { get; set; } = true;
|
||||
|
||||
|
||||
// This needs to be loaded last by staying at the bottom
|
||||
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
|
||||
|
||||
[JsonIgnore]
|
||||
public List<RegisteredHotkeyData> RegisteredHotkeys
|
||||
{
|
||||
get
|
||||
{
|
||||
var list = FixedHotkeys();
|
||||
|
||||
// Customizeable hotkeys
|
||||
if(!string.IsNullOrEmpty(Hotkey))
|
||||
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
|
||||
if(!string.IsNullOrEmpty(PreviewHotkey))
|
||||
list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = ""));
|
||||
if(!string.IsNullOrEmpty(AutoCompleteHotkey))
|
||||
list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = ""));
|
||||
if(!string.IsNullOrEmpty(AutoCompleteHotkey2))
|
||||
list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = ""));
|
||||
if(!string.IsNullOrEmpty(SelectNextItemHotkey))
|
||||
list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = ""));
|
||||
if(!string.IsNullOrEmpty(SelectNextItemHotkey2))
|
||||
list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = ""));
|
||||
if(!string.IsNullOrEmpty(SelectPrevItemHotkey))
|
||||
list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = ""));
|
||||
if(!string.IsNullOrEmpty(SelectPrevItemHotkey2))
|
||||
list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = ""));
|
||||
if(!string.IsNullOrEmpty(SettingWindowHotkey))
|
||||
list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = ""));
|
||||
if(!string.IsNullOrEmpty(OpenContextMenuHotkey))
|
||||
list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = ""));
|
||||
if(!string.IsNullOrEmpty(SelectNextPageHotkey))
|
||||
list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = ""));
|
||||
if(!string.IsNullOrEmpty(SelectPrevPageHotkey))
|
||||
list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = ""));
|
||||
if (!string.IsNullOrEmpty(CycleHistoryUpHotkey))
|
||||
list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = ""));
|
||||
if (!string.IsNullOrEmpty(CycleHistoryDownHotkey))
|
||||
list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = ""));
|
||||
|
||||
// Custom Query Hotkeys
|
||||
foreach (var customPluginHotkey in CustomPluginHotkeys)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey))
|
||||
list.Add(new(customPluginHotkey.Hotkey, "customQueryHotkey", () => customPluginHotkey.Hotkey = ""));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private List<RegisteredHotkeyData> FixedHotkeys()
|
||||
{
|
||||
return new List<RegisteredHotkeyData>
|
||||
{
|
||||
new("Up", "HotkeyLeftRightDesc"),
|
||||
new("Down", "HotkeyLeftRightDesc"),
|
||||
new("Left", "HotkeyUpDownDesc"),
|
||||
new("Right", "HotkeyUpDownDesc"),
|
||||
new("Escape", "HotkeyESCDesc"),
|
||||
new("F5", "ReloadPluginHotkey"),
|
||||
new("Alt+Home", "HotkeySelectFirstResult"),
|
||||
new("Alt+End", "HotkeySelectLastResult"),
|
||||
new("Ctrl+R", "HotkeyRequery"),
|
||||
new("Ctrl+H", "ToggleHistoryHotkey"),
|
||||
new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"),
|
||||
new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"),
|
||||
new("Ctrl+OemPlus", "QuickHeightHotkey"),
|
||||
new("Ctrl+OemMinus", "QuickHeightHotkey"),
|
||||
new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"),
|
||||
new("Shift+Enter", "OpenContextMenuHotkey"),
|
||||
new("Enter", "HotkeyRunDesc"),
|
||||
new("Ctrl+Enter", "OpenContainFolderHotkey"),
|
||||
new("Alt+Enter", "HotkeyOpenResult"),
|
||||
new("Ctrl+F12", "ToggleGameModeHotkey"),
|
||||
new("Ctrl+Shift+C", "CopyFilePathHotkey"),
|
||||
|
||||
new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1),
|
||||
new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2),
|
||||
new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3),
|
||||
new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4),
|
||||
new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5),
|
||||
new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6),
|
||||
new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7),
|
||||
new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8),
|
||||
new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9),
|
||||
new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum LastQueryMode
|
||||
|
|
@ -280,7 +381,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
Light,
|
||||
Dark
|
||||
}
|
||||
|
||||
|
||||
public enum SearchWindowScreens
|
||||
{
|
||||
RememberLastLaunchLocation,
|
||||
|
|
@ -289,7 +390,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
Primary,
|
||||
Custom
|
||||
}
|
||||
|
||||
|
||||
public enum SearchWindowAligns
|
||||
{
|
||||
Center,
|
||||
|
|
|
|||
|
|
@ -50,5 +50,12 @@ namespace Flow.Launcher.Plugin
|
|||
(AltPressed ? ModifierKeys.Alt : ModifierKeys.None) |
|
||||
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
|
||||
}
|
||||
|
||||
public static readonly SpecialKeyState Default = new () {
|
||||
CtrlPressed = false,
|
||||
ShiftPressed = false,
|
||||
AltPressed = false,
|
||||
WinPressed = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public const string Python = "Python";
|
||||
|
||||
/// <summary>
|
||||
/// Python V2
|
||||
/// </summary>
|
||||
public const string PythonV2 = "Python_v2";
|
||||
|
||||
/// <summary>
|
||||
/// C#
|
||||
/// </summary>
|
||||
|
|
@ -27,16 +32,31 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public const string Executable = "Executable";
|
||||
|
||||
/// <summary>
|
||||
/// Standard .exe
|
||||
/// </summary>
|
||||
public const string ExecutableV2 = "Executable_V2";
|
||||
|
||||
/// <summary>
|
||||
/// TypeScript
|
||||
/// </summary>
|
||||
public const string TypeScript = "TypeScript";
|
||||
|
||||
/// <summary>
|
||||
/// TypeScript
|
||||
/// </summary>
|
||||
public const string TypeScriptV2 = "TypeScript_V2";
|
||||
|
||||
/// <summary>
|
||||
/// JavaScript
|
||||
/// </summary>
|
||||
public const string JavaScript = "JavaScript";
|
||||
|
||||
/// <summary>
|
||||
/// JavaScript
|
||||
/// </summary>
|
||||
public const string JavaScriptV2 = "JavaScript_V2";
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this language is a .NET language
|
||||
/// </summary>
|
||||
|
|
@ -45,7 +65,7 @@ namespace Flow.Launcher.Plugin
|
|||
public static bool IsDotNet(string language)
|
||||
{
|
||||
return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(FSharp, StringComparison.OrdinalIgnoreCase);
|
||||
|| language.Equals(FSharp, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -56,10 +76,15 @@ namespace Flow.Launcher.Plugin
|
|||
public static bool IsAllowed(string language)
|
||||
{
|
||||
return IsDotNet(language)
|
||||
|| language.Equals(Python, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(Executable, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase);
|
||||
|| language.Equals(Python, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(Executable, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase);
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>4.1.1</Version>
|
||||
<PackageVersion>4.1.1</PackageVersion>
|
||||
<AssemblyVersion>4.1.1</AssemblyVersion>
|
||||
<FileVersion>4.1.1</FileVersion>
|
||||
<Version>4.3.0</Version>
|
||||
<PackageVersion>4.3.0</PackageVersion>
|
||||
<AssemblyVersion>4.3.0</AssemblyVersion>
|
||||
<FileVersion>4.3.0</FileVersion>
|
||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||
<Authors>Flow-Launcher</Authors>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -274,5 +274,29 @@ namespace Flow.Launcher.Plugin
|
|||
/// Non-C# plugins should use this method
|
||||
/// </summary>
|
||||
public void OpenAppUri(string appUri);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles Game Mode. off -> on and backwards
|
||||
/// </summary>
|
||||
public void ToggleGameMode();
|
||||
|
||||
/// <summary>
|
||||
/// Switches Game Mode to given value
|
||||
/// </summary>
|
||||
/// <param name="value">New Game Mode status</param>
|
||||
public void SetGameMode(bool value);
|
||||
|
||||
/// <summary>
|
||||
/// Representing Game Mode status
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsGameModeOn();
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the query.
|
||||
/// This method should run
|
||||
/// </summary>
|
||||
/// <param name="reselect">Choose the first result after reload if true; keep the last selected result if false. Default is true.</param>
|
||||
public void ReQuery(bool reselect = true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
|
|
@ -64,6 +67,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public string ActionKeyword { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
/// <summary>
|
||||
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
||||
/// </summary>
|
||||
|
|
@ -71,12 +75,14 @@ namespace Flow.Launcher.Plugin
|
|||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
public string FirstSearch => SplitSearch(0);
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
private string _secondToEndSearch;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// strings from second search (including) to last search
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : "";
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -85,6 +91,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <remarks>
|
||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string SecondSearch => SplitSearch(1);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -93,6 +100,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <remarks>
|
||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string ThirdSearch => SplitSearch(2);
|
||||
|
||||
private string SplitSearch(int index)
|
||||
|
|
|
|||
|
|
@ -183,6 +183,31 @@ namespace Flow.Launcher.Plugin
|
|||
return Title + SubTitle + Score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the current result
|
||||
/// </summary>
|
||||
public Result Clone()
|
||||
{
|
||||
return new Result
|
||||
{
|
||||
Title = Title,
|
||||
SubTitle = SubTitle,
|
||||
ActionKeywordAssigned = ActionKeywordAssigned,
|
||||
CopyText = CopyText,
|
||||
AutoCompleteText = AutoCompleteText,
|
||||
IcoPath = IcoPath,
|
||||
RoundedIcon = RoundedIcon,
|
||||
Icon = Icon,
|
||||
Glyph = Glyph,
|
||||
Action = Action,
|
||||
AsyncAction = AsyncAction,
|
||||
Score = Score,
|
||||
TitleHighlightData = TitleHighlightData,
|
||||
OriginQuery = OriginQuery,
|
||||
PluginDirectory = PluginDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Additional data associated with this result
|
||||
/// </summary>
|
||||
|
|
@ -233,7 +258,7 @@ namespace Flow.Launcher.Plugin
|
|||
public string ProgressBarColor { get; set; } = "#26a0da";
|
||||
|
||||
/// <summary>
|
||||
/// Contains data used to populate the the preview section of this result.
|
||||
/// Contains data used to populate the preview section of this result.
|
||||
/// </summary>
|
||||
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
#pragma warning disable IDE0005
|
||||
using System.Windows;
|
||||
#pragma warning restore IDE0005
|
||||
|
|
@ -200,6 +201,24 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
}
|
||||
}
|
||||
|
||||
///<summary>
|
||||
/// This checks whether a given string is a zip file path.
|
||||
/// By default does not check if the zip file actually exist on disk, can do so by
|
||||
/// setting checkFileExists = true.
|
||||
///</summary>
|
||||
public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false)
|
||||
{
|
||||
if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip")
|
||||
{
|
||||
if (checkFileExists)
|
||||
return FileExists(querySearchString);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
///<summary>
|
||||
/// This checks whether a given string is a directory path or network location string.
|
||||
/// It does not check if location actually exists.
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -191,11 +191,15 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[TestCase(@"\c:\", false)]
|
||||
[TestCase(@"cc:\", false)]
|
||||
[TestCase(@"\\\SomeNetworkLocation\", false)]
|
||||
[TestCase(@"\\SomeNetworkLocation\", true)]
|
||||
[TestCase("RandomFile", false)]
|
||||
[TestCase(@"c:\>*", true)]
|
||||
[TestCase(@"c:\>", true)]
|
||||
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
|
||||
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
|
||||
[TestCase(@"c:\SomeLocation\SomeOtherLocation\SomeFile.exe", true)]
|
||||
[TestCase(@"\\SomeNetworkLocation\SomeFile.exe", true)]
|
||||
|
||||
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
|
||||
{
|
||||
// When, Given
|
||||
|
|
|
|||
|
|
@ -53,21 +53,14 @@ namespace Flow.Launcher.Test.Plugins
|
|||
|
||||
public static List<JsonRPCQueryResponseModel> ResponseModelsSource = new()
|
||||
{
|
||||
new()
|
||||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>()),
|
||||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>
|
||||
{
|
||||
Result = new()
|
||||
},
|
||||
new()
|
||||
{
|
||||
Result = new()
|
||||
new JsonRPCResult
|
||||
{
|
||||
new JsonRPCResult
|
||||
{
|
||||
Title = "Test1",
|
||||
SubTitle = "Test2"
|
||||
}
|
||||
Title = "Test1", SubTitle = "Test2"
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
|
||||
|
|
@ -94,4 +87,4 @@ namespace Flow.Launcher.Test.Plugins
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
Scripts\post_build.ps1 = Scripts\post_build.ps1
|
||||
README.md = README.md
|
||||
SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs
|
||||
Settings.XamlStyler = Settings.XamlStyler
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Shell", "Plugins\Flow.Launcher.Plugin.Shell\Flow.Launcher.Plugin.Shell.csproj", "{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
</ui:ThemeResources>
|
||||
<ui:XamlControlsResources />
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/CustomControlTemplate.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/SettingWindowStyle.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Themes/Win11System.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
|
|
|||
|
|
@ -52,15 +52,17 @@ namespace Flow.Launcher
|
|||
{
|
||||
_portable.PreStartCleanUpAfterPortabilityUpdate();
|
||||
|
||||
Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
||||
Log.Info(
|
||||
"|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
||||
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
|
||||
RegisterAppDomainExceptions();
|
||||
RegisterDispatcherUnhandledException();
|
||||
|
||||
ImageLoader.Initialize();
|
||||
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||
|
||||
_settingsVM = new SettingWindowViewModel(_updater, _portable);
|
||||
_settings = _settingsVM.Settings;
|
||||
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
|
|
@ -78,6 +80,8 @@ namespace Flow.Launcher
|
|||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
await PluginManager.InitializePluginsAsync(API);
|
||||
await imageLoadertask;
|
||||
|
||||
var window = new MainWindow(_settings, _mainVM);
|
||||
|
||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||
|
|
@ -85,12 +89,13 @@ namespace Flow.Launcher
|
|||
Current.MainWindow = window;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
HotKeyMapper.Initialize(_mainVM);
|
||||
|
||||
// todo temp fix for instance code logic
|
||||
// load plugin before change language, because plugin language also needs be changed
|
||||
InternationalizationManager.Instance.Settings = _settings;
|
||||
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
|
||||
|
||||
HotKeyMapper.Initialize(_mainVM);
|
||||
|
||||
// main windows needs initialized before theme change because of blur settings
|
||||
ThemeManager.Instance.Settings = _settings;
|
||||
ThemeManager.Instance.ChangeTheme(_settings.Theme);
|
||||
|
|
@ -103,7 +108,8 @@ namespace Flow.Launcher
|
|||
AutoUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
||||
Log.Info(
|
||||
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +128,8 @@ namespace Flow.Launcher
|
|||
// but if it fails (permissions, etc) then don't keep retrying
|
||||
// this also gives the user a visual indication in the Settings widget
|
||||
_settings.StartFlowLauncherOnSystemStartup = false;
|
||||
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message);
|
||||
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
|
||||
e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,53 +3,38 @@ using System.Globalization;
|
|||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
internal class BoolToIMEConversionModeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool v)
|
||||
{
|
||||
if (v)
|
||||
{
|
||||
return ImeConversionModeValues.Alphanumeric;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ImeConversionModeValues.DoNotCare;
|
||||
}
|
||||
}
|
||||
return ImeConversionModeValues.DoNotCare;
|
||||
}
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
internal class BoolToIMEConversionModeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
true => ImeConversionModeValues.Alphanumeric,
|
||||
_ => ImeConversionModeValues.DoNotCare
|
||||
};
|
||||
}
|
||||
|
||||
internal class BoolToIMEStateConverter : IValueConverter
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool v)
|
||||
{
|
||||
if (v)
|
||||
{
|
||||
return InputMethodState.Off;
|
||||
}
|
||||
else
|
||||
{
|
||||
return InputMethodState.DoNotCare;
|
||||
}
|
||||
}
|
||||
return InputMethodState.DoNotCare;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class BoolToIMEStateConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
true => InputMethodState.Off,
|
||||
_ => InputMethodState.DoNotCare
|
||||
};
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,70 +2,39 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||
return (value, parameter) switch
|
||||
{
|
||||
if (parameter != null)
|
||||
{
|
||||
if (value is true)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
(true, not null) => Visibility.Collapsed,
|
||||
(_, not null) => Visibility.Visible,
|
||||
|
||||
else
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (value is true)
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
else {
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
(true, null) => Visibility.Visible,
|
||||
(_, null) => Visibility.Collapsed
|
||||
};
|
||||
}
|
||||
|
||||
public class SplitterConverter : IValueConverter
|
||||
/* Prevents the dragging part of the preview area from working when preview is turned off. */
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (parameter != null)
|
||||
{
|
||||
if (value is true)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value is true)
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
}
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
}
|
||||
|
||||
public class SplitterConverter : IValueConverter
|
||||
/* Prevents the dragging part of the preview area from working when preview is turned off. */
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return (value, parameter) switch
|
||||
{
|
||||
(true, not null) => 0,
|
||||
(_, not null) => 5,
|
||||
|
||||
(true, null) => 5,
|
||||
(_, null) => 0
|
||||
};
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,46 +7,41 @@ using System.Windows.Shapes;
|
|||
|
||||
// For Clipping inside listbox item
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class BorderClipConverter : IMultiValueConverter
|
||||
{
|
||||
public class BorderClipConverter : IMultiValueConverter
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
if (values is not [double width, double height, CornerRadius radius])
|
||||
{
|
||||
if (values.Length == 3 && values[0] is double && values[1] is double && values[2] is CornerRadius)
|
||||
{
|
||||
var width = (double)values[0];
|
||||
var height = (double)values[1];
|
||||
Path myPath = new Path();
|
||||
if (width < Double.Epsilon || height < Double.Epsilon)
|
||||
{
|
||||
return Geometry.Empty;
|
||||
}
|
||||
var radius = (CornerRadius)values[2];
|
||||
var radiusHeight = radius.TopLeft;
|
||||
|
||||
// Drawing Round box for bottom round, and rect for top area of listbox.
|
||||
var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft);
|
||||
var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0);
|
||||
|
||||
GeometryGroup myGeometryGroup = new GeometryGroup();
|
||||
myGeometryGroup.Children.Add(corner);
|
||||
myGeometryGroup.Children.Add(box);
|
||||
|
||||
CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box);
|
||||
myPath.Data = c1;
|
||||
|
||||
myPath.Data.Freeze();
|
||||
return myPath.Data;
|
||||
}
|
||||
|
||||
return DependencyProperty.UnsetValue;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
Path myPath = new Path();
|
||||
if (width < Double.Epsilon || height < Double.Epsilon)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
return Geometry.Empty;
|
||||
}
|
||||
var radiusHeight = radius.TopLeft;
|
||||
|
||||
// Drawing Round box for bottom round, and rect for top area of listbox.
|
||||
var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft);
|
||||
var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0);
|
||||
|
||||
GeometryGroup myGeometryGroup = new GeometryGroup();
|
||||
myGeometryGroup.Children.Add(corner);
|
||||
myGeometryGroup.Children.Add(box);
|
||||
|
||||
CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box);
|
||||
myPath.Data = c1;
|
||||
|
||||
myPath.Data.Freeze();
|
||||
return myPath.Data;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,17 @@
|
|||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class DateTimeFormatToNowConverter : IValueConverter
|
||||
{
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is not string format ? null : DateTime.Now.ToString(format);
|
||||
}
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public class DateTimeFormatToNowConverter : IValueConverter
|
||||
{
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is not string format ? null : DateTime.Now.ToString(format);
|
||||
}
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,22 @@ using System.Globalization;
|
|||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class DiameterToCenterPointConverter : IValueConverter
|
||||
{
|
||||
public class DiameterToCenterPointConverter : IValueConverter
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
if (value is double d)
|
||||
{
|
||||
if (value is double d)
|
||||
{
|
||||
return new Point(d / 2, d / 2);
|
||||
}
|
||||
|
||||
return new Point(0, 0);
|
||||
return new Point(d / 2, d / 2);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
return new Point(0, 0);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,48 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class HighlightTextConverter : IMultiValueConverter
|
||||
{
|
||||
public class HighlightTextConverter : IMultiValueConverter
|
||||
public object Convert(object[] value, Type targetType, object parameter, CultureInfo cultureInfo)
|
||||
{
|
||||
public object Convert(object[] value, Type targetType, object parameter, CultureInfo cultureInfo)
|
||||
if (value.Length < 2)
|
||||
return new Run(string.Empty);
|
||||
|
||||
if (value[0] is not string text)
|
||||
return new Run(string.Empty);
|
||||
|
||||
if (value[1] is not List<int> { Count: > 0 } highlightData)
|
||||
// No highlight data, just return the text
|
||||
return new Run(text);
|
||||
|
||||
var highlightStyle = (Style)Application.Current.FindResource("HighlightStyle");
|
||||
var textBlock = new Span();
|
||||
|
||||
for (var i = 0; i < text.Length; i++)
|
||||
{
|
||||
var text = value[0] as string;
|
||||
var highlightData = value[1] as List<int>;
|
||||
|
||||
var textBlock = new Span();
|
||||
|
||||
if (highlightData == null || !highlightData.Any())
|
||||
var currentCharacter = text.Substring(i, 1);
|
||||
var run = new Run(currentCharacter)
|
||||
{
|
||||
// No highlight data, just return the text
|
||||
return new Run(text);
|
||||
}
|
||||
|
||||
for (var i = 0; i < text.Length; i++)
|
||||
{
|
||||
var currentCharacter = text.Substring(i, 1);
|
||||
if (this.ShouldHighlight(highlightData, i))
|
||||
{
|
||||
|
||||
textBlock.Inlines.Add(new Run(currentCharacter) { Style = (Style)Application.Current.FindResource("HighlightStyle") });
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
textBlock.Inlines.Add(new Run(currentCharacter));
|
||||
}
|
||||
}
|
||||
return textBlock;
|
||||
Style = ShouldHighlight(highlightData, i) ? highlightStyle : null
|
||||
};
|
||||
textBlock.Inlines.Add(run);
|
||||
}
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return new[] { DependencyProperty.UnsetValue, DependencyProperty.UnsetValue };
|
||||
}
|
||||
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return new[] { DependencyProperty.UnsetValue, DependencyProperty.UnsetValue };
|
||||
}
|
||||
|
||||
private bool ShouldHighlight(List<int> highlightData, int index)
|
||||
{
|
||||
return highlightData.Contains(index);
|
||||
}
|
||||
private bool ShouldHighlight(List<int> highlightData, int index)
|
||||
{
|
||||
return highlightData.Contains(index);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,25 +2,19 @@
|
|||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class IconRadiusConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values.Length != 2)
|
||||
throw new ArgumentException("IconRadiusConverter must have 2 parameters");
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
return values[1] switch
|
||||
{
|
||||
true => (double)values[0] / 2,
|
||||
false => (double)values[0],
|
||||
_ => throw new ArgumentException("The second argument should be boolean", nameof(values))
|
||||
};
|
||||
}
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
public class IconRadiusConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values is not [double size, bool isIconCircular])
|
||||
throw new ArgumentException("IconRadiusConverter must have 2 parameters: [double, bool]");
|
||||
|
||||
return isIconCircular ? size / 2 : size;
|
||||
}
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,24 +4,23 @@ using System.Windows;
|
|||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||
public class OpenResultHotkeyVisibilityConverter : IValueConverter
|
||||
{
|
||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||
public class OpenResultHotkeyVisibilityConverter : IValueConverter
|
||||
private const int MaxVisibleHotkeys = 10;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
private const int MaxVisibleHotkeys = 10;
|
||||
var number = int.MaxValue;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var number = int.MaxValue;
|
||||
if (value is ListBoxItem listBoxItem
|
||||
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
||||
number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
|
||||
if (value is ListBoxItem listBoxItem
|
||||
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
||||
number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
|
||||
return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class OrdinalConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ListBoxItem listBoxItem
|
||||
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
||||
{
|
||||
var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
return res == 10 ? 0 : res; // 10th item => HOTKEY+0
|
||||
}
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class OrdinalConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not ListBoxItem listBoxItem
|
||||
|| ItemsControl.ItemsControlFromItemContainer(listBoxItem) is not ListBox listBox)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
return res == 10 ? 0 : res; // 10th item => HOTKEY+0
|
||||
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,74 +6,62 @@ using System.Windows.Media;
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class QuerySuggestionBoxConverter : IMultiValueConverter
|
||||
{
|
||||
public class QuerySuggestionBoxConverter : IMultiValueConverter
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
// values[0] is TextBox: The textbox displaying the autocomplete suggestion
|
||||
// values[1] is ResultViewModel: Currently selected item in the list
|
||||
// values[2] is string: Query text
|
||||
if (
|
||||
values.Length != 3 ||
|
||||
values[0] is not TextBox queryTextBox ||
|
||||
values[1] is null ||
|
||||
values[2] is not string queryText ||
|
||||
string.IsNullOrEmpty(queryText)
|
||||
)
|
||||
return string.Empty;
|
||||
|
||||
if (values[1] is not ResultViewModel selectedItem)
|
||||
return Binding.DoNothing;
|
||||
|
||||
try
|
||||
{
|
||||
if (values.Length != 3)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
var QueryTextBox = values[0] as TextBox;
|
||||
var selectedResult = selectedItem.Result;
|
||||
var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " ";
|
||||
var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
|
||||
|
||||
var queryText = (string)values[2];
|
||||
|
||||
if (string.IsNullOrEmpty(queryText))
|
||||
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
|
||||
return string.Empty;
|
||||
|
||||
// second prop is the current selected item result
|
||||
var val = values[1];
|
||||
if (val == null)
|
||||
{
|
||||
|
||||
// For AutocompleteQueryCommand.
|
||||
// When user typed lower case and result title is uppercase, we still want to display suggestion
|
||||
selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length);
|
||||
|
||||
// Check if Text will be larger than our QueryTextBox
|
||||
Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch);
|
||||
// TODO: Obsolete warning?
|
||||
var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black);
|
||||
|
||||
var offset = queryTextBox.Padding.Right;
|
||||
|
||||
if (ft.Width + offset > queryTextBox.ActualWidth || queryTextBox.HorizontalOffset != 0)
|
||||
return string.Empty;
|
||||
}
|
||||
if (!(val is ResultViewModel))
|
||||
{
|
||||
return System.Windows.Data.Binding.DoNothing;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var selectedItem = (ResultViewModel)val;
|
||||
|
||||
var selectedResult = selectedItem.Result;
|
||||
var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " ";
|
||||
var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
|
||||
|
||||
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
|
||||
return string.Empty;
|
||||
|
||||
|
||||
// For AutocompleteQueryCommand.
|
||||
// When user typed lower case and result title is uppercase, we still want to display suggestion
|
||||
selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length);
|
||||
|
||||
// Check if Text will be larger then our QueryTextBox
|
||||
System.Windows.Media.Typeface typeface = new Typeface(QueryTextBox.FontFamily, QueryTextBox.FontStyle, QueryTextBox.FontWeight, QueryTextBox.FontStretch);
|
||||
// TODO: Obsolete warning?
|
||||
System.Windows.Media.FormattedText ft = new FormattedText(QueryTextBox.Text, System.Globalization.CultureInfo.DefaultThreadCurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, QueryTextBox.FontSize, Brushes.Black);
|
||||
|
||||
var offset = QueryTextBox.Padding.Right;
|
||||
|
||||
if ((ft.Width + offset) > QueryTextBox.ActualWidth || QueryTextBox.HorizontalOffset != 0)
|
||||
{
|
||||
return string.Empty;
|
||||
};
|
||||
|
||||
return selectedItem.QuerySuggestionText;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e);
|
||||
return string.Empty;
|
||||
}
|
||||
return selectedItem.QuerySuggestionText;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,30 +3,27 @@ using System.Globalization;
|
|||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
class StringToKeyBindingConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var mode = parameter as string;
|
||||
var hotkeyStr = value as string;
|
||||
var converter = new KeyGestureConverter();
|
||||
var key = (KeyGesture)converter.ConvertFromString(hotkeyStr);
|
||||
if (mode == "key")
|
||||
{
|
||||
return key.Key;
|
||||
}
|
||||
else if (mode == "modifiers")
|
||||
{
|
||||
return key.Modifiers;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
class StringToKeyBindingConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (parameter is not string mode || value is not string hotkeyStr)
|
||||
return null;
|
||||
|
||||
var converter = new KeyGestureConverter();
|
||||
var key = (KeyGesture)converter.ConvertFromString(hotkeyStr);
|
||||
return mode switch
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
"key" => key?.Key,
|
||||
"modifiers" => key?.Modifiers,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,29 +4,27 @@ using System.Windows.Data;
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class TextConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var ID = value.ToString();
|
||||
switch(ID)
|
||||
{
|
||||
case PluginStoreItemViewModel.NewRelease:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_NewRelease");
|
||||
case PluginStoreItemViewModel.RecentlyUpdated:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_RecentlyUpdated");
|
||||
case PluginStoreItemViewModel.None:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_None");
|
||||
case PluginStoreItemViewModel.Installed:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_Installed");
|
||||
default:
|
||||
return ID;
|
||||
}
|
||||
|
||||
}
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
public class TextConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var id = value?.ToString();
|
||||
var translationKey = id switch
|
||||
{
|
||||
PluginStoreItemViewModel.NewRelease => "pluginStore_NewRelease",
|
||||
PluginStoreItemViewModel.RecentlyUpdated => "pluginStore_RecentlyUpdated",
|
||||
PluginStoreItemViewModel.None => "pluginStore_None",
|
||||
PluginStoreItemViewModel.Installed => "pluginStore_Installed",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (translationKey is null)
|
||||
return id;
|
||||
|
||||
return InternationalizationManager.Instance.GetTranslation(translationKey);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
x:Class="Flow.Launcher.CustomQueryHotkeySetting"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
||||
Title="{DynamicResource customeQueryHotkeyTitle}"
|
||||
Width="530"
|
||||
Background="{DynamicResource PopuBGColor}"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
Foreground="{DynamicResource PopupTextColor}"
|
||||
Icon="Images\app.png"
|
||||
MouseDown="window_MouseDown"
|
||||
|
|
@ -60,94 +62,76 @@
|
|||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="26,0,26,0">
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,12">
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,0"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource customeQueryHotkeyTitle}"
|
||||
TextAlignment="Left" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customeQueryHotkeyTips}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<Image
|
||||
Width="478"
|
||||
Margin="0,20,0,0"
|
||||
Source="/Images/illustration_01.png" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,0,12"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource customeQueryHotkeyTitle}"
|
||||
TextAlignment="Left" />
|
||||
<TextBlock
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customeQueryHotkeyTips}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<Image
|
||||
Width="478"
|
||||
Margin="0,20,0,0"
|
||||
Source="/Images/illustration_01.png" />
|
||||
|
||||
<StackPanel Margin="0,20,0,0" Orientation="Horizontal">
|
||||
<Grid Width="478">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource hotkey}" />
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal">
|
||||
<flowlauncher:HotkeyControl
|
||||
x:Name="ctlHotkey"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Height="36"
|
||||
Margin="10,0,10,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource actionKeyword}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customQuery}" />
|
||||
<DockPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
LastChildFill="True">
|
||||
<Button
|
||||
x:Name="btnTestActionKeyword"
|
||||
Margin="0,0,10,0"
|
||||
Padding="10,5,10,5"
|
||||
Click="BtnTestActionKeyword_OnClick"
|
||||
Content="{DynamicResource preview}"
|
||||
DockPanel.Dock="Right" />
|
||||
<TextBox
|
||||
x:Name="tbAction"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Grid Width="478" Margin="0,20,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource hotkey}" />
|
||||
<flowlauncher:HotkeyControl
|
||||
x:Name="HotkeyControl"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="10,0,10,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
HotkeySettings="{Binding Settings}"
|
||||
DefaultHotkey="" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customQuery}" />
|
||||
<TextBox
|
||||
x:Name="tbAction"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center" />
|
||||
<Button
|
||||
x:Name="btnTestActionKeyword"
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,10,0"
|
||||
Padding="10,5,10,5"
|
||||
Click="BtnTestActionKeyword_OnClick"
|
||||
Content="{DynamicResource preview}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Border
|
||||
|
|
@ -174,4 +158,4 @@
|
|||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
|
|
@ -14,13 +15,13 @@ namespace Flow.Launcher
|
|||
private SettingWindow _settingWidow;
|
||||
private bool update;
|
||||
private CustomPluginHotkey updateCustomHotkey;
|
||||
private Settings _settings;
|
||||
public Settings Settings { get; }
|
||||
|
||||
public CustomQueryHotkeySetting(SettingWindow settingWidow, Settings settings)
|
||||
{
|
||||
_settingWidow = settingWidow;
|
||||
Settings = settings;
|
||||
InitializeComponent();
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
|
||||
|
|
@ -32,36 +33,21 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (!update)
|
||||
{
|
||||
if (!ctlHotkey.CurrentHotkeyAvailable)
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("hotkeyIsNotUnavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settings.CustomPluginHotkeys == null)
|
||||
{
|
||||
_settings.CustomPluginHotkeys = new ObservableCollection<CustomPluginHotkey>();
|
||||
}
|
||||
Settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
|
||||
|
||||
var pluginHotkey = new CustomPluginHotkey
|
||||
{
|
||||
Hotkey = ctlHotkey.CurrentHotkey.ToString(),
|
||||
ActionKeyword = tbAction.Text
|
||||
Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text
|
||||
};
|
||||
_settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||
Settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||
|
||||
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (updateCustomHotkey.Hotkey != ctlHotkey.CurrentHotkey.ToString() && !ctlHotkey.CurrentHotkeyAvailable)
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("hotkeyIsNotUnavailable"));
|
||||
return;
|
||||
}
|
||||
var oldHotkey = updateCustomHotkey.Hotkey;
|
||||
updateCustomHotkey.ActionKeyword = tbAction.Text;
|
||||
updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString();
|
||||
updateCustomHotkey.Hotkey = HotkeyControl.CurrentHotkey.ToString();
|
||||
//remove origin hotkey
|
||||
HotKeyMapper.RemoveHotkey(oldHotkey);
|
||||
HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
|
||||
|
|
@ -70,9 +56,11 @@ namespace Flow.Launcher
|
|||
Close();
|
||||
}
|
||||
|
||||
|
||||
public void UpdateItem(CustomPluginHotkey item)
|
||||
{
|
||||
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o => o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
||||
updateCustomHotkey = Settings.CustomPluginHotkeys.FirstOrDefault(o =>
|
||||
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
||||
if (updateCustomHotkey == null)
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
||||
|
|
@ -81,7 +69,7 @@ namespace Flow.Launcher
|
|||
}
|
||||
|
||||
tbAction.Text = updateCustomHotkey.ActionKeyword;
|
||||
_ = ctlHotkey.SetHotkeyAsync(updateCustomHotkey.Hotkey, false);
|
||||
HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false);
|
||||
update = true;
|
||||
lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update");
|
||||
}
|
||||
|
|
@ -101,12 +89,10 @@ namespace Flow.Launcher
|
|||
|
||||
private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */
|
||||
{
|
||||
TextBox textBox = Keyboard.FocusedElement as TextBox;
|
||||
if (textBox != null)
|
||||
{
|
||||
TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Next);
|
||||
textBox.MoveFocus(tRequest);
|
||||
}
|
||||
if (Keyboard.FocusedElement is not TextBox textBox) return;
|
||||
|
||||
TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Next);
|
||||
textBox.MoveFocus(tRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ namespace Flow.Launcher
|
|||
{
|
||||
public partial class CustomShortcutSetting : Window
|
||||
{
|
||||
private SettingWindowViewModel viewModel;
|
||||
public string Key { get; set; } = String.Empty;
|
||||
public string Value { get; set; } = String.Empty;
|
||||
private string originalKey { get; init; } = null;
|
||||
|
|
@ -17,13 +16,11 @@ namespace Flow.Launcher
|
|||
|
||||
public CustomShortcutSetting(SettingWindowViewModel vm)
|
||||
{
|
||||
viewModel = vm;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
|
||||
public CustomShortcutSetting(string key, string value)
|
||||
{
|
||||
viewModel = vm;
|
||||
Key = key;
|
||||
Value = value;
|
||||
originalKey = key;
|
||||
|
|
@ -46,8 +43,7 @@ namespace Flow.Launcher
|
|||
return;
|
||||
}
|
||||
// Check if key is modified or adding a new one
|
||||
if (((update && originalKey != Key) || !update)
|
||||
&& viewModel.ShortcutExists(Key))
|
||||
if ((update && originalKey != Key) || !update)
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="Fody" Version="6.5.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
<PackageReference Include="NHotkey.Wpf" Version="2.1.1" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="SharpVectors" Version="1.8.2" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.7" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -3,57 +3,56 @@ using Flow.Launcher.Infrastructure;
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public class AutoStartup
|
||||
{
|
||||
public class AutoStartup
|
||||
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
|
||||
|
||||
public static bool IsEnabled
|
||||
{
|
||||
private const string StartupPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run";
|
||||
|
||||
public static bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
var path = key?.GetValue(Constant.FlowLauncher) as string;
|
||||
return path == Constant.ExecutablePath;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Disable()
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||
var path = key?.GetValue(Constant.FlowLauncher) as string;
|
||||
return path == Constant.ExecutablePath;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}");
|
||||
throw;
|
||||
Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Enable()
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Disable()
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}");
|
||||
throw;
|
||||
}
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Enable()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,70 +4,69 @@ using System.Runtime.InteropServices;
|
|||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public class DwmDropShadow
|
||||
{
|
||||
public class DwmDropShadow
|
||||
|
||||
[DllImport("dwmapi.dll", PreserveSig = true)]
|
||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset);
|
||||
|
||||
/// <summary>
|
||||
/// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven).
|
||||
/// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
|
||||
/// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window).
|
||||
/// </summary>
|
||||
/// <param name="window">Window to which the shadow will be applied</param>
|
||||
public static void DropShadowToWindow(Window window)
|
||||
{
|
||||
|
||||
[DllImport("dwmapi.dll", PreserveSig = true)]
|
||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset);
|
||||
|
||||
/// <summary>
|
||||
/// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven).
|
||||
/// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
|
||||
/// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window).
|
||||
/// </summary>
|
||||
/// <param name="window">Window to which the shadow will be applied</param>
|
||||
public static void DropShadowToWindow(Window window)
|
||||
if (!DropShadow(window))
|
||||
{
|
||||
if (!DropShadow(window))
|
||||
{
|
||||
window.SourceInitialized += window_SourceInitialized;
|
||||
}
|
||||
window.SourceInitialized += window_SourceInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo
|
||||
private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo
|
||||
{
|
||||
Window window = (Window)sender;
|
||||
|
||||
DropShadow(window);
|
||||
|
||||
window.SourceInitialized -= window_SourceInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The actual method that makes API calls to drop the shadow to the window
|
||||
/// </summary>
|
||||
/// <param name="window">Window to which the shadow will be applied</param>
|
||||
/// <returns>True if the method succeeded, false if not</returns>
|
||||
private static bool DropShadow(Window window)
|
||||
{
|
||||
try
|
||||
{
|
||||
Window window = (Window)sender;
|
||||
WindowInteropHelper helper = new WindowInteropHelper(window);
|
||||
int val = 2;
|
||||
int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4);
|
||||
|
||||
DropShadow(window);
|
||||
|
||||
window.SourceInitialized -= window_SourceInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The actual method that makes API calls to drop the shadow to the window
|
||||
/// </summary>
|
||||
/// <param name="window">Window to which the shadow will be applied</param>
|
||||
/// <returns>True if the method succeeded, false if not</returns>
|
||||
private static bool DropShadow(Window window)
|
||||
{
|
||||
try
|
||||
if (ret1 == 0)
|
||||
{
|
||||
WindowInteropHelper helper = new WindowInteropHelper(window);
|
||||
int val = 2;
|
||||
int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4);
|
||||
|
||||
if (ret1 == 0)
|
||||
{
|
||||
Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 };
|
||||
int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m);
|
||||
return ret2 == 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 };
|
||||
int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m);
|
||||
return ret2 == 0;
|
||||
}
|
||||
catch (Exception)
|
||||
else
|
||||
{
|
||||
// Probably dwmapi.dll not found (incompatible OS)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception)
|
||||
{
|
||||
// Probably dwmapi.dll not found (incompatible OS)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,68 +2,67 @@
|
|||
using System.IO;
|
||||
using System.Net;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public class DataWebRequestFactory : IWebRequestCreate
|
||||
{
|
||||
public class DataWebRequestFactory : IWebRequestCreate
|
||||
class DataWebRequest : WebRequest
|
||||
{
|
||||
class DataWebRequest : WebRequest
|
||||
private readonly Uri _uri;
|
||||
|
||||
public DataWebRequest(Uri uri)
|
||||
{
|
||||
private readonly Uri m_uri;
|
||||
|
||||
public DataWebRequest(Uri uri)
|
||||
{
|
||||
m_uri = uri;
|
||||
}
|
||||
|
||||
public override WebResponse GetResponse()
|
||||
{
|
||||
return new DataWebResponse(m_uri);
|
||||
}
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
class DataWebResponse : WebResponse
|
||||
public override WebResponse GetResponse()
|
||||
{
|
||||
private readonly string m_contentType;
|
||||
private readonly byte[] m_data;
|
||||
|
||||
public DataWebResponse(Uri uri)
|
||||
{
|
||||
string uriString = uri.AbsoluteUri;
|
||||
|
||||
int commaIndex = uriString.IndexOf(',');
|
||||
var headers = uriString.Substring(0, commaIndex).Split(';');
|
||||
m_contentType = headers[0];
|
||||
string dataString = uriString.Substring(commaIndex + 1);
|
||||
m_data = Convert.FromBase64String(dataString);
|
||||
}
|
||||
|
||||
public override string ContentType
|
||||
{
|
||||
get { return m_contentType; }
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long ContentLength
|
||||
{
|
||||
get { return m_data.Length; }
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override Stream GetResponseStream()
|
||||
{
|
||||
return new MemoryStream(m_data);
|
||||
}
|
||||
}
|
||||
|
||||
public WebRequest Create(Uri uri)
|
||||
{
|
||||
return new DataWebRequest(uri);
|
||||
return new DataWebResponse(_uri);
|
||||
}
|
||||
}
|
||||
|
||||
class DataWebResponse : WebResponse
|
||||
{
|
||||
private readonly string _contentType;
|
||||
private readonly byte[] _data;
|
||||
|
||||
public DataWebResponse(Uri uri)
|
||||
{
|
||||
string uriString = uri.AbsoluteUri;
|
||||
|
||||
int commaIndex = uriString.IndexOf(',');
|
||||
var headers = uriString.Substring(0, commaIndex).Split(';');
|
||||
_contentType = headers[0];
|
||||
string dataString = uriString.Substring(commaIndex + 1);
|
||||
_data = Convert.FromBase64String(dataString);
|
||||
}
|
||||
|
||||
public override string ContentType
|
||||
{
|
||||
get { return _contentType; }
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long ContentLength
|
||||
{
|
||||
get { return _data.Length; }
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override Stream GetResponseStream()
|
||||
{
|
||||
return new MemoryStream(_data);
|
||||
}
|
||||
}
|
||||
|
||||
public WebRequest Create(Uri uri)
|
||||
{
|
||||
return new DataWebRequest(uri);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,45 +4,52 @@ using NLog;
|
|||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Exception;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public static class ErrorReporting
|
||||
{
|
||||
public static class ErrorReporting
|
||||
private static void Report(Exception e)
|
||||
{
|
||||
private static void Report(Exception e)
|
||||
{
|
||||
var logger = LogManager.GetLogger("UnHandledException");
|
||||
logger.Fatal(ExceptionFormatter.FormatExcpetion(e));
|
||||
var reportWindow = new ReportWindow(e);
|
||||
reportWindow.Show();
|
||||
}
|
||||
var logger = LogManager.GetLogger("UnHandledException");
|
||||
logger.Fatal(ExceptionFormatter.FormatExcpetion(e));
|
||||
var reportWindow = new ReportWindow(e);
|
||||
reportWindow.Show();
|
||||
}
|
||||
|
||||
public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
//handle non-ui thread exceptions
|
||||
Report((Exception)e.ExceptionObject);
|
||||
}
|
||||
public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
//handle non-ui thread exceptions
|
||||
Report((Exception)e.ExceptionObject);
|
||||
}
|
||||
|
||||
public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
//handle ui thread exceptions
|
||||
Report(e.Exception);
|
||||
//prevent application exist, so the user can copy prompted error info
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
public static string RuntimeInfo()
|
||||
{
|
||||
var info = $"\nFlow Launcher version: {Constant.Version}" +
|
||||
$"\nOS Version: {ExceptionFormatter.GetWindowsFullVersionFromRegistry()}" +
|
||||
$"\nIntPtr Length: {IntPtr.Size}" +
|
||||
$"\nx64: {Environment.Is64BitOperatingSystem}";
|
||||
return info;
|
||||
}
|
||||
public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
//handle ui thread exceptions
|
||||
Report(e.Exception);
|
||||
//prevent application exist, so the user can copy prompted error info
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
public static string DependenciesInfo()
|
||||
{
|
||||
var info = $"\nPython Path: {Constant.PythonPath}\nNode Path: {Constant.NodePath}";
|
||||
return info;
|
||||
}
|
||||
public static string RuntimeInfo()
|
||||
{
|
||||
var info =
|
||||
$"""
|
||||
|
||||
Flow Launcher version: {Constant.Version}
|
||||
OS Version: {ExceptionFormatter.GetWindowsFullVersionFromRegistry()}
|
||||
IntPtr Length: {IntPtr.Size}
|
||||
x64: {Environment.Is64BitOperatingSystem}
|
||||
""";
|
||||
return info;
|
||||
}
|
||||
|
||||
public static string DependenciesInfo()
|
||||
{
|
||||
var info = $"""
|
||||
|
||||
Python Path: {Constant.PythonPath}
|
||||
Node Path: {Constant.NodePath}
|
||||
""";
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,97 +7,96 @@ using Flow.Launcher.Core.Resource;
|
|||
using System.Windows;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
internal static class HotKeyMapper
|
||||
{
|
||||
internal static class HotKeyMapper
|
||||
private static Settings _settings;
|
||||
private static MainViewModel _mainViewModel;
|
||||
|
||||
internal static void Initialize(MainViewModel mainVM)
|
||||
{
|
||||
private static Settings settings;
|
||||
private static MainViewModel mainViewModel;
|
||||
_mainViewModel = mainVM;
|
||||
_settings = _mainViewModel.Settings;
|
||||
|
||||
internal static void Initialize(MainViewModel mainVM)
|
||||
SetHotkey(_settings.Hotkey, OnToggleHotkey);
|
||||
LoadCustomPluginHotkey();
|
||||
}
|
||||
|
||||
internal static void OnToggleHotkey(object sender, HotkeyEventArgs args)
|
||||
{
|
||||
if (!_mainViewModel.ShouldIgnoreHotkeys())
|
||||
_mainViewModel.ToggleFlowLauncher();
|
||||
}
|
||||
|
||||
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
var hotkey = new HotkeyModel(hotkeyStr);
|
||||
SetHotkey(hotkey, action);
|
||||
}
|
||||
|
||||
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
string hotkeyStr = hotkey.ToString();
|
||||
try
|
||||
{
|
||||
mainViewModel = mainVM;
|
||||
settings = mainViewModel.Settings;
|
||||
|
||||
SetHotkey(settings.Hotkey, OnToggleHotkey);
|
||||
LoadCustomPluginHotkey();
|
||||
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
||||
}
|
||||
|
||||
internal static void OnToggleHotkey(object sender, HotkeyEventArgs args)
|
||||
catch (Exception)
|
||||
{
|
||||
if (!mainViewModel.ShouldIgnoreHotkeys())
|
||||
mainViewModel.ToggleFlowLauncher();
|
||||
}
|
||||
|
||||
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
var hotkey = new HotkeyModel(hotkeyStr);
|
||||
SetHotkey(hotkey, action);
|
||||
}
|
||||
|
||||
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
string hotkeyStr = hotkey.ToString();
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||
MessageBox.Show(errorMsg,errorMsgTitle);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RemoveHotkey(string hotkeyStr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyStr);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void LoadCustomPluginHotkey()
|
||||
{
|
||||
if (settings.CustomPluginHotkeys == null)
|
||||
return;
|
||||
|
||||
foreach (CustomPluginHotkey hotkey in settings.CustomPluginHotkeys)
|
||||
{
|
||||
SetCustomQueryHotkey(hotkey);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
|
||||
{
|
||||
SetHotkey(hotkey.Hotkey, (s, e) =>
|
||||
{
|
||||
if (mainViewModel.ShouldIgnoreHotkeys())
|
||||
return;
|
||||
|
||||
mainViewModel.Show();
|
||||
mainViewModel.ChangeQueryText(hotkey.ActionKeyword, true);
|
||||
});
|
||||
}
|
||||
|
||||
internal static bool CheckAvailability(HotkeyModel currentHotkey)
|
||||
{
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { });
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
|
||||
}
|
||||
|
||||
return false;
|
||||
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||
MessageBox.Show(errorMsg,errorMsgTitle);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RemoveHotkey(string hotkeyStr)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyStr);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void LoadCustomPluginHotkey()
|
||||
{
|
||||
if (_settings.CustomPluginHotkeys == null)
|
||||
return;
|
||||
|
||||
foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys)
|
||||
{
|
||||
SetCustomQueryHotkey(hotkey);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
|
||||
{
|
||||
SetHotkey(hotkey.Hotkey, (s, e) =>
|
||||
{
|
||||
if (_mainViewModel.ShouldIgnoreHotkeys())
|
||||
return;
|
||||
|
||||
_mainViewModel.Show();
|
||||
_mainViewModel.ChangeQueryText(hotkey.ActionKeyword, true);
|
||||
});
|
||||
}
|
||||
|
||||
internal static bool CheckAvailability(HotkeyModel currentHotkey)
|
||||
{
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { });
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,26 @@
|
|||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
{
|
||||
public static class SingletonWindowOpener
|
||||
{
|
||||
public static T Open<T>(params object[] args) where T : Window
|
||||
{
|
||||
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
||||
?? (T)Activator.CreateInstance(typeof(T), args);
|
||||
|
||||
// Fix UI bug
|
||||
// Add `window.WindowState = WindowState.Normal`
|
||||
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
||||
// Not sure why this works tho
|
||||
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
||||
// https://stackoverflow.com/a/59719760/4230390
|
||||
window.WindowState = WindowState.Normal;
|
||||
window.Show();
|
||||
|
||||
window.Focus();
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
return (T)window;
|
||||
}
|
||||
public static class SingletonWindowOpener
|
||||
{
|
||||
public static T Open<T>(params object[] args) where T : Window
|
||||
{
|
||||
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
||||
?? (T)Activator.CreateInstance(typeof(T), args);
|
||||
|
||||
// Fix UI bug
|
||||
// Add `window.WindowState = WindowState.Normal`
|
||||
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
||||
// Not sure why this works tho
|
||||
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
||||
// https://stackoverflow.com/a/59719760/4230390
|
||||
window.WindowState = WindowState.Normal;
|
||||
window.Show();
|
||||
|
||||
window.Focus();
|
||||
|
||||
return (T)window;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
{
|
||||
public static class SyntaxSugars
|
||||
{
|
||||
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback)
|
||||
{
|
||||
return CallOrRescueDefault(callback, default(TResult));
|
||||
}
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback, TResult def)
|
||||
public static class SyntaxSugars
|
||||
{
|
||||
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback)
|
||||
{
|
||||
return CallOrRescueDefault(callback, default(TResult));
|
||||
}
|
||||
|
||||
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback, TResult def)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
return callback();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return def;
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,44 +5,43 @@ using System.Text;
|
|||
using System.Windows.Media;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public static class WallpaperPathRetrieval
|
||||
{
|
||||
public static class WallpaperPathRetrieval
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern Int32 SystemParametersInfo(UInt32 action,
|
||||
Int32 uParam, StringBuilder vParam, UInt32 winIni);
|
||||
private static readonly UInt32 SPI_GETDESKWALLPAPER = 0x73;
|
||||
private static int MAX_PATH = 260;
|
||||
|
||||
public static string GetWallpaperPath()
|
||||
{
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern Int32 SystemParametersInfo(UInt32 action,
|
||||
Int32 uParam, StringBuilder vParam, UInt32 winIni);
|
||||
private static readonly UInt32 SPI_GETDESKWALLPAPER = 0x73;
|
||||
private static int MAX_PATH = 260;
|
||||
var wallpaper = new StringBuilder(MAX_PATH);
|
||||
SystemParametersInfo(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaper, 0);
|
||||
|
||||
public static string GetWallpaperPath()
|
||||
var str = wallpaper.ToString();
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return null;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
public static Color GetWallpaperColor()
|
||||
{
|
||||
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true);
|
||||
var result = key?.GetValue("Background", null);
|
||||
if (result is string strResult)
|
||||
{
|
||||
var wallpaper = new StringBuilder(MAX_PATH);
|
||||
SystemParametersInfo(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaper, 0);
|
||||
|
||||
var str = wallpaper.ToString();
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return null;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
public static Color GetWallpaperColor()
|
||||
{
|
||||
RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Colors", true);
|
||||
var result = key.GetValue(@"Background", null);
|
||||
if (result != null && result is string)
|
||||
try
|
||||
{
|
||||
var parts = strResult.Trim().Split(new[] {' '}, 3).Select(byte.Parse).ToList();
|
||||
return Color.FromRgb(parts[0], parts[1], parts[2]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = result.ToString().Trim().Split(new[] {' '}, 3).Select(byte.Parse).ToList();
|
||||
return Color.FromRgb(parts[0], parts[1], parts[2]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
return Colors.Transparent;
|
||||
}
|
||||
return Colors.Transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,156 +8,152 @@ using System.Windows.Interop;
|
|||
using System.Windows.Media;
|
||||
using Point = System.Windows.Point;
|
||||
|
||||
namespace Flow.Launcher.Helper
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public class WindowsInteropHelper
|
||||
{
|
||||
public class WindowsInteropHelper
|
||||
private const int GWL_STYLE = -16; //WPF's Message code for Title Bar's Style
|
||||
private const int WS_SYSMENU = 0x80000; //WPF's Message code for System Menu
|
||||
private static IntPtr _hwnd_shell;
|
||||
private static IntPtr _hwnd_desktop;
|
||||
|
||||
//Accessors for shell and desktop handlers
|
||||
//Will set the variables once and then will return them
|
||||
private static IntPtr HWND_SHELL
|
||||
{
|
||||
private const int GWL_STYLE = -16; //WPF's Message code for Title Bar's Style
|
||||
private const int WS_SYSMENU = 0x80000; //WPF's Message code for System Menu
|
||||
private static IntPtr _hwnd_shell;
|
||||
private static IntPtr _hwnd_desktop;
|
||||
|
||||
//Accessors for shell and desktop handlers
|
||||
//Will set the variables once and then will return them
|
||||
private static IntPtr HWND_SHELL
|
||||
get
|
||||
{
|
||||
get
|
||||
{
|
||||
return _hwnd_shell != IntPtr.Zero ? _hwnd_shell : _hwnd_shell = GetShellWindow();
|
||||
}
|
||||
return _hwnd_shell != IntPtr.Zero ? _hwnd_shell : _hwnd_shell = GetShellWindow();
|
||||
}
|
||||
private static IntPtr HWND_DESKTOP
|
||||
}
|
||||
private static IntPtr HWND_DESKTOP
|
||||
{
|
||||
get
|
||||
{
|
||||
get
|
||||
{
|
||||
return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow();
|
||||
}
|
||||
return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow();
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetDesktopWindow();
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetShellWindow();
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetDesktopWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern int GetWindowRect(IntPtr hwnd, out RECT rc);
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetShellWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern int GetWindowRect(IntPtr hwnd, out RECT rc);
|
||||
|
||||
[DllImport("user32.DLL")]
|
||||
public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
||||
|
||||
[DllImport("user32.DLL")]
|
||||
public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
|
||||
|
||||
|
||||
const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
|
||||
const string WINDOW_CLASS_WINTAB = "Flip3D";
|
||||
const string WINDOW_CLASS_PROGMAN = "Progman";
|
||||
const string WINDOW_CLASS_WORKERW = "WorkerW";
|
||||
const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
|
||||
const string WINDOW_CLASS_WINTAB = "Flip3D";
|
||||
const string WINDOW_CLASS_PROGMAN = "Progman";
|
||||
const string WINDOW_CLASS_WORKERW = "WorkerW";
|
||||
|
||||
public static bool IsWindowFullscreen()
|
||||
public static bool IsWindowFullscreen()
|
||||
{
|
||||
//get current active window
|
||||
IntPtr hWnd = GetForegroundWindow();
|
||||
|
||||
if (hWnd.Equals(IntPtr.Zero))
|
||||
{
|
||||
//get current active window
|
||||
IntPtr hWnd = GetForegroundWindow();
|
||||
|
||||
if (!hWnd.Equals(IntPtr.Zero))
|
||||
{
|
||||
//if current active window is NOT desktop or shell
|
||||
if (!(hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)))
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
GetClassName(hWnd, sb, sb.Capacity);
|
||||
string windowClass = sb.ToString();
|
||||
|
||||
//for Win+Tab (Flip3D)
|
||||
if (windowClass == WINDOW_CLASS_WINTAB)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT appBounds;
|
||||
GetWindowRect(hWnd, out appBounds);
|
||||
|
||||
//for console (ConsoleWindowClass), we have to check for negative dimensions
|
||||
if (windowClass == WINDOW_CLASS_CONSOLE)
|
||||
{
|
||||
return appBounds.Top < 0 && appBounds.Bottom < 0;
|
||||
}
|
||||
|
||||
//for desktop (Progman or WorkerW, depends on the system), we have to check
|
||||
if (windowClass == WINDOW_CLASS_PROGMAN || windowClass == WINDOW_CLASS_WORKERW)
|
||||
{
|
||||
IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||
hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView");
|
||||
if (!hWndDesktop.Equals(IntPtr.Zero))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds;
|
||||
if ((appBounds.Bottom - appBounds.Top) == screenBounds.Height && (appBounds.Right - appBounds.Left) == screenBounds.Width)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// disable windows toolbar's control box
|
||||
/// this will also disable system menu with Alt+Space hotkey
|
||||
/// </summary>
|
||||
public static void DisableControlBox(Window win)
|
||||
//if current active window is desktop or shell, exit early
|
||||
if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(win).Handle;
|
||||
SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms pixels to Device Independent Pixels used by WPF
|
||||
/// </summary>
|
||||
/// <param name="visual">current window, required to get presentation source</param>
|
||||
/// <param name="unitX">horizontal position in pixels</param>
|
||||
/// <param name="unitY">vertical position in pixels</param>
|
||||
/// <returns>point containing device independent pixels</returns>
|
||||
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
GetClassName(hWnd, sb, sb.Capacity);
|
||||
string windowClass = sb.ToString();
|
||||
|
||||
//for Win+Tab (Flip3D)
|
||||
if (windowClass == WINDOW_CLASS_WINTAB)
|
||||
{
|
||||
Matrix matrix;
|
||||
var source = PresentationSource.FromVisual(visual);
|
||||
if (source != null)
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT appBounds;
|
||||
GetWindowRect(hWnd, out appBounds);
|
||||
|
||||
//for console (ConsoleWindowClass), we have to check for negative dimensions
|
||||
if (windowClass == WINDOW_CLASS_CONSOLE)
|
||||
{
|
||||
return appBounds.Top < 0 && appBounds.Bottom < 0;
|
||||
}
|
||||
|
||||
//for desktop (Progman or WorkerW, depends on the system), we have to check
|
||||
if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW)
|
||||
{
|
||||
IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||
hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView");
|
||||
if (!hWndDesktop.Equals(IntPtr.Zero))
|
||||
{
|
||||
matrix = source.CompositionTarget.TransformFromDevice;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var src = new HwndSource(new HwndSourceParameters()))
|
||||
{
|
||||
matrix = src.CompositionTarget.TransformFromDevice;
|
||||
}
|
||||
}
|
||||
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
|
||||
}
|
||||
|
||||
Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds;
|
||||
return (appBounds.Bottom - appBounds.Top) == screenBounds.Height && (appBounds.Right - appBounds.Left) == screenBounds.Width;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT
|
||||
/// <summary>
|
||||
/// disable windows toolbar's control box
|
||||
/// this will also disable system menu with Alt+Space hotkey
|
||||
/// </summary>
|
||||
public static void DisableControlBox(Window win)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(win).Handle;
|
||||
SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms pixels to Device Independent Pixels used by WPF
|
||||
/// </summary>
|
||||
/// <param name="visual">current window, required to get presentation source</param>
|
||||
/// <param name="unitX">horizontal position in pixels</param>
|
||||
/// <param name="unitY">vertical position in pixels</param>
|
||||
/// <returns>point containing device independent pixels</returns>
|
||||
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
|
||||
{
|
||||
Matrix matrix;
|
||||
var source = PresentationSource.FromVisual(visual);
|
||||
if (source is not null)
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
matrix = source.CompositionTarget.TransformFromDevice;
|
||||
}
|
||||
else
|
||||
{
|
||||
using var src = new HwndSource(new HwndSourceParameters());
|
||||
matrix = src.CompositionTarget.TransformFromDevice;
|
||||
}
|
||||
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
|
||||
}
|
||||
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs
Normal file
11
Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.Win32;
|
||||
|
||||
namespace Flow.Launcher.Helper;
|
||||
internal static class WindowsMediaPlayerHelper
|
||||
{
|
||||
internal static bool IsWindowsMediaPlayerInstalled()
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MediaPlayer");
|
||||
return key.GetValue("Installation Directory") != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,54 +3,75 @@
|
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:input="clr-namespace:System.Windows.Input;assembly=PresentationCore"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Height="24"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="300"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Popup
|
||||
x:Name="popup"
|
||||
AllowDrop="True"
|
||||
AllowsTransparency="True"
|
||||
IsOpen="{Binding IsKeyboardFocused, ElementName=tbHotkey, Mode=OneWay}"
|
||||
Placement="Top"
|
||||
PlacementTarget="{Binding ElementName=tbHotkey}"
|
||||
PopupAnimation="Fade"
|
||||
StaysOpen="True"
|
||||
VerticalOffset="-5">
|
||||
<Border
|
||||
Width="140"
|
||||
Height="30"
|
||||
Background="{DynamicResource Color01B}"
|
||||
BorderBrush="{DynamicResource Color21B}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
x:Name="tbMsg"
|
||||
Margin="0,0,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Color05B}"
|
||||
Text="{DynamicResource flowlauncherPressHotkey}"
|
||||
Visibility="Visible" />
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
<TextBox
|
||||
x:Name="tbHotkey"
|
||||
Margin="0,0,18,0"
|
||||
VerticalContentAlignment="Center"
|
||||
input:InputMethod.IsInputMethodEnabled="False"
|
||||
GotFocus="tbHotkey_GotFocus"
|
||||
LostFocus="tbHotkey_LostFocus"
|
||||
PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
|
||||
TabIndex="100" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
<Button
|
||||
Width="Auto"
|
||||
FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Color01B}"
|
||||
Click="GetNewHotkey">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="ButtonBorder"
|
||||
Padding="5,0,5,0"
|
||||
Background="{DynamicResource ButtonBackgroundColor}"
|
||||
BorderBrush="{DynamicResource ButtonInsideBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
<Condition Property="IsPressed" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="ButtonBorder" Property="Background"
|
||||
Value="{DynamicResource ButtonMousePressed}" />
|
||||
<Setter TargetName="ButtonBorder" Property="BorderBrush"
|
||||
Value="{DynamicResource ButtonMousePressedInsideBorder}" />
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="ButtonBorder" Property="Background"
|
||||
Value="{DynamicResource ButtonMouseOver}" />
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsPressed" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="ButtonBorder" Property="Background"
|
||||
Value="{DynamicResource ButtonMousePressed}" />
|
||||
<Setter TargetName="ButtonBorder" Property="BorderBrush"
|
||||
Value="{DynamicResource CustomContextHover}" />
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Button.Content>
|
||||
<ItemsControl x:Name="HotkeyList">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Margin="2,5,2,5"
|
||||
Padding="10,5,10,5"
|
||||
Background="{DynamicResource AccentButtonBackground}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<TextBlock Text="{Binding}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Button.Content>
|
||||
</Button>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -1,141 +1,204 @@
|
|||
using System;
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Threading;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class HotkeyControl : UserControl
|
||||
public partial class HotkeyControl
|
||||
{
|
||||
public HotkeyModel CurrentHotkey { get; private set; }
|
||||
public bool CurrentHotkeyAvailable { get; private set; }
|
||||
public IHotkeySettings HotkeySettings {
|
||||
get { return (IHotkeySettings)GetValue(HotkeySettingsProperty); }
|
||||
set { SetValue(HotkeySettingsProperty, value); }
|
||||
}
|
||||
|
||||
public event EventHandler HotkeyChanged;
|
||||
public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register(
|
||||
nameof(HotkeySettings),
|
||||
typeof(IHotkeySettings),
|
||||
typeof(HotkeyControl),
|
||||
new PropertyMetadata()
|
||||
);
|
||||
public string WindowTitle {
|
||||
get { return (string)GetValue(WindowTitleProperty); }
|
||||
set { SetValue(WindowTitleProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty WindowTitleProperty = DependencyProperty.Register(
|
||||
nameof(WindowTitle),
|
||||
typeof(string),
|
||||
typeof(HotkeyControl),
|
||||
new PropertyMetadata(string.Empty)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Designed for Preview Hotkey and KeyGesture.
|
||||
/// </summary>
|
||||
public bool ValidateKeyGesture { get; set; } = false;
|
||||
public static readonly DependencyProperty ValidateKeyGestureProperty = DependencyProperty.Register(
|
||||
nameof(ValidateKeyGesture),
|
||||
typeof(bool),
|
||||
typeof(HotkeyControl),
|
||||
new PropertyMetadata(default(bool))
|
||||
);
|
||||
|
||||
protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public HotkeyControl()
|
||||
public bool ValidateKeyGesture
|
||||
{
|
||||
InitializeComponent();
|
||||
get { return (bool)GetValue(ValidateKeyGestureProperty); }
|
||||
set { SetValue(ValidateKeyGestureProperty, value); }
|
||||
}
|
||||
|
||||
private CancellationTokenSource hotkeyUpdateSource;
|
||||
public static readonly DependencyProperty DefaultHotkeyProperty = DependencyProperty.Register(
|
||||
nameof(DefaultHotkey),
|
||||
typeof(string),
|
||||
typeof(HotkeyControl),
|
||||
new PropertyMetadata(default(string))
|
||||
);
|
||||
|
||||
private void TbHotkey_OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
public string DefaultHotkey
|
||||
{
|
||||
hotkeyUpdateSource?.Cancel();
|
||||
hotkeyUpdateSource?.Dispose();
|
||||
hotkeyUpdateSource = new();
|
||||
var token = hotkeyUpdateSource.Token;
|
||||
e.Handled = true;
|
||||
get { return (string)GetValue(DefaultHotkeyProperty); }
|
||||
set { SetValue(DefaultHotkeyProperty, value); }
|
||||
}
|
||||
|
||||
//when alt is pressed, the real key should be e.SystemKey
|
||||
Key key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||
|
||||
SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers();
|
||||
|
||||
var hotkeyModel = new HotkeyModel(
|
||||
specialKeyState.AltPressed,
|
||||
specialKeyState.ShiftPressed,
|
||||
specialKeyState.WinPressed,
|
||||
specialKeyState.CtrlPressed,
|
||||
key);
|
||||
|
||||
if (hotkeyModel.Equals(CurrentHotkey))
|
||||
private static void OnHotkeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not HotkeyControl hotkeyControl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
await Task.Delay(500, token);
|
||||
if (!token.IsCancellationRequested)
|
||||
await SetHotkeyAsync(hotkeyModel);
|
||||
});
|
||||
hotkeyControl.SetKeysToDisplay(new HotkeyModel(hotkeyControl.Hotkey));
|
||||
hotkeyControl.CurrentHotkey = new HotkeyModel(hotkeyControl.Hotkey);
|
||||
}
|
||||
|
||||
public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true)
|
||||
{
|
||||
tbHotkey.Text = keyModel.ToString();
|
||||
tbHotkey.Select(tbHotkey.Text.Length, 0);
|
||||
|
||||
public static readonly DependencyProperty ChangeHotkeyProperty = DependencyProperty.Register(
|
||||
nameof(ChangeHotkey),
|
||||
typeof(ICommand),
|
||||
typeof(HotkeyControl),
|
||||
new PropertyMetadata(default(ICommand))
|
||||
);
|
||||
|
||||
public ICommand? ChangeHotkey
|
||||
{
|
||||
get { return (ICommand)GetValue(ChangeHotkeyProperty); }
|
||||
set { SetValue(ChangeHotkeyProperty, value); }
|
||||
}
|
||||
|
||||
|
||||
public static readonly DependencyProperty HotkeyProperty = DependencyProperty.Register(
|
||||
nameof(Hotkey),
|
||||
typeof(string),
|
||||
typeof(HotkeyControl),
|
||||
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged)
|
||||
);
|
||||
|
||||
public string Hotkey
|
||||
{
|
||||
get { return (string)GetValue(HotkeyProperty); }
|
||||
set { SetValue(HotkeyProperty, value); }
|
||||
}
|
||||
|
||||
public HotkeyControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
HotkeyList.ItemsSource = KeysToDisplay;
|
||||
SetKeysToDisplay(CurrentHotkey);
|
||||
}
|
||||
|
||||
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
|
||||
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
|
||||
|
||||
public string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none");
|
||||
|
||||
public ObservableCollection<string> KeysToDisplay { get; set; } = new();
|
||||
|
||||
public HotkeyModel CurrentHotkey { get; private set; } = new(false, false, false, false, Key.None);
|
||||
|
||||
|
||||
public void GetNewHotkey(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OpenHotkeyDialog();
|
||||
}
|
||||
|
||||
private async Task OpenHotkeyDialog()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Hotkey))
|
||||
{
|
||||
HotKeyMapper.RemoveHotkey(Hotkey);
|
||||
}
|
||||
|
||||
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, HotkeySettings, WindowTitle);
|
||||
await dialog.ShowAsync();
|
||||
switch (dialog.ResultType)
|
||||
{
|
||||
case HotkeyControlDialog.EResultType.Cancel:
|
||||
SetHotkey(Hotkey);
|
||||
return;
|
||||
case HotkeyControlDialog.EResultType.Save:
|
||||
SetHotkey(dialog.ResultValue);
|
||||
break;
|
||||
case HotkeyControlDialog.EResultType.Delete:
|
||||
Delete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true)
|
||||
{
|
||||
if (triggerValidate)
|
||||
{
|
||||
bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture);
|
||||
CurrentHotkeyAvailable = hotkeyAvailable;
|
||||
SetMessage(hotkeyAvailable);
|
||||
OnHotkeyChanged();
|
||||
|
||||
var token = hotkeyUpdateSource.Token;
|
||||
await Task.Delay(500, token);
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (CurrentHotkeyAvailable)
|
||||
if (!hotkeyAvailable)
|
||||
{
|
||||
CurrentHotkey = keyModel;
|
||||
// To trigger LostFocus
|
||||
FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null);
|
||||
Keyboard.ClearFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
Hotkey = keyModel.ToString();
|
||||
SetKeysToDisplay(CurrentHotkey);
|
||||
ChangeHotkey?.Execute(keyModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentHotkey = keyModel;
|
||||
Hotkey = keyModel.ToString();
|
||||
ChangeHotkey?.Execute(keyModel);
|
||||
}
|
||||
}
|
||||
|
||||
public Task SetHotkeyAsync(string keyStr, bool triggerValidate = true)
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate);
|
||||
if (!string.IsNullOrEmpty(Hotkey))
|
||||
HotKeyMapper.RemoveHotkey(Hotkey);
|
||||
Hotkey = "";
|
||||
SetKeysToDisplay(new HotkeyModel(false, false, false, false, Key.None));
|
||||
}
|
||||
|
||||
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
|
||||
|
||||
public new bool IsFocused => tbHotkey.IsFocused;
|
||||
|
||||
private void tbHotkey_LostFocus(object sender, RoutedEventArgs e)
|
||||
private void SetKeysToDisplay(HotkeyModel? hotkey)
|
||||
{
|
||||
tbHotkey.Text = CurrentHotkey?.ToString() ?? "";
|
||||
tbHotkey.Select(tbHotkey.Text.Length, 0);
|
||||
}
|
||||
KeysToDisplay.Clear();
|
||||
|
||||
private void tbHotkey_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ResetMessage();
|
||||
}
|
||||
|
||||
private void ResetMessage()
|
||||
{
|
||||
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("flowlauncherPressHotkey");
|
||||
tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B");
|
||||
}
|
||||
|
||||
private void SetMessage(bool hotkeyAvailable)
|
||||
{
|
||||
if (!hotkeyAvailable)
|
||||
if (hotkey == null || hotkey == default(HotkeyModel))
|
||||
{
|
||||
tbMsg.Foreground = new SolidColorBrush(Colors.Red);
|
||||
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable");
|
||||
KeysToDisplay.Add(EmptyHotkey);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
foreach (var key in hotkey.Value.EnumerateDisplayKeys()!)
|
||||
{
|
||||
tbMsg.Foreground = new SolidColorBrush(Colors.Green);
|
||||
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success");
|
||||
KeysToDisplay.Add(key);
|
||||
}
|
||||
tbMsg.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
public void SetHotkey(string? keyStr, bool triggerValidate = true)
|
||||
{
|
||||
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
169
Flow.Launcher/HotkeyControlDialog.xaml
Normal file
169
Flow.Launcher/HotkeyControlDialog.xaml
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<ui:ContentDialog
|
||||
x:Class="Flow.Launcher.HotkeyControlDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
Background="{DynamicResource PopuBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="8"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
Foreground="{DynamicResource PopupTextColor}"
|
||||
PreviewKeyDown="OnPreviewKeyDown"
|
||||
Style="{DynamicResource ContentDialog}">
|
||||
<ui:ContentDialog.Resources>
|
||||
<Thickness x:Key="ContentDialogPadding">0</Thickness>
|
||||
<Thickness x:Key="ContentDialogTitleMargin">0</Thickness>
|
||||
</ui:ContentDialog.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="100" />
|
||||
<RowDefinition Height="80" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Window title and the keys in the hotkey -->
|
||||
<Grid Grid.Row="0" Margin="26,12,26,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,0,0"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding WindowTitle}"
|
||||
TextAlignment="Left" />
|
||||
<TextBlock FontSize="14" Text="{DynamicResource hotkeyRegGuide}" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Width="450"
|
||||
Height="100"
|
||||
Margin="0,100,0,0"
|
||||
Padding="26,12,26,0">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ItemsControl ItemsSource="{Binding KeysToDisplay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
MinWidth="50"
|
||||
MinHeight="50"
|
||||
Margin="5,0,5,0"
|
||||
Padding="8"
|
||||
Background="{DynamicResource AccentButtonBackground}"
|
||||
CornerRadius="6">
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource Color01B}"
|
||||
Text="{Binding}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Warning message for when something went wrong with the new hotkey. -->
|
||||
<Border Grid.Row="1">
|
||||
|
||||
<Border
|
||||
x:Name="Alert"
|
||||
Width="420"
|
||||
Padding="0, 10"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Background="{DynamicResource InfoBarWarningBG}"
|
||||
BorderBrush="{DynamicResource InfoBarBD}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Visibility="Collapsed">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ui:FontIcon
|
||||
Grid.Column="0"
|
||||
Margin="20,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="15"
|
||||
Foreground="{DynamicResource InfoBarWarningIcon}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
x:Name="tbMsg"
|
||||
Margin="0,0,0,2"
|
||||
Padding="0,0,8,0"
|
||||
HorizontalAlignment="Left"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Color05B}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Border>
|
||||
|
||||
<!-- Action buttons at the bottom of the dialog -->
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="0 0 8 8">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="OverwriteBtn"
|
||||
Height="30"
|
||||
MinWidth="100"
|
||||
Margin="0,0,4,0"
|
||||
Click="Overwrite"
|
||||
Content="{DynamicResource commonOverwrite}"
|
||||
Visibility="Collapsed"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
<Button
|
||||
x:Name="SaveBtn"
|
||||
Height="30"
|
||||
MinWidth="100"
|
||||
Margin="0,0,4,0"
|
||||
Click="Save"
|
||||
Content="{DynamicResource commonSave}"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
<Button
|
||||
Height="30"
|
||||
MinWidth="100"
|
||||
Margin="4,0,4,0"
|
||||
Click="Reset"
|
||||
Content="{DynamicResource commonReset}" />
|
||||
<Button
|
||||
Height="30"
|
||||
MinWidth="100"
|
||||
Margin="4,0,4,0"
|
||||
Click="Delete"
|
||||
Content="{DynamicResource commonDelete}" />
|
||||
<Button
|
||||
Height="30"
|
||||
MinWidth="100"
|
||||
Margin="4,0,0,0"
|
||||
Click="Cancel"
|
||||
Content="{DynamicResource commonCancel}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ui:ContentDialog>
|
||||
179
Flow.Launcher/HotkeyControlDialog.xaml.cs
Normal file
179
Flow.Launcher/HotkeyControlDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ModernWpf.Controls;
|
||||
|
||||
namespace Flow.Launcher;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public partial class HotkeyControlDialog : ContentDialog
|
||||
{
|
||||
private IHotkeySettings _hotkeySettings;
|
||||
private Action? _overwriteOtherHotkey;
|
||||
private string DefaultHotkey { get; }
|
||||
public string WindowTitle { get; }
|
||||
public HotkeyModel CurrentHotkey { get; private set; }
|
||||
public ObservableCollection<string> KeysToDisplay { get; } = new();
|
||||
|
||||
public enum EResultType
|
||||
{
|
||||
Cancel,
|
||||
Save,
|
||||
Delete
|
||||
}
|
||||
|
||||
public EResultType ResultType { get; private set; } = EResultType.Cancel;
|
||||
public string ResultValue { get; private set; } = string.Empty;
|
||||
public static string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none");
|
||||
|
||||
public HotkeyControlDialog(string hotkey, string defaultHotkey, IHotkeySettings hotkeySettings, string windowTitle = "")
|
||||
{
|
||||
WindowTitle = windowTitle switch
|
||||
{
|
||||
"" or null => InternationalizationManager.Instance.GetTranslation("hotkeyRegTitle"),
|
||||
_ => windowTitle
|
||||
};
|
||||
DefaultHotkey = defaultHotkey;
|
||||
CurrentHotkey = new HotkeyModel(hotkey);
|
||||
_hotkeySettings = hotkeySettings;
|
||||
SetKeysToDisplay(CurrentHotkey);
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Reset(object sender, RoutedEventArgs routedEventArgs)
|
||||
{
|
||||
SetKeysToDisplay(new HotkeyModel(DefaultHotkey));
|
||||
}
|
||||
|
||||
private void Delete(object sender, RoutedEventArgs routedEventArgs)
|
||||
{
|
||||
KeysToDisplay.Clear();
|
||||
KeysToDisplay.Add(EmptyHotkey);
|
||||
}
|
||||
|
||||
private void Cancel(object sender, RoutedEventArgs routedEventArgs)
|
||||
{
|
||||
ResultType = EResultType.Cancel;
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void Save(object sender, RoutedEventArgs routedEventArgs)
|
||||
{
|
||||
if (KeysToDisplay.Count == 1 && KeysToDisplay[0] == EmptyHotkey)
|
||||
{
|
||||
ResultType = EResultType.Delete;
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
ResultType = EResultType.Save;
|
||||
ResultValue = string.Join("+", KeysToDisplay);
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
//when alt is pressed, the real key should be e.SystemKey
|
||||
Key key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||
|
||||
SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers();
|
||||
|
||||
var hotkeyModel = new HotkeyModel(
|
||||
specialKeyState.AltPressed,
|
||||
specialKeyState.ShiftPressed,
|
||||
specialKeyState.WinPressed,
|
||||
specialKeyState.CtrlPressed,
|
||||
key);
|
||||
|
||||
CurrentHotkey = hotkeyModel;
|
||||
SetKeysToDisplay(CurrentHotkey);
|
||||
}
|
||||
|
||||
private void SetKeysToDisplay(HotkeyModel? hotkey)
|
||||
{
|
||||
_overwriteOtherHotkey = null;
|
||||
KeysToDisplay.Clear();
|
||||
|
||||
if (hotkey == null || hotkey == default(HotkeyModel))
|
||||
{
|
||||
KeysToDisplay.Add(EmptyHotkey);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var key in hotkey.Value.EnumerateDisplayKeys()!)
|
||||
{
|
||||
KeysToDisplay.Add(key);
|
||||
}
|
||||
|
||||
if (tbMsg == null)
|
||||
return;
|
||||
|
||||
if (_hotkeySettings.RegisteredHotkeys.FirstOrDefault(v => v.Hotkey == hotkey) is { } registeredHotkeyData)
|
||||
{
|
||||
var description = string.Format(
|
||||
InternationalizationManager.Instance.GetTranslation(registeredHotkeyData.DescriptionResourceKey),
|
||||
registeredHotkeyData.DescriptionFormatVariables
|
||||
);
|
||||
Alert.Visibility = Visibility.Visible;
|
||||
if (registeredHotkeyData.RemoveHotkey is not null)
|
||||
{
|
||||
tbMsg.Text = string.Format(
|
||||
InternationalizationManager.Instance.GetTranslation("hotkeyUnavailableEditable"),
|
||||
description
|
||||
);
|
||||
SaveBtn.IsEnabled = false;
|
||||
SaveBtn.Visibility = Visibility.Collapsed;
|
||||
OverwriteBtn.IsEnabled = true;
|
||||
OverwriteBtn.Visibility = Visibility.Visible;
|
||||
_overwriteOtherHotkey = registeredHotkeyData.RemoveHotkey;
|
||||
}
|
||||
else
|
||||
{
|
||||
tbMsg.Text = string.Format(
|
||||
InternationalizationManager.Instance.GetTranslation("hotkeyUnavailableUneditable"),
|
||||
description
|
||||
);
|
||||
SaveBtn.IsEnabled = false;
|
||||
SaveBtn.Visibility = Visibility.Visible;
|
||||
OverwriteBtn.IsEnabled = false;
|
||||
OverwriteBtn.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
OverwriteBtn.IsEnabled = false;
|
||||
OverwriteBtn.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (!CheckHotkeyAvailability(hotkey.Value, true))
|
||||
{
|
||||
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable");
|
||||
Alert.Visibility = Visibility.Visible;
|
||||
SaveBtn.IsEnabled = false;
|
||||
SaveBtn.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
Alert.Visibility = Visibility.Collapsed;
|
||||
SaveBtn.IsEnabled = true;
|
||||
SaveBtn.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
|
||||
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
|
||||
|
||||
private void Overwrite(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_overwriteOtherHotkey?.Invoke();
|
||||
Save(sender, e);
|
||||
}
|
||||
}
|
||||
BIN
Flow.Launcher/Images/info.png
Normal file
BIN
Flow.Launcher/Images/info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Flow.Launcher/Images/keyboard.png
Normal file
BIN
Flow.Launcher/Images/keyboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 873 B |
BIN
Flow.Launcher/Images/plugins.png
Normal file
BIN
Flow.Launcher/Images/plugins.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 804 B |
BIN
Flow.Launcher/Images/proxy.png
Normal file
BIN
Flow.Launcher/Images/proxy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Flow.Launcher/Images/store.png
Normal file
BIN
Flow.Launcher/Images/store.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 763 B |
BIN
Flow.Launcher/Images/theme.png
Normal file
BIN
Flow.Launcher/Images/theme.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 KiB |
|
|
@ -154,6 +154,9 @@
|
|||
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
||||
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
||||
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
||||
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||
<system:String x:Key="Animation">Animation</system:String>
|
||||
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||
|
|
@ -168,14 +171,37 @@
|
|||
<!-- Setting Hotkey -->
|
||||
<system:String x:Key="hotkey">Hotkey</system:String>
|
||||
<system:String x:Key="hotkeys">Hotkeys</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher Hotkey</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
||||
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||
<system:String x:Key="openResultModifiers">Open Result Modifier Key</system:String>
|
||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
||||
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
||||
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||
<system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String>
|
||||
<system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String>
|
||||
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||
<system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String>
|
||||
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||
<system:String x:Key="customQueryHotkey">Custom Query Hotkeys</system:String>
|
||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcuts</system:String>
|
||||
<system:String x:Key="builtinShortcuts">Built-in Shortcuts</system:String>
|
||||
|
|
@ -186,6 +212,7 @@
|
|||
<system:String x:Key="delete">Delete</system:String>
|
||||
<system:String x:Key="edit">Edit</system:String>
|
||||
<system:String x:Key="add">Add</system:String>
|
||||
<system:String x:Key="none">None</system:String>
|
||||
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
|
||||
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
|
||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||
|
|
@ -284,6 +311,11 @@
|
|||
<system:String x:Key="hotkeyIsNotUnavailable">Hotkey is unavailable, please select a new hotkey</system:String>
|
||||
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
|
||||
<system:String x:Key="update">Update</system:String>
|
||||
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||
|
||||
<!-- Custom Query Shortcut Dialog -->
|
||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||
|
|
@ -295,8 +327,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
|||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||
|
||||
<!-- Hotkey Control -->
|
||||
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
|
||||
<!-- Common Action -->
|
||||
<system:String x:Key="commonSave">Save</system:String>
|
||||
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||
<system:String x:Key="commonCancel">Cancel</system:String>
|
||||
<system:String x:Key="commonReset">Reset</system:String>
|
||||
<system:String x:Key="commonDelete">Delete</system:String>
|
||||
|
||||
<!-- Crash Reporter -->
|
||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||
|
|
@ -366,6 +402,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
|||
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
||||
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
||||
|
||||
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||
|
||||
<system:String x:Key="RecommendWeather">Weather</system:String>
|
||||
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||
|
|
|
|||
|
|
@ -154,6 +154,9 @@
|
|||
<system:String x:Key="ColorSchemeDark">Tmavý</system:String>
|
||||
<system:String x:Key="SoundEffect">Zvukový efekt</system:String>
|
||||
<system:String x:Key="SoundEffectTip">Přehrát krátký zvuk při otevření okna vyhledávání</system:String>
|
||||
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||
<system:String x:Key="Animation">Animace</system:String>
|
||||
<system:String x:Key="AnimationTip">Použít animaci v UI</system:String>
|
||||
<system:String x:Key="AnimationSpeed">Rychlost animace</system:String>
|
||||
|
|
@ -168,14 +171,37 @@
|
|||
<!-- Setting Hotkey -->
|
||||
<system:String x:Key="hotkey">Klávesová zkratka</system:String>
|
||||
<system:String x:Key="hotkeys">Klávesové zkratky</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Klávesová zkratka pro Flow Launcher</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||
<system:String x:Key="flowlauncherHotkeyToolTip">Zadejte zkratku pro zobrazení/skrytí nástroje Flow Launcher.</system:String>
|
||||
<system:String x:Key="previewHotkey">Klávesová zkratka pro náhled</system:String>
|
||||
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||
<system:String x:Key="previewHotkeyToolTip">Zadejte klávesovou zkratku pro zobrazení/skrytí náhledu v okně vyhledávání.</system:String>
|
||||
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||
<system:String x:Key="openResultModifiers">Modifikační klávesa pro otevření výsledků</system:String>
|
||||
<system:String x:Key="openResultModifiersToolTip">Výběrem modifikační klávesy otevřete vybraný výsledek pomocí klávesnice.</system:String>
|
||||
<system:String x:Key="showOpenResultHotkey">Zobrazit klávesovou zkratku</system:String>
|
||||
<system:String x:Key="showOpenResultHotkeyToolTip">Zobrazí klávesovou zkratku spolu s výsledky.</system:String>
|
||||
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||
<system:String x:Key="OpenContextMenuHotkey">Otevřít kontextovou nabídku</system:String>
|
||||
<system:String x:Key="SettingWindowHotkey">Otevřít okno s nastavením</system:String>
|
||||
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||
<system:String x:Key="OpenContainFolderHotkey">Otevřít umístění složky</system:String>
|
||||
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||
<system:String x:Key="customQueryHotkey">Vlastní klávesové zkratky pro vyhledávání</system:String>
|
||||
<system:String x:Key="customQueryShortcut">Vlastní zkratky dotazů</system:String>
|
||||
<system:String x:Key="builtinShortcuts">Vestavěné zkratky</system:String>
|
||||
|
|
@ -186,6 +212,7 @@
|
|||
<system:String x:Key="delete">Smazat</system:String>
|
||||
<system:String x:Key="edit">Editovat</system:String>
|
||||
<system:String x:Key="add">Přidat</system:String>
|
||||
<system:String x:Key="none">None</system:String>
|
||||
<system:String x:Key="pleaseSelectAnItem">Vyberte prosím položku</system:String>
|
||||
<system:String x:Key="deleteCustomHotkeyWarning">Jste si jisti, že chcete odstranit klávesovou zkratku {0} pro plugin?</system:String>
|
||||
<system:String x:Key="deleteCustomShortcutWarning">Opravdu chcete odstranit zástupce: {0} pro dotaz {1}?</system:String>
|
||||
|
|
@ -284,6 +311,11 @@
|
|||
<system:String x:Key="hotkeyIsNotUnavailable">Klávesová zkratka je nedostupná, zadejte prosím novou zkratku</system:String>
|
||||
<system:String x:Key="invalidPluginHotkey">Neplatná klávesová zkratka pluginu</system:String>
|
||||
<system:String x:Key="update">Aktualizovat</system:String>
|
||||
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||
|
||||
<!-- Custom Query Shortcut Dialog -->
|
||||
<system:String x:Key="customeQueryShortcutTitle">Vlastní klávesová zkratka pro zadávání dotazů</system:String>
|
||||
|
|
@ -295,8 +327,12 @@ Pokud před zkratku při zadávání přidáte znak "@", bude odpovíd
|
|||
<system:String x:Key="duplicateShortcut">Zkratka již existuje, zadejte novou zkratku nebo upravte stávající.</system:String>
|
||||
<system:String x:Key="emptyShortcut">Zkratka a/nebo její plné znění je prázdné.</system:String>
|
||||
|
||||
<!-- Hotkey Control -->
|
||||
<system:String x:Key="hotkeyUnavailable">Klávesová zkratka je nedostupná</system:String>
|
||||
<!-- Common Action -->
|
||||
<system:String x:Key="commonSave">Uložit</system:String>
|
||||
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||
<system:String x:Key="commonCancel">Zrušit</system:String>
|
||||
<system:String x:Key="commonReset">Reset</system:String>
|
||||
<system:String x:Key="commonDelete">Smazat</system:String>
|
||||
|
||||
<!-- Crash Reporter -->
|
||||
<system:String x:Key="reportWindow_version">Verze</system:String>
|
||||
|
|
@ -366,6 +402,12 @@ Pokud před zkratku při zadávání přidáte znak "@", bude odpovíd
|
|||
<system:String x:Key="HotkeyCtrlIDesc">Otevřít okno s nastavením</system:String>
|
||||
<system:String x:Key="HotkeyF5Desc">Znovu načíst data pluginů</system:String>
|
||||
|
||||
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||
|
||||
<system:String x:Key="RecommendWeather">Počasí</system:String>
|
||||
<system:String x:Key="RecommendWeatherDesc">Výsledky počasí Google</system:String>
|
||||
<system:String x:Key="RecommendShell">> ping 8.8.8</system:String>
|
||||
|
|
|
|||
|
|
@ -154,6 +154,9 @@
|
|||
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
||||
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
||||
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
||||
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||
<system:String x:Key="Animation">Animation</system:String>
|
||||
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||
|
|
@ -168,14 +171,37 @@
|
|||
<!-- Setting Hotkey -->
|
||||
<system:String x:Key="hotkey">Genvejstast</system:String>
|
||||
<system:String x:Key="hotkeys">Genvejstast</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher genvejstast</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
||||
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||
<system:String x:Key="openResultModifiers">Åbn resultatmodifikatorer</system:String>
|
||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
||||
<system:String x:Key="showOpenResultHotkey">Vis hotkey</system:String>
|
||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
||||
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||
<system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String>
|
||||
<system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String>
|
||||
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||
<system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String>
|
||||
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||
<system:String x:Key="customQueryHotkey">Tilpasset søgegenvejstast</system:String>
|
||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
|
||||
<system:String x:Key="builtinShortcuts">Built-in Shortcut</system:String>
|
||||
|
|
@ -186,6 +212,7 @@
|
|||
<system:String x:Key="delete">Slet</system:String>
|
||||
<system:String x:Key="edit">Rediger</system:String>
|
||||
<system:String x:Key="add">Tilføj</system:String>
|
||||
<system:String x:Key="none">None</system:String>
|
||||
<system:String x:Key="pleaseSelectAnItem">Vælg venligst</system:String>
|
||||
<system:String x:Key="deleteCustomHotkeyWarning">Er du sikker på du vil slette {0} plugin genvejstast?</system:String>
|
||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||
|
|
@ -284,6 +311,11 @@
|
|||
<system:String x:Key="hotkeyIsNotUnavailable">Genvejstast er utilgængelig, vælg venligst en ny genvejstast</system:String>
|
||||
<system:String x:Key="invalidPluginHotkey">Ugyldig plugin genvejstast</system:String>
|
||||
<system:String x:Key="update">Opdater</system:String>
|
||||
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||
|
||||
<!-- Custom Query Shortcut Dialog -->
|
||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||
|
|
@ -295,8 +327,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
|||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||
|
||||
<!-- Hotkey Control -->
|
||||
<system:String x:Key="hotkeyUnavailable">Genvejstast utilgængelig</system:String>
|
||||
<!-- Common Action -->
|
||||
<system:String x:Key="commonSave">Gem</system:String>
|
||||
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||
<system:String x:Key="commonCancel">Annuller</system:String>
|
||||
<system:String x:Key="commonReset">Reset</system:String>
|
||||
<system:String x:Key="commonDelete">Slet</system:String>
|
||||
|
||||
<!-- Crash Reporter -->
|
||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||
|
|
@ -366,6 +402,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
|||
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
||||
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
||||
|
||||
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||
|
||||
<system:String x:Key="RecommendWeather">Weather</system:String>
|
||||
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@
|
|||
<system:String x:Key="pluginStore">Erweiterungen laden</system:String>
|
||||
<system:String x:Key="pluginStore_NewRelease">New Release</system:String>
|
||||
<system:String x:Key="pluginStore_RecentlyUpdated">Recently Updated</system:String>
|
||||
<system:String x:Key="pluginStore_None">Plugins</system:String>
|
||||
<system:String x:Key="pluginStore_None">Erweiterungen</system:String>
|
||||
<system:String x:Key="pluginStore_Installed">Installed</system:String>
|
||||
<system:String x:Key="refresh">Aktualisieren</system:String>
|
||||
<system:String x:Key="installbtn">Installieren</system:String>
|
||||
|
|
@ -154,6 +154,9 @@
|
|||
<system:String x:Key="ColorSchemeDark">Dunkel</system:String>
|
||||
<system:String x:Key="SoundEffect">Soundeffekt</system:String>
|
||||
<system:String x:Key="SoundEffectTip">Ton abspielen, wenn das Suchfenster geöffnet wird</system:String>
|
||||
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||
<system:String x:Key="Animation">Animation</system:String>
|
||||
<system:String x:Key="AnimationTip">Animationen in der Oberfläche verwenden</system:String>
|
||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||
|
|
@ -168,35 +171,59 @@
|
|||
<!-- Setting Hotkey -->
|
||||
<system:String x:Key="hotkey">Tastenkombination</system:String>
|
||||
<system:String x:Key="hotkeys">Tastenkombination</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher Tastenkombination</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||
<system:String x:Key="flowlauncherHotkeyToolTip">Verknüpfung eingeben, um Flow Launcher anzuzeigen/auszublenden.</system:String>
|
||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
||||
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||
<system:String x:Key="openResultModifiers">Öffnen Sie die Ergebnismodifikatoren</system:String>
|
||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
||||
<system:String x:Key="openResultModifiersToolTip">Wählen Sie eine Modifikatortaste, um das ausgewählte Ergebnis über die Tastatur zu öffnen.</system:String>
|
||||
<system:String x:Key="showOpenResultHotkey">Hotkey anzeigen</system:String>
|
||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
||||
<system:String x:Key="showOpenResultHotkeyToolTip">Hotkey für die Ergebnisauswahl mit Ergebnissen anzeigen.</system:String>
|
||||
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||
<system:String x:Key="OpenContextMenuHotkey">Kontextmenü öffnen</system:String>
|
||||
<system:String x:Key="SettingWindowHotkey">Einstellungsfenster öffnen</system:String>
|
||||
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||
<system:String x:Key="ToggleGameModeHotkey">Gott Modus</system:String>
|
||||
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||
<system:String x:Key="OpenContainFolderHotkey">Öffne beinhaltenden Ordner</system:String>
|
||||
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||
<system:String x:Key="customQueryHotkey">Benutzerdefinierte Abfrage Tastenkombination</system:String>
|
||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
|
||||
<system:String x:Key="builtinShortcuts">Built-in Shortcut</system:String>
|
||||
<system:String x:Key="customQuery">Query</system:String>
|
||||
<system:String x:Key="customQuery">Abfrage</system:String>
|
||||
<system:String x:Key="customShortcut">Shortcut</system:String>
|
||||
<system:String x:Key="customShortcutExpansion">Expansion</system:String>
|
||||
<system:String x:Key="builtinShortcutDescription">Beschreibung</system:String>
|
||||
<system:String x:Key="delete">Löschen</system:String>
|
||||
<system:String x:Key="edit">Bearbeiten</system:String>
|
||||
<system:String x:Key="add">Hinzufügen</system:String>
|
||||
<system:String x:Key="none">None</system:String>
|
||||
<system:String x:Key="pleaseSelectAnItem">Bitte einen Eintrag auswählen</system:String>
|
||||
<system:String x:Key="deleteCustomHotkeyWarning">Wollen Sie die {0} Plugin Tastenkombination wirklich löschen?</system:String>
|
||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||
<system:String x:Key="shortcut_clipboard_description">Get text from clipboard.</system:String>
|
||||
<system:String x:Key="shortcut_active_explorer_path">Get path from active explorer.</system:String>
|
||||
<system:String x:Key="queryWindowShadowEffect">Query window shadow effect</system:String>
|
||||
<system:String x:Key="shadowEffectCPUUsage">Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited.</system:String>
|
||||
<system:String x:Key="windowWidthSize">Window Width Size</system:String>
|
||||
<system:String x:Key="queryWindowShadowEffect">Schatteneffekt im Abfragefenster</system:String>
|
||||
<system:String x:Key="shadowEffectCPUUsage">Der Schatteneffekt beansprucht die GPU stark. Nicht empfohlen, wenn die Leistung deines Computers begrenzt ist.</system:String>
|
||||
<system:String x:Key="windowWidthSize">Fenstergröße Breite</system:String>
|
||||
<system:String x:Key="windowWidthSizeToolTip">You can also quickly adjust this by using Ctrl+[ and Ctrl+].</system:String>
|
||||
<system:String x:Key="useGlyphUI">Use Segoe Fluent Icons</system:String>
|
||||
<system:String x:Key="useGlyphUIEffect">Use Segoe Fluent Icons for query results where supported</system:String>
|
||||
<system:String x:Key="useGlyphUI">Segoe Fluent Icons verwenden</system:String>
|
||||
<system:String x:Key="useGlyphUIEffect">Segoe Fluent Icons für Abfrageergebnisse verwenden, sofern unterstützt</system:String>
|
||||
<system:String x:Key="flowlauncherPressHotkey">Press Key</system:String>
|
||||
|
||||
<!-- Setting Proxy -->
|
||||
|
|
@ -226,15 +253,15 @@
|
|||
<system:String x:Key="checkUpdates">Nach Aktuallisierungen Suchen</system:String>
|
||||
<system:String x:Key="BecomeASponsor">Become A Sponsor</system:String>
|
||||
<system:String x:Key="newVersionTips">Eine neue Version ({0}) ist vorhanden. Bitte starten Sie Flow Launcher neu.</system:String>
|
||||
<system:String x:Key="checkUpdatesFailed">Check updates failed, please check your connection and proxy settings to api.github.com.</system:String>
|
||||
<system:String x:Key="checkUpdatesFailed">Überprüfung der Updates fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindungs- und Proxy-Einstellungen zu api.github.com.</system:String>
|
||||
<system:String x:Key="downloadUpdatesFailed">
|
||||
Download updates failed, please check your connection and proxy settings to github-cloud.s3.amazonaws.com,
|
||||
or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually.
|
||||
Das Herunterladen von Updates ist fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindungs- und Proxy-Einstellungen zu github-cloud.s3.amazonaws.com,
|
||||
oder gehen Sie zu https://github.com/Flow-Launcher/Flow.Launcher/releases, um Updates manuell herunterzuladen.
|
||||
</system:String>
|
||||
<system:String x:Key="releaseNotes">Versionshinweise</system:String>
|
||||
<system:String x:Key="documentation">Usage Tips</system:String>
|
||||
<system:String x:Key="devtool">DevTools</system:String>
|
||||
<system:String x:Key="settingfolder">Setting Folder</system:String>
|
||||
<system:String x:Key="documentation">Verwendungshinweise</system:String>
|
||||
<system:String x:Key="devtool">Entwicklerwerkzeuge</system:String>
|
||||
<system:String x:Key="settingfolder">Einstellungsordner</system:String>
|
||||
<system:String x:Key="logfolder">Protokoll-Verzeichnis</system:String>
|
||||
<system:String x:Key="clearlogfolder">Protokolldateien leeren</system:String>
|
||||
<system:String x:Key="clearlogfolderMessage">Sind Sie sicher, dass Sie alle Protokolle löschen möchten?</system:String>
|
||||
|
|
@ -246,13 +273,13 @@
|
|||
<system:String x:Key="fileManager_tips2">"%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d".</system:String>
|
||||
<system:String x:Key="fileManager_name">Datei-Manager</system:String>
|
||||
<system:String x:Key="fileManager_profile_name">Profilname</system:String>
|
||||
<system:String x:Key="fileManager_path">File Manager Path</system:String>
|
||||
<system:String x:Key="fileManager_path">Dateimanager-Pfad</system:String>
|
||||
<system:String x:Key="fileManager_directory_arg">Argumente für Ordner</system:String>
|
||||
<system:String x:Key="fileManager_file_arg">Argumente für Datei</system:String>
|
||||
|
||||
<!-- DefaultBrowser Setting Dialog -->
|
||||
<system:String x:Key="defaultBrowserTitle">Standard-Webbrowser</system:String>
|
||||
<system:String x:Key="defaultBrowser_tips">The default setting follows the OS default browser setting. If specified separately, flow uses that browser.</system:String>
|
||||
<system:String x:Key="defaultBrowser_tips">Die Standardeinstellung verwendet die Browser-Standardeinstellung des Betriebssystems. Falls angegeben, verwendet Flow diesen Browser.</system:String>
|
||||
<system:String x:Key="defaultBrowser_name">Browser</system:String>
|
||||
<system:String x:Key="defaultBrowser_profile_name">Browser-Name</system:String>
|
||||
<system:String x:Key="defaultBrowser_path">Browserpfad</system:String>
|
||||
|
|
@ -262,8 +289,8 @@
|
|||
|
||||
<!-- Priority Setting Dialog -->
|
||||
<system:String x:Key="changePriorityWindow">Priorität ändern</system:String>
|
||||
<system:String x:Key="priority_tips">Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number</system:String>
|
||||
<system:String x:Key="invalidPriority">Please provide an valid integer for Priority!</system:String>
|
||||
<system:String x:Key="priority_tips">Je höher die Zahl, desto höher wird das Ergebnis gewertet. Versuchen Sie es mit einer 5. Sollen die Ergebnisse tiefer sein als diejenigen anderer Plugins, verwenden Sie eine negative Zahl</system:String>
|
||||
<system:String x:Key="invalidPriority">Bitte eine gültige Ganzzahl angeben für die Priorität!</system:String>
|
||||
|
||||
<!-- Action Keyword Setting Dialog -->
|
||||
<system:String x:Key="oldActionKeywords">Altes Aktionsschlüsselwort</system:String>
|
||||
|
|
@ -274,7 +301,7 @@
|
|||
<system:String x:Key="newActionKeywordsCannotBeEmpty">Neues Aktionsschlüsselwort darf nicht leer sein</system:String>
|
||||
<system:String x:Key="newActionKeywordsHasBeenAssigned">Aktionsschlüsselwort ist schon bei einem anderen Plugin in verwendung. Bitte stellen Sie ein anderes Aktionsschlüsselwort ein.</system:String>
|
||||
<system:String x:Key="success">Erfolgreich</system:String>
|
||||
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
|
||||
<system:String x:Key="completedSuccessfully">Erfolgreich abgeschlossen</system:String>
|
||||
<system:String x:Key="actionkeyword_tips">Benutzen Sie * wenn Sie ein Aktionsschlüsselwort definieren wollen.</system:String>
|
||||
|
||||
<!-- Custom Query Hotkey Dialog -->
|
||||
|
|
@ -284,6 +311,11 @@
|
|||
<system:String x:Key="hotkeyIsNotUnavailable">Tastenkombination ist nicht verfügbar, bitte wähle eine andere Tastenkombination</system:String>
|
||||
<system:String x:Key="invalidPluginHotkey">Ungültige Plugin Tastenkombination</system:String>
|
||||
<system:String x:Key="update">Aktualisieren</system:String>
|
||||
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||
|
||||
<!-- Custom Query Shortcut Dialog -->
|
||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||
|
|
@ -295,8 +327,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
|||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||
|
||||
<!-- Hotkey Control -->
|
||||
<system:String x:Key="hotkeyUnavailable">Tastenkombination nicht verfügbar</system:String>
|
||||
<!-- Common Action -->
|
||||
<system:String x:Key="commonSave">Speichern</system:String>
|
||||
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||
<system:String x:Key="commonCancel">Abbrechen</system:String>
|
||||
<system:String x:Key="commonReset">Reset</system:String>
|
||||
<system:String x:Key="commonDelete">Löschen</system:String>
|
||||
|
||||
<!-- Crash Reporter -->
|
||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||
|
|
@ -347,10 +383,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
|||
<system:String x:Key="Welcome_Page2_Text01">Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.</system:String>
|
||||
<system:String x:Key="Welcome_Page2_Text02">Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard.</system:String>
|
||||
<system:String x:Key="Welcome_Page3_Title">Tastenkombination</system:String>
|
||||
<system:String x:Key="Welcome_Page4_Title">Action Keyword and Commands</system:String>
|
||||
<system:String x:Key="Welcome_Page4_Title">Aktions-Schlüsselwort und Befehle</system:String>
|
||||
<system:String x:Key="Welcome_Page4_Text01">Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher.</system:String>
|
||||
<system:String x:Key="Welcome_Page5_Title">Let's Start Flow Launcher</system:String>
|
||||
<system:String x:Key="Welcome_Page5_Text01">Finished. Enjoy Flow Launcher. Don't forget the hotkey to start :)</system:String>
|
||||
<system:String x:Key="Welcome_Page5_Title">Starte Flow-Launcher</system:String>
|
||||
<system:String x:Key="Welcome_Page5_Text01">Fertig. Genießen Sie Flow Launcher. Die Tastenkombination nicht vergessen, um Flow Launcher zu starten :)</system:String>
|
||||
|
||||
<!-- General Guide & Hotkey -->
|
||||
|
||||
|
|
@ -366,13 +402,19 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
|||
<system:String x:Key="HotkeyCtrlIDesc">Einstellungsfenster öffnen</system:String>
|
||||
<system:String x:Key="HotkeyF5Desc">Plugin-Daten neu laden</system:String>
|
||||
|
||||
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||
|
||||
<system:String x:Key="RecommendWeather">Wetter</system:String>
|
||||
<system:String x:Key="RecommendWeatherDesc">Wetter in Google-Ergebnis</system:String>
|
||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||
<system:String x:Key="RecommendShellDesc">Shell-Befehl</system:String>
|
||||
<system:String x:Key="RecommendBluetooth">s Bluetooth</system:String>
|
||||
<system:String x:Key="RecommendBluetoothDesc">Bluetooth in Windows-Einstellungen</system:String>
|
||||
<system:String x:Key="RecommendAcronyms">sn</system:String>
|
||||
<system:String x:Key="RecommendAcronymsDesc">Sticky Notes</system:String>
|
||||
<system:String x:Key="RecommendAcronyms">nt</system:String>
|
||||
<system:String x:Key="RecommendAcronymsDesc">Notizen</system:String>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@
|
|||
<system:String x:Key="LastQueryPreserved">Preserve Last Query</system:String>
|
||||
<system:String x:Key="LastQuerySelected">Select last Query</system:String>
|
||||
<system:String x:Key="LastQueryEmpty">Empty last Query</system:String>
|
||||
<system:String x:Key="KeepMaxResults">Fixed Window Height</system:String>
|
||||
<system:String x:Key="KeepMaxResultsToolTip">The window height will not be resizeable by dragging</system:String>
|
||||
<system:String x:Key="maxShowResults">Maximum results shown</system:String>
|
||||
<system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String>
|
||||
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
|
||||
|
|
@ -77,6 +79,9 @@
|
|||
<system:String x:Key="hideNotifyIconToolTip">When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window.</system:String>
|
||||
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
|
||||
<system:String x:Key="querySearchPrecisionToolTip">Changes minimum match score required for results.</system:String>
|
||||
<system:String x:Key="SearchPrecisionNone">None</system:String>
|
||||
<system:String x:Key="SearchPrecisionLow">Low</system:String>
|
||||
<system:String x:Key="SearchPrecisionRegular">Regular</system:String>
|
||||
<system:String x:Key="ShouldUsePinyin">Search with Pinyin</system:String>
|
||||
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese.</system:String>
|
||||
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
|
||||
|
|
@ -142,8 +147,13 @@
|
|||
<system:String x:Key="SampleSubTitleProgram">Launch programs as admin or a different user</system:String>
|
||||
<system:String x:Key="SampleTitleProcessKiller">ProcessKiller</system:String>
|
||||
<system:String x:Key="SampleSubTitleProcessKiller">Terminate unwanted processes</system:String>
|
||||
<system:String x:Key="SearchBarHeight">Search Bar Height</system:String>
|
||||
<system:String x:Key="ItemHeight">Item Height</system:String>
|
||||
<system:String x:Key="queryBoxFont">Query Box Font</system:String>
|
||||
<system:String x:Key="resultItemFont">Result Item Font</system:String>
|
||||
<system:String x:Key="resultItemFont">Result Title Font</system:String>
|
||||
<system:String x:Key="resultSubItemFont">Result Subtitle Font</system:String>
|
||||
<system:String x:Key="resetCustomize">Reset</system:String>
|
||||
<system:String x:Key="CustomizeToolTip">Customize</system:String>
|
||||
<system:String x:Key="windowMode">Window Mode</system:String>
|
||||
<system:String x:Key="opacity">Opacity</system:String>
|
||||
<system:String x:Key="theme_load_failure_path_not_exists">Theme {0} not exists, fallback to default theme</system:String>
|
||||
|
|
@ -156,6 +166,9 @@
|
|||
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
||||
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
||||
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
||||
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||
<system:String x:Key="Animation">Animation</system:String>
|
||||
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||
|
|
@ -167,17 +180,41 @@
|
|||
<system:String x:Key="Clock">Clock</system:String>
|
||||
<system:String x:Key="Date">Date</system:String>
|
||||
|
||||
|
||||
<!-- Setting Hotkey -->
|
||||
<system:String x:Key="hotkey">Hotkey</system:String>
|
||||
<system:String x:Key="hotkeys">Hotkeys</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher Hotkey</system:String>
|
||||
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
||||
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||
<system:String x:Key="openResultModifiers">Open Result Modifier Key</system:String>
|
||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
||||
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
||||
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||
<system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String>
|
||||
<system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String>
|
||||
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||
<system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String>
|
||||
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||
<system:String x:Key="customQueryHotkey">Custom Query Hotkeys</system:String>
|
||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcuts</system:String>
|
||||
<system:String x:Key="builtinShortcuts">Built-in Shortcuts</system:String>
|
||||
|
|
@ -188,6 +225,7 @@
|
|||
<system:String x:Key="delete">Delete</system:String>
|
||||
<system:String x:Key="edit">Edit</system:String>
|
||||
<system:String x:Key="add">Add</system:String>
|
||||
<system:String x:Key="none">None</system:String>
|
||||
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
|
||||
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
|
||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||
|
|
@ -286,6 +324,11 @@
|
|||
<system:String x:Key="hotkeyIsNotUnavailable">Hotkey is unavailable, please select a new hotkey</system:String>
|
||||
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
|
||||
<system:String x:Key="update">Update</system:String>
|
||||
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||
|
||||
<!-- Custom Query Shortcut Dialog -->
|
||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||
|
|
@ -295,8 +338,12 @@
|
|||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||
|
||||
<!-- Hotkey Control -->
|
||||
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
|
||||
<!-- Common Action -->
|
||||
<system:String x:Key="commonSave">Save</system:String>
|
||||
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||
<system:String x:Key="commonCancel">Cancel</system:String>
|
||||
<system:String x:Key="commonReset">Reset</system:String>
|
||||
<system:String x:Key="commonDelete">Delete</system:String>
|
||||
|
||||
<!-- Crash Reporter -->
|
||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||
|
|
@ -366,6 +413,12 @@
|
|||
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
||||
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
||||
|
||||
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||
|
||||
<system:String x:Key="RecommendWeather">Weather</system:String>
|
||||
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||
|
|
@ -375,4 +428,8 @@
|
|||
<system:String x:Key="RecommendAcronyms">sn</system:String>
|
||||
<system:String x:Key="RecommendAcronymsDesc">Sticky Notes</system:String>
|
||||
|
||||
<!-- Preview Area -->
|
||||
<system:String x:Key="FileSize">File Size</system:String>
|
||||
<system:String x:Key="Created">Created</system:String>
|
||||
<system:String x:Key="LastModified">Last Modified</system:String>
|
||||
</ResourceDictionary>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue