Merge branch 'dev' into flow-theme-selector-plugin

This commit is contained in:
Jeremy Wu 2025-01-18 21:26:10 +11:00 committed by GitHub
commit fe8bb6b683
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
514 changed files with 29423 additions and 17228 deletions

80
.cm/gitstream.cm Normal file
View file

@ -0,0 +1,80 @@
# -*- mode: yaml -*-
# This example configuration for provides basic automations to get started with gitStream.
# View the gitStream quickstart for more examples: https://docs.gitstream.cm/examples/
manifest:
version: 1.0
triggers:
exclude:
branch:
- l10n_dev
- dev
- r/([Dd]ependabot|[Rr]enovate)/
automations:
# Add a label that indicates how many minutes it will take to review the PR.
estimated_time_to_review:
on:
- commit
if:
- true
run:
- action: add-label@v1
args:
label: "{{ calc.etr }} min review"
color: {{ colors.red if (calc.etr >= 20) else ( colors.yellow if (calc.etr >= 5) else colors.green ) }}
# Post a comment that lists the best experts for the files that were modified.
explain_code_experts:
if:
- true
run:
- action: explain-code-experts@v1
args:
gt: 10
# Post a comment notifying that the PR contains a TODO statement.
review_todo_comments:
if:
- {{ source.diff.files | matchDiffLines(regex=r/^[+].*\b(TODO|todo)\b/) | some }}
run:
- action: add-comment@v1
args:
comment: |
This PR contains a TODO statement. Please check to see if they should be removed.
# Post a comment that request a before and after screenshot
request_screenshot:
# Triggered for PRs that lack an image file or link to an image in the PR description
if:
- {{ not (has.screenshot_link or has.image_uploaded) }}
run:
- action: add-comment@v1
args:
comment: |
Be a legend :trophy: by adding a before and after screenshot of the changes you made, especially if they are around UI/UX.
# +----------------------------------------------------------------------------+
# | Custom Expressions |
# | https://docs.gitstream.cm/how-it-works/#custom-expressions |
# +----------------------------------------------------------------------------+
calc:
etr: {{ branch | estimatedReviewTime }}
colors:
red: 'b60205'
yellow: 'fbca04'
green: '0e8a16'
changes:
# Sum all the lines added/edited in the PR
additions: {{ branch.diff.files_metadata | map(attr='additions') | sum }}
# Sum all the line removed in the PR
deletions: {{ branch.diff.files_metadata | map(attr='deletions') | sum }}
# Calculate the ratio of new code
ratio: {{ (changes.additions / (changes.additions + changes.deletions)) * 100 | round(2) }}
has:
screenshot_link: {{ pr.description | includes(regex=r/!\[.*\]\(.*(jpg|svg|png|gif|psd).*\)/) }}
image_uploaded: {{ pr.description | includes(regex=r/(<img.*src.*(jpg|svg|png|gif|psd).*>)|!\[image\]\(.*github\.com.*\)/) }}

View file

@ -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

View file

@ -3,3 +3,6 @@ https
ssh
ubuntu
runcount
Firefox
Português
Português (Brasil)

View file

@ -74,6 +74,7 @@ WCA_ACCENT_POLICY
HGlobal
dopusrt
firefox
Firefox
msedge
svgc
ime
@ -97,6 +98,8 @@ Português
Português (Brasil)
Italiano
Slovenský
quicklook
Tiếng Việt
Droplex
Preinstalled
errormetadatafile

31
.github/pr-labeler.yml vendored Normal file
View file

@ -0,0 +1,31 @@
# The bot always updates the labels, add/remove as necessary [default: false]
alwaysReplace: false
# Treats the text and labels as case sensitive [default: true]
caseSensitive: false
# Array of labels to be applied to the PR [default: []]
customLabels:
# Finds the `text` within the PR title and body and applies the `label`
- text: 'bug'
label: 'bug'
- text: 'fix'
label: 'bug'
- text: 'dependabot'
label: 'bug'
- text: 'New Crowdin updates'
label: 'bug'
- text: 'New Crowdin updates'
label: 'kind/i18n'
- text: 'feature'
label: 'enhancement'
- text: 'add new'
label: 'enhancement'
- text: 'refactor'
label: 'enhancement'
- text: 'refactor'
label: 'Code Refactor'
# Search the body of the PR for the `text` [default: true]
searchBody: true
# Search the title of the PR for the `text` [default: true]
searchTitle: true
# Search for whole words only [default: false]
wholeWords: false

View file

@ -2,7 +2,7 @@ name: Publish Default Plugins
on:
push:
branches: ['dev']
branches: ['master']
paths: ['Plugins/**']
workflow_dispatch:
@ -369,4 +369,4 @@ jobs:
tag_name: "v${{steps.updated-version-windowssettings.outputs.prop}}"
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
env:
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}

49
.github/workflows/gitstream.yml vendored Normal file
View file

@ -0,0 +1,49 @@
# Code generated by gitStream GitHub app - DO NOT EDIT
name: gitStream workflow automation
run-name: |
/:\ gitStream: PR #${{ fromJSON(fromJSON(github.event.inputs.client_payload)).pullRequestNumber }} from ${{ github.event.inputs.full_repository }}
on:
workflow_dispatch:
inputs:
client_payload:
description: The Client payload
required: true
full_repository:
description: the repository name include the owner in `owner/repo_name` format
required: true
head_ref:
description: the head sha
required: true
base_ref:
description: the base ref
required: true
installation_id:
description: the installation id
required: false
resolver_url:
description: the resolver url to pass results to
required: true
resolver_token:
description: Optional resolver token for resolver service
required: false
default: ''
jobs:
gitStream:
timeout-minutes: 5
runs-on: ubuntu-latest
name: gitStream workflow automation
steps:
- name: Evaluate Rules
uses: linear-b/gitstream-github-action@v2
id: rules-engine
with:
full_repository: ${{ github.event.inputs.full_repository }}
head_ref: ${{ github.event.inputs.head_ref }}
base_ref: ${{ github.event.inputs.base_ref }}
client_payload: ${{ github.event.inputs.client_payload }}
installation_id: ${{ github.event.inputs.installation_id }}
resolver_url: ${{ github.event.inputs.resolver_url }}
resolver_token: ${{ github.event.inputs.resolver_token }}

17
.github/workflows/pr_assignee.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Assign PR to creator
on:
pull_request_target:
types: [opened]
branches-ignore:
- l10n_dev
permissions:
pull-requests: write
jobs:
automation:
runs-on: ubuntu-latest
steps:
- name: Assign PR to creator
uses: toshimaru/auto-author-assign@v2.1.1

22
.github/workflows/pr_milestone.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Set Milestone
# Assigns the earliest created milestone that matches the below glob pattern.
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
automation:
runs-on: ubuntu-latest
steps:
- name: set-milestone
uses: andrefcdias/add-to-milestone@v1.3.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
milestone: "+([0-9]).+([0-9]).+([0-9])"
use-expression: true

View file

@ -0,0 +1,25 @@
name: Top-Ranking Issues
on:
schedule:
- cron: '0 0 */1 * *'
workflow_dispatch:
jobs:
ShowAndLabelTopIssues:
name: Display and label top issues.
runs-on: ubuntu-latest
steps:
- name: Top Issues action
uses: rickstaa/top-issues-action@v1.3.101
env:
github_token: ${{ secrets.GITHUB_TOKEN }}
with:
dashboard: true
dashboard_show_total_reactions: true
top_list_size: 10
top_features: true
top_bugs: true
dashboard_title: Top-Ranking Issues 📈
dashboard_label: ⭐ Dashboard
hide_dashboard_footer: true
top_issues: false

View file

@ -40,7 +40,7 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.PortableDataPath);
MessageBox.Show("Flow Launcher needs to restart to finish disabling portable mode, " +
MessageBoxEx.Show("Flow Launcher needs to restart to finish disabling portable mode, " +
"after the restart your portable data profile will be deleted and roaming data profile kept");
UpdateManager.RestartApp(Constant.ApplicationFileName);
@ -64,7 +64,7 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.RoamingDataPath);
MessageBox.Show("Flow Launcher needs to restart to finish enabling portable mode, " +
MessageBoxEx.Show("Flow Launcher needs to restart to finish enabling portable mode, " +
"after the restart your roaming data profile will be deleted and portable data profile kept");
UpdateManager.RestartApp(Constant.ApplicationFileName);
@ -95,13 +95,13 @@ namespace Flow.Launcher.Core.Configuration
public void MoveUserDataFolder(string fromLocation, string toLocation)
{
FilesFolders.CopyAll(fromLocation, toLocation);
FilesFolders.CopyAll(fromLocation, toLocation, MessageBoxEx.Show);
VerifyUserDataAfterMove(fromLocation, toLocation);
}
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
{
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation);
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, MessageBoxEx.Show);
}
public void CreateShortcuts()
@ -157,13 +157,13 @@ namespace Flow.Launcher.Core.Configuration
// delete it and prompt the user to pick the portable data location
if (File.Exists(roamingDataDeleteFilePath))
{
FilesFolders.RemoveFolderIfExists(roamingDataDir);
FilesFolders.RemoveFolderIfExists(roamingDataDir, MessageBoxEx.Show);
if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " +
if (MessageBoxEx.Show("Flow Launcher has detected you enabled portable mode, " +
"would you like to move it to a different location?", string.Empty,
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
FilesFolders.OpenPath(Constant.RootDirectory);
FilesFolders.OpenPath(Constant.RootDirectory, MessageBoxEx.Show);
Environment.Exit(0);
}
@ -172,9 +172,9 @@ namespace Flow.Launcher.Core.Configuration
// delete it and notify the user about it.
else if (File.Exists(portableDataDeleteFilePath))
{
FilesFolders.RemoveFolderIfExists(portableDataDir);
FilesFolders.RemoveFolderIfExists(portableDataDir, MessageBoxEx.Show);
MessageBox.Show("Flow Launcher has detected you disabled portable mode, " +
MessageBoxEx.Show("Flow Launcher has detected you disabled portable mode, " +
"the relevant shortcuts and uninstaller entry have been created");
}
}
@ -186,7 +186,7 @@ namespace Flow.Launcher.Core.Configuration
if (roamingLocationExists && portableLocationExists)
{
MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " +
MessageBoxEx.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " +
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));

View file

@ -4,9 +4,10 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using Flow.Launcher.Core.Resource;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@ -50,14 +51,15 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
}
if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " +
$"will require {EnvName} to run. Would you like to download {EnvName}? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
$"and you will be prompted to select the folder that contains the {EnvName} executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
var noRuntimeMessage = string.Format(
InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
Language,
EnvName,
Environment.NewLine
);
if (MessageBoxEx.Show(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{
var msg = $"Please select the {EnvName} executable";
var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
string selectedFile;
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
@ -80,8 +82,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
else
{
MessageBox.Show(
$"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom).");
MessageBoxEx.Show(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
Log.Error("PluginsLoader",
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");
@ -97,7 +98,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
if (expectedPath == currentPath)
return;
FilesFolders.RemoveFolderIfExists(installedDirPath);
FilesFolders.RemoveFolderIfExists(installedDirPath, MessageBoxEx.Show);
InstallEnvironment();

View file

@ -28,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath);
FilesFolders.RemoveFolderIfExists(InstallPath, MessageBoxEx.Show);
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and
// uses Python plugin they need to custom install and use v3.8.9

View file

@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath);
FilesFolders.RemoveFolderIfExists(InstallPath, MessageBoxEx.Show);
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();

View file

@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath);
FilesFolders.RemoveFolderIfExists(InstallPath, MessageBoxEx.Show);
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();

View file

@ -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 bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
}
}

View file

@ -54,11 +54,11 @@
<ItemGroup>
<PackageReference Include="Droplex" Version="1.7.0" />
<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="FSharp.Core" Version="9.0.100" />
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
<PackageReference Include="StreamJsonRpc" Version="2.17.11" />
<PackageReference Include="StreamJsonRpc" Version="2.20.20" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,155 @@
<Window
x:Class="Flow.Launcher.Core.MessageBoxEx"
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:local="clr-namespace:Flow.Launcher.Core"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="MessageBoxWindow"
Width="420"
Height="Auto"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="KeyEsc_OnPress" />
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition MinHeight="68" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="1"
Click="Button_Cancel"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
</StackPanel>
<Grid Grid.Row="1" Margin="30 0 30 24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0 0 0 12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image
Name="Img"
Grid.Column="0"
Width="18"
Height="18"
Margin="0 0 10 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="Fant"
Stretch="UniformToFill"
Visibility="Collapsed" />
<TextBlock
x:Name="TitleTextBlock"
Grid.Column="1"
MaxWidth="400"
Margin="0 0 26 0"
VerticalAlignment="Center"
FontFamily="Segoe UI"
FontSize="20"
FontWeight="SemiBold"
TextAlignment="Left"
TextWrapping="Wrap" />
</Grid>
<TextBlock
x:Name="DescTextBlock"
Grid.Row="1"
MaxWidth="400"
Margin="0 0 26 0"
HorizontalAlignment="Stretch"
FontSize="14"
TextAlignment="Left"
TextWrapping="Wrap" />
<TextBlock
x:Name="DescOnlyTextBlock"
Grid.Row="0"
Grid.RowSpan="2"
MaxWidth="400"
Margin="0 0 26 0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
FontSize="14"
TextAlignment="Left"
TextWrapping="Wrap"
Visibility="Collapsed" />
</Grid>
<Border
Grid.Row="2"
Margin="0 0 0 0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0">
<WrapPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="btnOk"
MinWidth="120"
Margin="5 0 5 0"
Click="Button_Click"
Content="{DynamicResource commonOK}" />
<Button
x:Name="btnYes"
MinWidth="120"
Margin="5 0 5 0"
Click="Button_Click"
Content="{DynamicResource commonYes}" />
<Button
x:Name="btnNo"
MinWidth="120"
Margin="5 0 5 0"
Click="Button_Click"
Content="{DynamicResource commonNo}" />
<Button
x:Name="btnCancel"
MinWidth="120"
Margin="5 0 5 0"
Click="Button_Click"
Content="{DynamicResource commonCancel}" />
</WrapPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Core
{
public partial class MessageBoxEx : Window
{
private static MessageBoxEx msgBox;
private static MessageBoxResult _result = MessageBoxResult.None;
private readonly MessageBoxButton _button;
private MessageBoxEx(MessageBoxButton button)
{
_button = button;
InitializeComponent();
}
public static MessageBoxResult Show(string messageBoxText)
=> Show(messageBoxText, string.Empty, MessageBoxButton.OK, MessageBoxImage.None, MessageBoxResult.OK);
public static MessageBoxResult Show(
string messageBoxText,
string caption = "",
MessageBoxButton button = MessageBoxButton.OK,
MessageBoxImage icon = MessageBoxImage.None,
MessageBoxResult defaultResult = MessageBoxResult.OK)
{
if (!Application.Current.Dispatcher.CheckAccess())
{
return Application.Current.Dispatcher.Invoke(() => Show(messageBoxText, caption, button, icon, defaultResult));
}
try
{
msgBox = new MessageBoxEx(button);
if (caption == string.Empty && button == MessageBoxButton.OK && icon == MessageBoxImage.None)
{
msgBox.Title = messageBoxText;
msgBox.DescOnlyTextBlock.Visibility = Visibility.Visible;
msgBox.DescOnlyTextBlock.Text = messageBoxText;
}
else
{
msgBox.Title = caption;
msgBox.TitleTextBlock.Text = caption;
msgBox.DescTextBlock.Text = messageBoxText;
_ = SetImageOfMessageBoxAsync(icon);
}
SetButtonVisibilityFocusAndResult(button, defaultResult);
msgBox.ShowDialog();
return _result;
}
catch (Exception e)
{
Log.Error($"|MessageBoxEx.Show|An error occurred: {e.Message}");
msgBox = null;
return MessageBoxResult.None;
}
}
private static void SetButtonVisibilityFocusAndResult(MessageBoxButton button, MessageBoxResult defaultResult)
{
switch (button)
{
case MessageBoxButton.OK:
msgBox.btnCancel.Visibility = Visibility.Collapsed;
msgBox.btnNo.Visibility = Visibility.Collapsed;
msgBox.btnYes.Visibility = Visibility.Collapsed;
msgBox.btnOk.Focus();
_result = MessageBoxResult.OK;
break;
case MessageBoxButton.OKCancel:
msgBox.btnNo.Visibility = Visibility.Collapsed;
msgBox.btnYes.Visibility = Visibility.Collapsed;
if (defaultResult == MessageBoxResult.Cancel)
{
msgBox.btnCancel.Focus();
_result = MessageBoxResult.Cancel;
}
else
{
msgBox.btnOk.Focus();
_result = MessageBoxResult.OK;
}
break;
case MessageBoxButton.YesNo:
msgBox.btnOk.Visibility = Visibility.Collapsed;
msgBox.btnCancel.Visibility = Visibility.Collapsed;
if (defaultResult == MessageBoxResult.No)
{
msgBox.btnNo.Focus();
_result = MessageBoxResult.No;
}
else
{
msgBox.btnYes.Focus();
_result = MessageBoxResult.Yes;
}
break;
case MessageBoxButton.YesNoCancel:
msgBox.btnOk.Visibility = Visibility.Collapsed;
if (defaultResult == MessageBoxResult.No)
{
msgBox.btnNo.Focus();
_result = MessageBoxResult.No;
}
else if (defaultResult == MessageBoxResult.Cancel)
{
msgBox.btnCancel.Focus();
_result = MessageBoxResult.Cancel;
}
else
{
msgBox.btnYes.Focus();
_result = MessageBoxResult.Yes;
}
break;
default:
break;
}
}
private static async Task SetImageOfMessageBoxAsync(MessageBoxImage icon)
{
switch (icon)
{
case MessageBoxImage.Exclamation:
await msgBox.SetImageAsync("Exclamation.png");
msgBox.Img.Visibility = Visibility.Visible;
break;
case MessageBoxImage.Question:
await msgBox.SetImageAsync("Question.png");
msgBox.Img.Visibility = Visibility.Visible;
break;
case MessageBoxImage.Information:
await msgBox.SetImageAsync("Information.png");
msgBox.Img.Visibility = Visibility.Visible;
break;
case MessageBoxImage.Error:
await msgBox.SetImageAsync("Error.png");
msgBox.Img.Visibility = Visibility.Visible;
break;
default:
msgBox.Img.Visibility = Visibility.Collapsed;
break;
}
}
private async Task SetImageAsync(string imageName)
{
var imagePath = Path.Combine(Constant.ProgramDirectory, "Images", imageName);
var imageSource = await ImageLoader.LoadAsync(imagePath);
Img.Source = imageSource;
}
private void KeyEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
{
if (_button == MessageBoxButton.YesNo)
return;
else if (_button == MessageBoxButton.OK)
_result = MessageBoxResult.OK;
else
_result = MessageBoxResult.Cancel;
DialogResult = false;
Close();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (sender == btnOk)
_result = MessageBoxResult.OK;
else if (sender == btnYes)
_result = MessageBoxResult.Yes;
else if (sender == btnNo)
_result = MessageBoxResult.No;
else if (sender == btnCancel)
_result = MessageBoxResult.Cancel;
else
_result = MessageBoxResult.None;
msgBox.Close();
msgBox = null;
}
private void Button_Cancel(object sender, RoutedEventArgs e)
{
if (_button == MessageBoxButton.YesNo)
return;
else if (_button == MessageBoxButton.OK)
_result = MessageBoxResult.OK;
else
_result = MessageBoxResult.Cancel;
msgBox.Close();
msgBox = null;
}
}
}

View file

@ -4,8 +4,15 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Forms;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
using CheckBox = System.Windows.Controls.CheckBox;
using ComboBox = System.Windows.Controls.ComboBox;
using Control = System.Windows.Controls.Control;
using Orientation = System.Windows.Controls.Orientation;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
namespace Flow.Launcher.Core.Plugin
{
@ -224,6 +231,7 @@ namespace Flow.Launcher.Core.Plugin
break;
}
case "inputWithFileBtn":
case "inputWithFolderBtn":
{
var textBox = new TextBox()
{
@ -243,6 +251,24 @@ namespace Flow.Launcher.Core.Plugin
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
};
Btn.Click += (_, _) =>
{
using CommonDialog dialog = type switch
{
"inputWithFolderBtn" => new FolderBrowserDialog(),
_ => new OpenFileDialog(),
};
if (dialog.ShowDialog() != DialogResult.OK) return;
var path = dialog switch
{
FolderBrowserDialog folderDialog => folderDialog.SelectedPath,
OpenFileDialog fileDialog => fileDialog.FileName,
};
textBox.Text = path;
Settings[attribute.Name] = path;
};
var dockPanel = new DockPanel() { Margin = settingControlMargin };
DockPanel.SetDock(Btn, Dock.Right);

View file

@ -139,11 +139,20 @@ namespace Flow.Launcher.Core.Plugin
return Task.CompletedTask;
}
public virtual ValueTask DisposeAsync()
public virtual async ValueTask DisposeAsync()
{
RPC?.Dispose();
ErrorStream?.Dispose();
return ValueTask.CompletedTask;
try
{
await RPC.InvokeAsync("close");
}
catch (RemoteMethodNotFoundException e)
{
}
finally
{
RPC?.Dispose();
ErrorStream?.Dispose();
}
}
}
}

View file

@ -13,6 +13,7 @@ using Flow.Launcher.Plugin;
using ISavable = Flow.Launcher.Plugin.ISavable;
using Flow.Launcher.Plugin.SharedCommands;
using System.Text.Json;
using Flow.Launcher.Core.Resource;
namespace Flow.Launcher.Core.Plugin
{
@ -51,7 +52,7 @@ namespace Flow.Launcher.Core.Plugin
}
/// <summary>
/// Save json and ISavable
/// Save json and ISavable
/// </summary>
public static void Save()
{
@ -90,6 +91,48 @@ namespace Flow.Launcher.Core.Plugin
}).ToArray());
}
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
}
public static async Task CloseExternalPreviewAsync()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.ClosePreviewAsync(),
_ => Task.CompletedTask,
}).ToArray());
}
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
}
public static bool UseExternalPreview()
{
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
}
public static bool AllowAlwaysPreview()
{
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled);
if (plugin is null)
return false;
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
}
static PluginManager()
{
// validate user directory
@ -160,12 +203,21 @@ namespace Flow.Launcher.Core.Plugin
}
}
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
InternationalizationManager.Instance.ChangeLanguage(InternationalizationManager.Instance.Settings.Language);
if (failedPlugins.Any())
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
API.ShowMsg($"Fail to Init Plugins",
$"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help",
"", false);
API.ShowMsg(
InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsTitle"),
string.Format(
InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsMessage"),
failed
),
"",
false
);
}
}
@ -173,11 +225,11 @@ namespace Flow.Launcher.Core.Plugin
{
if (query is null)
return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
return GlobalPlugins;
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair>
{
@ -237,8 +289,8 @@ namespace Flow.Launcher.Core.Plugin
r.PluginID = metadata.ID;
r.OriginQuery = query;
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
if (metadata.ActionKeywords.Count == 1)
r.ActionKeywordAssigned = query.ActionKeyword;
}
@ -256,7 +308,8 @@ namespace Flow.Launcher.Core.Plugin
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
{
return AllPlugins.Where(p => p.Plugin is T);
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty<PluginPair>();
}
public static List<Result> GetContextMenusForPlugin(Result result)
@ -380,7 +433,8 @@ namespace Flow.Launcher.Core.Plugin
/// <summary>
/// Update a plugin to new version, from a zip file. Will Delete zip after updating.
/// 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)
{
@ -390,11 +444,11 @@ namespace Flow.Launcher.Core.Plugin
}
/// <summary>
/// Install a plugin. Will Delete zip after updating.
/// 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, true);
InstallPlugin(plugin, zipFilePath, checkModified: true);
}
/// <summary>
@ -420,7 +474,9 @@ namespace Flow.Launcher.Core.Plugin
// Unzip plugin files to temp folder
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
File.Delete(zipFilePath);
if(!plugin.IsFromLocalInstallPath)
File.Delete(zipFilePath);
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
@ -463,7 +519,7 @@ namespace Flow.Launcher.Core.Plugin
var newPluginPath = Path.Combine(installDirectory, folderName);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, MessageBoxEx.Show);
Directory.Delete(tempFolderPluginPath, true);

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows;
using Flow.Launcher.Core.ExternalPlugins.Environments;
#pragma warning disable IDE0005
using Flow.Launcher.Infrastructure.Logger;
@ -119,10 +119,10 @@ namespace Flow.Launcher.Core.Plugin
_ = Task.Run(() =>
{
MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
MessageBoxEx.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
$"Please refer to the logs for more information", "",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
MessageBoxButton.OK, MessageBoxImage.Warning);
});
}

View file

@ -82,10 +82,10 @@ namespace Flow.Launcher.Core.Plugin
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
ClientProcess.Kill(true);
await ClientProcess.WaitForExitAsync();
ClientProcess.Dispose();
await base.DisposeAsync();
}
}
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace Flow.Launcher.Core.Resource
{
@ -27,6 +27,9 @@ 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 Language Hebrew = new Language("he", "עברית");
public static List<Language> GetAvailableLanguages()
{
@ -54,7 +57,9 @@ namespace Flow.Launcher.Core.Resource
Slovak,
Turkish,
Czech,
Arabic
Arabic,
Vietnamese,
Hebrew
};
return languages;
}

View file

@ -25,10 +25,6 @@ namespace Flow.Launcher.Core.Resource
public Internationalization()
{
AddPluginLanguageDirectories();
LoadDefaultLanguage();
// we don't want to load /Languages/en.xaml twice
// so add flowlauncher language directory after load plugin language files
AddFlowLauncherLanguageDirectory();
}
@ -40,9 +36,9 @@ namespace Flow.Launcher.Core.Resource
}
private void AddPluginLanguageDirectories()
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
{
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
foreach (var plugin in plugins)
{
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
var dir = Path.GetDirectoryName(location);
@ -56,10 +52,15 @@ namespace Flow.Launcher.Core.Resource
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
}
}
LoadDefaultLanguage();
}
private void LoadDefaultLanguage()
{
// Removes language files loaded before any plugins were loaded.
// Prevents the language Flow started in from overwriting English if the user switches back to English
RemoveOldLanguageFiles();
LoadLanguage(AvailableLanguages.English);
_oldResources.Clear();
}
@ -96,13 +97,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;
@ -126,7 +124,7 @@ namespace Flow.Launcher.Core.Resource
// "Do you want to search with pinyin?"
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
if (MessageBoxEx.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
return false;
return true;
@ -143,11 +141,14 @@ namespace Flow.Launcher.Core.Resource
private void LoadLanguage(Language language)
{
var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile);
var dicts = Application.Current.Resources.MergedDictionaries;
var filename = $"{language.LanguageCode}{Extension}";
var files = _languageDirectories
.Select(d => LanguageFile(d, filename))
.Where(f => !string.IsNullOrEmpty(f))
// Exclude Flow's English language file since it's built into the binary, and there's no need to load
// it again from the file system.
.Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile)
.ToArray();
if (files.Length > 0)
@ -193,7 +194,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)
{

View file

@ -2,10 +2,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Effects;
@ -17,6 +16,10 @@ namespace Flow.Launcher.Core.Resource
{
public class Theme
{
private const string ThemeMetadataNamePrefix = "Name:";
private const string ThemeMetadataIsDarkPrefix = "IsDark:";
private const string ThemeMetadataHasBlurPrefix = "HasBlur:";
private const int ShadowExtraMargin = 32;
private readonly List<string> _themeDirectories = new List<string>();
@ -79,33 +82,33 @@ namespace Flow.Launcher.Core.Resource
{
if (string.IsNullOrEmpty(path))
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
// reload all resources even if the theme itself hasn't changed in order to pickup changes
// to things like fonts
UpdateResourceDictionary(GetResourceDictionary(theme));
Settings.Theme = theme;
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
if (_oldTheme != theme || theme == defaultTheme)
{
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
BlurEnabled = IsBlurTheme();
BlurEnabled = Win32Helper.IsBlurTheme();
if (Settings.UseDropShadowEffect && !BlurEnabled)
AddDropShadowEffectToCurrentTheme();
SetBlurForWindow();
Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
}
catch (DirectoryNotFoundException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
if (theme != defaultTheme)
{
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
MessageBoxEx.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
ChangeTheme(defaultTheme);
}
return false;
@ -115,7 +118,7 @@ namespace Flow.Launcher.Core.Resource
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
if (theme != defaultTheme)
{
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
MessageBoxEx.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
ChangeTheme(defaultTheme);
}
return false;
@ -148,7 +151,7 @@ namespace Flow.Launcher.Core.Resource
public ResourceDictionary GetResourceDictionary(string theme)
{
var dict = GetThemeResourceDictionary(theme);
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
{
@ -176,8 +179,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 +190,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;
@ -205,17 +222,53 @@ namespace Flow.Launcher.Core.Resource
return GetResourceDictionary(Settings.Theme);
}
public List<string> LoadAvailableThemes()
public List<ThemeData> LoadAvailableThemes()
{
List<string> themes = new List<string>();
List<ThemeData> themes = new List<ThemeData>();
foreach (var themeDirectory in _themeDirectories)
{
themes.AddRange(
Directory.GetFiles(themeDirectory)
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
.ToList());
var filePaths = Directory
.GetFiles(themeDirectory)
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
.Select(GetThemeDataFromPath);
themes.AddRange(filePaths);
}
return themes.OrderBy(o => o).ToList();
return themes.OrderBy(o => o.Name).ToList();
}
private ThemeData GetThemeDataFromPath(string path)
{
using var reader = XmlReader.Create(path);
reader.Read();
var extensionlessName = Path.GetFileNameWithoutExtension(path);
if (reader.NodeType is not XmlNodeType.Comment)
return new ThemeData(extensionlessName, extensionlessName);
var commentLines = reader.Value.Trim().Split('\n').Select(v => v.Trim());
var name = extensionlessName;
bool? isDark = null;
bool? hasBlur = null;
foreach (var line in commentLines)
{
if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase))
{
name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim();
}
else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase))
{
isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim());
}
else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase))
{
hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim());
}
}
return new ThemeData(extensionlessName, name, isDark, hasBlur);
}
private string GetThemePath(string themeName)
@ -302,96 +355,6 @@ namespace Flow.Launcher.Core.Resource
UpdateResourceDictionary(dict);
}
#region Blur Handling
/*
Found on https://github.com/riverar/sample-win10-aeroglass
*/
private enum AccentState
{
ACCENT_DISABLED = 0,
ACCENT_ENABLE_GRADIENT = 1,
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
ACCENT_ENABLE_BLURBEHIND = 3,
ACCENT_INVALID_STATE = 4
}
[StructLayout(LayoutKind.Sequential)]
private struct AccentPolicy
{
public AccentState AccentState;
public int AccentFlags;
public int GradientColor;
public int AnimationId;
}
[StructLayout(LayoutKind.Sequential)]
private struct WindowCompositionAttributeData
{
public WindowCompositionAttribute Attribute;
public IntPtr Data;
public int SizeOfData;
}
private enum WindowCompositionAttribute
{
WCA_ACCENT_POLICY = 19
}
[DllImport("user32.dll")]
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
/// <summary>
/// Sets the blur for a window via SetWindowCompositionAttribute
/// </summary>
public void SetBlurForWindow()
{
if (BlurEnabled)
{
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
}
else
{
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
}
}
private bool IsBlurTheme()
{
if (Environment.OSVersion.Version >= new Version(6, 2))
{
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
if (resource is bool)
return (bool)resource;
return false;
}
return false;
}
private void SetWindowAccent(Window w, AccentState state)
{
var windowHelper = new WindowInteropHelper(w);
windowHelper.EnsureHandle();
var accent = new AccentPolicy { AccentState = state };
var accentStructSize = Marshal.SizeOf(accent);
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
Marshal.StructureToPtr(accent, accentPtr, false);
var data = new WindowCompositionAttributeData
{
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
SizeOfData = accentStructSize,
Data = accentPtr
};
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
Marshal.FreeHGlobal(accentPtr);
}
#endregion
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
}
}

View file

@ -53,7 +53,7 @@ namespace Flow.Launcher.Core
if (newReleaseVersion <= currentVersion)
{
if (!silentUpdate)
MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
MessageBoxEx.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
return;
}
@ -68,9 +68,9 @@ namespace Flow.Launcher.Core
if (DataLocation.PortableDataLocationInUse())
{
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination);
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination))
MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, MessageBoxEx.Show);
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, MessageBoxEx.Show))
MessageBoxEx.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
DataLocation.PortableDataPath,
targetDestination));
}
@ -83,7 +83,7 @@ namespace Flow.Launcher.Core
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
if (MessageBoxEx.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
UpdateManager.RestartApp(Constant.ApplicationFileName);
}

View file

@ -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";
@ -30,6 +31,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
public static string PythonPath;
public static string NodePath;

View file

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Windows.Win32;
namespace Flow.Launcher.Infrastructure
{
@ -54,10 +54,6 @@ namespace Flow.Launcher.Infrastructure
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
/// <summary>
@ -70,9 +66,9 @@ namespace Flow.Launcher.Infrastructure
var index = 0;
var numRemaining = hWnds.Count;
EnumWindows((wnd, _) =>
PInvoke.EnumWindows((wnd, _) =>
{
var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32());
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
if (searchIndex != -1)
{
z[searchIndex] = index;

View file

@ -35,6 +35,10 @@
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="NativeMethods.txt" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SolutionAssemblyInfo.cs" Link="Properties\SolutionAssemblyInfo.cs" />
<None Include="FodyWeavers.xml" />
@ -49,16 +53,18 @@
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="FastCache.Cached" Version="1.8.2" />
<PackageReference Include="BitFaster.Caching" Version="2.5.3" />
<PackageReference Include="Fody" Version="6.5.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MemoryPack" Version="1.10.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.7.30" />
<PackageReference Include="MemoryPack" Version="1.21.3" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.12.19" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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-->

View file

@ -1,6 +1,11 @@
using System;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Flow.Launcher.Plugin;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Flow.Launcher.Infrastructure.Hotkey
{
@ -10,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey
/// </summary>
public unsafe class GlobalHotkey : IDisposable
{
private static readonly IntPtr hookId;
private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback;
private static readonly UnhookWindowsHookExSafeHandle hookId;
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
internal static Func<KeyEvent, int, SpecialKeyState, bool> hookedKeyboardCallback;
//Modifier key constants
private const int VK_SHIFT = 0x10;
private const int VK_CONTROL = 0x11;
private const int VK_ALT = 0x12;
private const int VK_WIN = 91;
static GlobalHotkey()
{
// Set the hook
hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc);
hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL);
}
private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId)
{
using var curProcess = Process.GetCurrentProcess();
using var curModule = curProcess.MainModule;
return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0);
}
public static SpecialKeyState CheckModifiers()
{
SpecialKeyState state = new SpecialKeyState();
if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0)
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0)
{
//SHIFT is pressed
state.ShiftPressed = true;
}
if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0)
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0)
{
//CONTROL is pressed
state.CtrlPressed = true;
}
if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0)
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0)
{
//ALT is pressed
state.AltPressed = true;
}
if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0)
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 ||
(PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0)
{
//WIN is pressed
state.WinPressed = true;
@ -56,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey
return state;
}
[UnmanagedCallersOnly]
private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam)
{
bool continues = true;
if (nCode >= 0)
{
if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN ||
wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP ||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN ||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP)
if (wParam.Value == (int)KeyEvent.WM_KEYDOWN ||
wParam.Value == (int)KeyEvent.WM_KEYUP ||
wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN ||
wParam.Value == (int)KeyEvent.WM_SYSKEYUP)
{
if (hookedKeyboardCallback != null)
continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers());
continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers());
}
}
if (continues)
{
return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam);
return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam);
}
return (IntPtr)(-1);
return new LRESULT(1);
}
public void Dispose()
{
InterceptKeys.UnhookWindowsHookEx(hookId);
hookId.Dispose();
}
~GlobalHotkey()
@ -90,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey
Dispose();
}
}
}
}

View file

@ -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);

View 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; }
}

View file

@ -1,38 +0,0 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Flow.Launcher.Infrastructure.Hotkey
{
internal static unsafe class InterceptKeys
{
public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
private const int WH_KEYBOARD_LL = 13;
public static IntPtr SetHook(delegate* unmanaged<int, UIntPtr, IntPtr, IntPtr> proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(int idHook, delegate* unmanaged<int, UIntPtr, IntPtr, IntPtr> lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
public static extern short GetKeyState(int keyCode);
}
}

View file

@ -1,3 +1,5 @@
using Windows.Win32;
namespace Flow.Launcher.Infrastructure.Hotkey
{
public enum KeyEvent
@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey
/// <summary>
/// Key down
/// </summary>
WM_KEYDOWN = 256,
WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN,
/// <summary>
/// Key up
/// </summary>
WM_KEYUP = 257,
WM_KEYUP = (int)PInvoke.WM_KEYUP,
/// <summary>
/// System key up
/// </summary>
WM_SYSKEYUP = 261,
WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP,
/// <summary>
/// System key down
/// </summary>
WM_SYSKEYDOWN = 260
WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN
}
}
}

View 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;
}
}

View file

@ -3,33 +3,23 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using FastCache;
using FastCache.Services;
using BitFaster.Caching.Lfu;
namespace Flow.Launcher.Infrastructure.Image
{
public class ImageUsage
{
public int usage;
public ImageSource imageSource;
public ImageUsage(int usage, ImageSource image)
{
this.usage = usage;
imageSource = image;
}
}
public class ImageCache
{
private const int MaxCached = 150;
public void Initialize(Dictionary<(string, bool), int> usage)
private ConcurrentLfu<(string, bool), ImageSource> CacheManager { get; set; } = new(MaxCached);
public void Initialize(IEnumerable<(string, bool)> usage)
{
foreach (var key in usage.Keys)
foreach (var key in usage)
{
Cached<ImageUsage>.Save(key, new ImageUsage(usage[key], null), TimeSpan.MaxValue, MaxCached);
CacheManager.AddOrUpdate(key, null);
}
}
@ -37,48 +27,42 @@ namespace Flow.Launcher.Infrastructure.Image
{
get
{
if (!Cached<ImageUsage>.TryGet((path, isFullImage), out var value))
{
return null;
}
value.Value.usage++;
return value.Value.imageSource;
return CacheManager.TryGet((path, isFullImage), out var value) ? value : null;
}
set
{
if (Cached<ImageUsage>.TryGet((path, isFullImage), out var cached))
{
cached.Value.imageSource = value;
cached.Value.usage++;
}
Cached<ImageUsage>.Save((path, isFullImage), new ImageUsage(0, value), TimeSpan.MaxValue,
MaxCached);
CacheManager.AddOrUpdate((path, isFullImage), value);
}
}
public async ValueTask<ImageSource> GetOrAddAsync(string key,
Func<(string, bool), Task<ImageSource>> valueFactory,
bool isFullImage = false)
{
return await CacheManager.GetOrAddAsync((key, isFullImage), valueFactory);
}
public bool ContainsKey(string key, bool isFullImage)
{
return Cached<ImageUsage>.TryGet((key, isFullImage), out _);
return CacheManager.TryGet((key, isFullImage), out _);
}
public bool TryGetValue(string key, bool isFullImage, out ImageSource image)
{
if (Cached<ImageUsage>.TryGet((key, isFullImage), out var value))
if (CacheManager.TryGet((key, isFullImage), out var value))
{
image = value.Value.imageSource;
value.Value.usage++;
image = value;
return image != null;
}
image = null;
return false;
}
public int CacheSize()
{
return CacheManager.TotalCount<(string, bool), ImageUsage>();
return CacheManager.Count;
}
/// <summary>
@ -86,14 +70,14 @@ namespace Flow.Launcher.Infrastructure.Image
/// </summary>
public int UniqueImagesInCache()
{
return CacheManager.EnumerateEntries<(string, bool), ImageUsage>().Select(x => x.Value.imageSource)
return CacheManager.Select(x => x.Value)
.Distinct()
.Count();
}
public IEnumerable<Cached<(string, bool), ImageUsage>> EnumerateEntries()
public IEnumerable<KeyValuePair<(string, bool), ImageSource>> EnumerateEntries()
{
return CacheManager.EnumerateEntries<(string, bool), ImageUsage>();
return CacheManager;
}
}
}

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Flow.Launcher.Infrastructure.Logger;
@ -17,10 +18,11 @@ namespace Flow.Launcher.Infrastructure.Image
{
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 BinaryStorage<List<(string, bool)>> _storage;
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
private static IImageHashGenerator _hashGenerator;
private static readonly bool EnableImageHash = true;
public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon));
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
public const int SmallIconSize = 64;
@ -31,12 +33,12 @@ namespace Flow.Launcher.Infrastructure.Image
public static async Task InitializeAsync()
{
_storage = new BinaryStorage<Dictionary<(string, bool), int>>("Image");
_storage = new BinaryStorage<List<(string, bool)>>("Image");
_hashGenerator = new ImageHashGenerator();
var usage = await LoadStorageToConcurrentDictionaryAsync();
ImageCache.Initialize(usage.ToDictionary(x => x.Key, x => x.Value));
ImageCache.Initialize(usage);
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
{
@ -49,7 +51,7 @@ namespace Flow.Launcher.Infrastructure.Image
{
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
{
foreach (var ((path, isFullImage), _) in usage)
foreach (var (path, isFullImage) in usage)
{
await LoadAsync(path, isFullImage);
}
@ -66,9 +68,8 @@ namespace Flow.Launcher.Infrastructure.Image
try
{
await _storage.SaveAsync(ImageCache.EnumerateEntries()
.ToDictionary(
x => x.Key,
x => x.Value.usage));
.Select(x => x.Key)
.ToList());
}
finally
{
@ -76,14 +77,12 @@ namespace Flow.Launcher.Infrastructure.Image
}
}
private static async Task<ConcurrentDictionary<(string, bool), int>> LoadStorageToConcurrentDictionaryAsync()
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
{
await storageLock.WaitAsync();
try
{
var loaded = await _storage.TryLoadAsync(new Dictionary<(string, bool), int>());
return new ConcurrentDictionary<(string, bool), int>(loaded);
return await _storage.TryLoadAsync(new List<(string, bool)>());
}
finally
{
@ -141,7 +140,7 @@ namespace Flow.Launcher.Infrastructure.Image
return new ImageResult(image, ImageType.ImageFile);
}
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
if (path.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
var imageSource = new BitmapImage(new Uri(path));
imageSource.Freeze();
@ -217,8 +216,16 @@ namespace Flow.Launcher.Infrastructure.Image
type = ImageType.ImageFile;
if (loadFullImage)
{
image = LoadFullImage(path);
type = ImageType.FullImageFile;
try
{
image = LoadFullImage(path);
type = ImageType.FullImageFile;
}
catch (NotSupportedException)
{
image = Image;
type = ImageType.Error;
}
}
else
{

View file

@ -1,12 +1,19 @@
using System;
using System.Runtime.InteropServices;
using System.IO;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using System.Windows;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.Graphics.Gdi;
namespace Flow.Launcher.Infrastructure.Image
{
/// <summary>
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/>
/// </summary>
[Flags]
public enum ThumbnailOptions
{
@ -22,91 +29,13 @@ namespace Flow.Launcher.Infrastructure.Image
{
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SHCreateItemFromParsingName(
[MarshalAs(UnmanagedType.LPWStr)] string path,
IntPtr pbc,
ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteObject(IntPtr hObject);
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
internal interface IShellItem
{
void BindToHandler(IntPtr pbc,
[MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
[MarshalAs(UnmanagedType.LPStruct)]Guid riid,
out IntPtr ppv);
void GetParent(out IShellItem ppsi);
void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
void Compare(IShellItem psi, uint hint, out int piOrder);
};
internal enum SIGDN : uint
{
NORMALDISPLAY = 0,
PARENTRELATIVEPARSING = 0x80018001,
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
DESKTOPABSOLUTEPARSING = 0x80028000,
PARENTRELATIVEEDITING = 0x80031001,
DESKTOPABSOLUTEEDITING = 0x8004c000,
FILESYSPATH = 0x80058000,
URL = 0x80068000
}
internal enum HResult
{
Ok = 0x0000,
False = 0x0001,
InvalidArguments = unchecked((int)0x80070057),
OutOfMemory = unchecked((int)0x8007000E),
NoInterface = unchecked((int)0x80004002),
Fail = unchecked((int)0x80004005),
ExtractionFailed = unchecked((int)0x8004B200),
ElementNotFound = unchecked((int)0x80070490),
TypeElementNotFound = unchecked((int)0x8002802B),
NoObject = unchecked((int)0x800401E5),
Win32ErrorCanceled = 1223,
Canceled = unchecked((int)0x800704C7),
ResourceInUse = unchecked((int)0x800700AA),
AccessDenied = unchecked((int)0x80030005)
}
[ComImportAttribute()]
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IShellItemImageFactory
{
[PreserveSig]
HResult GetImage(
[In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
[In] ThumbnailOptions flags,
[Out] out IntPtr phbm);
}
[StructLayout(LayoutKind.Sequential)]
internal struct NativeSize
{
private int width;
private int height;
public int Width { set { width = value; } }
public int Height { set { height = value; } }
};
private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID;
private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200;
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
try
{
@ -115,39 +44,56 @@ namespace Flow.Launcher.Infrastructure.Image
finally
{
// delete HBitmap to avoid memory leaks
DeleteObject(hBitmap);
PInvoke.DeleteObject(hBitmap);
}
}
private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
{
IShellItem nativeShellItem;
Guid shellItem2Guid = new Guid(IShellItem2Guid);
int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
if (retCode != 0)
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
{
var retCode = PInvoke.SHCreateItemFromParsingName(
fileName,
null,
GUID_IShellItem,
out var nativeShellItem);
if (retCode != HRESULT.S_OK)
throw Marshal.GetExceptionForHR(retCode);
NativeSize nativeSize = new NativeSize
if (nativeShellItem is not IShellItemImageFactory imageFactory)
{
Width = width,
Height = height
};
IntPtr hBitmap;
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);
// if extracting image thumbnail and failed, extract shell icon
if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed)
{
hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap);
Marshal.ReleaseComObject(nativeShellItem);
nativeShellItem = null;
throw new InvalidOperationException("Failed to get IShellItemImageFactory");
}
Marshal.ReleaseComObject(nativeShellItem);
SIZE size = new SIZE
{
cx = width,
cy = height
};
if (hr == HResult.Ok) return hBitmap;
HBITMAP hBitmap = default;
try
{
try
{
imageFactory.GetImage(size, (SIIGBF)options, &hBitmap);
}
catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly)
{
// Fallback to IconOnly if ThumbnailOnly fails
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
}
}
finally
{
if (nativeShellItem != null)
{
Marshal.ReleaseComObject(nativeShellItem);
}
}
throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr));
return hBitmap;
}
}
}
}

View file

@ -0,0 +1,19 @@
SHCreateItemFromParsingName
DeleteObject
IShellItem
IShellItemImageFactory
S_OK
SetWindowsHookEx
UnhookWindowsHookEx
CallNextHookEx
GetModuleHandle
GetKeyState
VIRTUAL_KEY
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
EnumWindows

View file

@ -67,7 +67,7 @@ namespace Flow.Launcher.Infrastructure.Storage
}
catch (System.Exception e)
{
Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
return defaultData;
}
}

View file

@ -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
{
@ -42,7 +55,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
OnPropertyChanged(nameof(MaxResultsToShow));
}
}
public bool UseDropShadowEffect { get; set; } = false;
public bool UseDropShadowEffect { get; set; } = true;
/* 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,6 +71,10 @@ 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;
@ -64,8 +88,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public double SettingWindowWidth { get; set; } = 1000;
public double SettingWindowHeight { get; set; } = 700;
public double SettingWindowTop { get; set; }
public double SettingWindowLeft { get; set; }
public double? SettingWindowTop { get; set; } = null;
public double? SettingWindowLeft { get; set; } = null;
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
public int CustomExplorerIndex { get; set; } = 0;
@ -161,35 +185,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/// when false Alphabet static service will always return empty results
/// </summary>
public bool ShouldUsePinyin { get; set; } = false;
public bool AlwaysPreview { get; set; } = false;
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;
}
}
@ -197,17 +207,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; }
@ -219,7 +230,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)
};
@ -243,7 +254,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;
@ -260,16 +271,108 @@ 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
{
Selected,
Empty,
Preserved
Preserved,
ActionKeywordPreserved,
ActionKeywordSelected
}
public enum ColorSchemes
@ -278,7 +381,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
Light,
Dark
}
public enum SearchWindowScreens
{
RememberLastLaunchLocation,
@ -287,7 +390,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
Primary,
Custom
}
public enum SearchWindowAligns
{
Center,

View file

@ -0,0 +1,101 @@
using System;
using System.Runtime.InteropServices;
using System.Windows.Interop;
using System.Windows;
namespace Flow.Launcher.Infrastructure
{
public static class Win32Helper
{
#region Blur Handling
/*
Found on https://github.com/riverar/sample-win10-aeroglass
*/
private enum AccentState
{
ACCENT_DISABLED = 0,
ACCENT_ENABLE_GRADIENT = 1,
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
ACCENT_ENABLE_BLURBEHIND = 3,
ACCENT_INVALID_STATE = 4
}
[StructLayout(LayoutKind.Sequential)]
private struct AccentPolicy
{
public AccentState AccentState;
public int AccentFlags;
public int GradientColor;
public int AnimationId;
}
[StructLayout(LayoutKind.Sequential)]
private struct WindowCompositionAttributeData
{
public WindowCompositionAttribute Attribute;
public IntPtr Data;
public int SizeOfData;
}
private enum WindowCompositionAttribute
{
WCA_ACCENT_POLICY = 19
}
[DllImport("user32.dll")]
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
/// <summary>
/// Checks if the blur theme is enabled
/// </summary>
public static bool IsBlurTheme()
{
if (Environment.OSVersion.Version >= new Version(6, 2))
{
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
if (resource is bool b)
return b;
return false;
}
return false;
}
/// <summary>
/// Sets the blur for a window via SetWindowCompositionAttribute
/// </summary>
public static void SetBlurForWindow(Window w, bool blur)
{
SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED);
}
private static void SetWindowAccent(Window w, AccentState state)
{
var windowHelper = new WindowInteropHelper(w);
windowHelper.EnsureHandle();
var accent = new AccentPolicy { AccentState = state };
var accentStructSize = Marshal.SizeOf(accent);
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
Marshal.StructureToPtr(accent, accentPtr, false);
var data = new WindowCompositionAttributeData
{
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
SizeOfData = accentStructSize,
Data = accentPtr
};
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
Marshal.FreeHGlobal(accentPtr);
}
#endregion
}
}

View file

@ -14,10 +14,10 @@
</PropertyGroup>
<PropertyGroup>
<Version>4.2.0</Version>
<PackageVersion>4.2.0</PackageVersion>
<AssemblyVersion>4.2.0</AssemblyVersion>
<FileVersion>4.2.0</FileVersion>
<Version>4.4.0</Version>
<PackageVersion>4.4.0</PackageVersion>
<AssemblyVersion>4.4.0</AssemblyVersion>
<FileVersion>4.4.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -57,7 +57,11 @@
</PropertyGroup>
<ItemGroup>
<None Include="Readme.md" Pack="true" PackagePath="\"/>
<AdditionalFiles Include="NativeMethods.txt" />
</ItemGroup>
<ItemGroup>
<None Include="Readme.md" Pack="true" PackagePath="\" />
<None Include="FodyWeavers.xml" />
</ItemGroup>
@ -67,7 +71,11 @@
<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.3.0" />
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
</ItemGroup>

View file

@ -0,0 +1,40 @@
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// This interface is for plugins that wish to provide file preview (external preview)
/// via a third party app instead of the default preview.
/// </summary>
public interface IAsyncExternalPreview : IFeatures
{
/// <summary>
/// Method for opening/showing the preview.
/// </summary>
/// <param name="path">The file path to open the preview for</param>
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
public Task OpenPreviewAsync(string path, bool sendFailToast = true);
/// <summary>
/// Method for closing/hiding the preview.
/// </summary>
public Task ClosePreviewAsync();
/// <summary>
/// Method for switching the preview to the next file result.
/// This requires the external preview be already open/showing
/// </summary>
/// <param name="path">The file path to switch the preview for</param>
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
public Task SwitchPreviewAsync(string path, bool sendFailToast = true);
/// <summary>
/// Allows the preview plugin to override the AlwaysPreview setting. Typically useful if plugin's preview does not
/// fully work well with being shown together when the query window appears with results.
/// When AlwaysPreview setting is on and this is set to false, the preview will not be shown when query
/// window appears with results, instead the internal preview will be shown.
/// </summary>
/// <returns></returns>
public bool AllowAlwaysPreview();
}
}

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
@ -7,6 +7,7 @@ using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace Flow.Launcher.Plugin
{
@ -294,9 +295,26 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Reloads the query.
/// This method should run
/// This method should run when selected item is from query results.
/// </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);
/// <summary>
/// Back to the query results.
/// This method should run when selected item is from context menu or history.
/// </summary>
public void BackToQueryResults();
/// <summary>
/// Displays a standardised Flow message box.
/// </summary>
/// <param name="messageBoxText">The message of the message box.</param>
/// <param name="caption">The caption of the message box.</param>
/// <param name="button">Specifies which button or buttons to display.</param>
/// <param name="icon">Specifies the icon to display.</param>
/// <param name="defaultResult">Specifies the default result of the message box.</param>
/// <returns>Specifies which message box button is clicked by the user.</returns>
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK);
}
}

View file

@ -0,0 +1,3 @@
EnumThreadWindows
GetWindowText
GetWindowTextLength

View file

@ -70,7 +70,8 @@ namespace Flow.Launcher.Plugin
&& !string.IsNullOrEmpty(PluginDirectory)
&& !Path.IsPathRooted(value)
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
_icoPath = Path.Combine(PluginDirectory, value);
}
@ -156,27 +157,6 @@ namespace Flow.Launcher.Plugin
}
}
/// <inheritdoc />
public override bool Equals(object obj)
{
var r = obj as Result;
var equality = string.Equals(r?.Title, Title) &&
string.Equals(r?.SubTitle, SubTitle) &&
string.Equals(r?.AutoCompleteText, AutoCompleteText) &&
string.Equals(r?.CopyText, CopyText) &&
string.Equals(r?.IcoPath, IcoPath) &&
TitleHighlightData == r.TitleHighlightData;
return equality;
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Title, SubTitle, AutoCompleteText, CopyText, IcoPath);
}
/// <inheritdoc />
public override string ToString()
{
@ -204,7 +184,7 @@ namespace Flow.Launcher.Plugin
Score = Score,
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory
PluginDirectory = PluginDirectory,
};
}
@ -258,10 +238,20 @@ 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;
/// <summary>
/// Determines if the user selection count should be added to the score. This can be useful when set to false to allow the result sequence order to be the same everytime instead of changing based on selection.
/// </summary>
public bool AddSelectedCount { get; set; } = true;
/// <summary>
/// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users.
/// </summary>
public const int MaxScore = int.MaxValue;
/// <summary>
/// Info of the preview section of a <see cref="Result"/>
/// </summary>
@ -270,12 +260,12 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Full image used for preview panel
/// </summary>
public string PreviewImagePath { get; set; }
public string PreviewImagePath { get; set; } = null;
/// <summary>
/// Determines if the preview image should occupy the full width of the preview panel.
/// </summary>
public bool IsMedia { get; set; }
public bool IsMedia { get; set; } = false;
/// <summary>
/// Result description text that is shown at the bottom of the preview panel.
@ -283,12 +273,17 @@ namespace Flow.Launcher.Plugin
/// <remarks>
/// When a value is not set, the <see cref="SubTitle"/> will be used.
/// </remarks>
public string Description { get; set; }
public string Description { get; set; } = null;
/// <summary>
/// Delegate to get the preview panel's image
/// </summary>
public IconDelegate PreviewDelegate { get; set; }
public IconDelegate PreviewDelegate { get; set; } = null;
/// <summary>
/// File path of the result. For third-party programs providing external preview.
/// </summary>
public string FilePath { get; set; } = null;
/// <summary>
/// Default instance of <see cref="PreviewInfo"/>
@ -299,6 +294,7 @@ namespace Flow.Launcher.Plugin
Description = null,
IsMedia = false,
PreviewDelegate = null,
FilePath = null,
};
}
}

View file

@ -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
@ -20,7 +21,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="targetPath"></param>
public static void CopyAll(this string sourcePath, string targetPath)
/// <param name="messageBoxExShow"></param>
public static void CopyAll(this string sourcePath, string targetPath, Func<string, MessageBoxResult> messageBoxExShow = null)
{
// Get the subdirectories for the specified directory.
DirectoryInfo dir = new DirectoryInfo(sourcePath);
@ -53,7 +55,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
foreach (DirectoryInfo subdir in dirs)
{
string temppath = Path.Combine(targetPath, subdir.Name);
CopyAll(subdir.FullName, temppath);
CopyAll(subdir.FullName, temppath, messageBoxExShow);
}
}
catch (Exception)
@ -61,8 +63,9 @@ namespace Flow.Launcher.Plugin.SharedCommands
#if DEBUG
throw;
#else
MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
RemoveFolderIfExists(targetPath);
messageBoxExShow ??= MessageBox.Show;
messageBoxExShow(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
RemoveFolderIfExists(targetPath, messageBoxExShow);
#endif
}
@ -74,8 +77,9 @@ namespace Flow.Launcher.Plugin.SharedCommands
/// </summary>
/// <param name="fromPath"></param>
/// <param name="toPath"></param>
/// <param name="messageBoxExShow"></param>
/// <returns></returns>
public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath)
public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath, Func<string, MessageBoxResult> messageBoxExShow = null)
{
try
{
@ -95,7 +99,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
#if DEBUG
throw;
#else
MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
messageBoxExShow ??= MessageBox.Show;
messageBoxExShow(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
return false;
#endif
}
@ -106,7 +111,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
/// Deletes a folder if it exists
/// </summary>
/// <param name="path"></param>
public static void RemoveFolderIfExists(this string path)
/// <param name="messageBoxExShow"></param>
public static void RemoveFolderIfExists(this string path, Func<string, MessageBoxResult> messageBoxExShow = null)
{
try
{
@ -118,7 +124,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
#if DEBUG
throw;
#else
MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
messageBoxExShow ??= MessageBox.Show;
messageBoxExShow(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
#endif
}
}
@ -147,7 +154,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
/// Open a directory window (using the OS's default handler, usually explorer)
/// </summary>
/// <param name="fileOrFolderPath"></param>
public static void OpenPath(string fileOrFolderPath)
/// <param name="messageBoxExShow"></param>
public static void OpenPath(string fileOrFolderPath, Func<string, MessageBoxResult> messageBoxExShow = null)
{
var psi = new ProcessStartInfo
{
@ -165,7 +173,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
#if DEBUG
throw;
#else
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
messageBoxExShow ??= MessageBox.Show;
messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
#endif
}
}
@ -176,7 +185,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
/// <param name="filePath">File path</param>
/// <param name="workingDir">Working directory</param>
/// <param name="asAdmin">Open as Administrator</param>
public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false)
/// <param name="messageBoxExShow"></param>
public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false, Func<string, MessageBoxResult> messageBoxExShow = null)
{
var psi = new ProcessStartInfo
{
@ -195,11 +205,30 @@ namespace Flow.Launcher.Plugin.SharedCommands
#if DEBUG
throw;
#else
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", filePath));
messageBoxExShow ??= MessageBox.Show;
messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", filePath));
#endif
}
}
///<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.

View file

@ -2,18 +2,15 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Windows.Win32;
using Windows.Win32.Foundation;
namespace Flow.Launcher.Plugin.SharedCommands
{
public static class ShellCommand
{
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.dll")] static extern bool EnumThreadWindows(uint threadId, EnumThreadDelegate lpfn, IntPtr lParam);
[DllImport("user32.dll")] static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hwnd);
private static bool containsSecurityWindow;
@ -28,6 +25,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
CheckSecurityWindow();
Thread.Sleep(25);
}
while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open
{
containsSecurityWindow = false;
@ -42,24 +40,33 @@ namespace Flow.Launcher.Plugin.SharedCommands
{
ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads;
for (int i = 0; i < ptc.Count; i++)
EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
PInvoke.EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
}
private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam)
private static BOOL CheckSecurityThread(HWND hwnd, LPARAM lParam)
{
if (GetWindowTitle(hwnd) == "Windows Security")
containsSecurityWindow = true;
return true;
}
private static string GetWindowTitle(IntPtr hwnd)
private static unsafe string GetWindowTitle(HWND hwnd)
{
StringBuilder sb = new StringBuilder(GetWindowTextLength(hwnd) + 1);
GetWindowText(hwnd, sb, sb.Capacity);
return sb.ToString();
var capacity = PInvoke.GetWindowTextLength(hwnd) + 1;
int length;
Span<char> buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity];
fixed (char* pBuffer = buffer)
{
// If the window has no title bar or text, if the title bar is empty,
// or if the window or control handle is invalid, the return value is zero.
length = PInvoke.GetWindowText(hwnd, pBuffer, capacity);
}
return buffer[..length].ToString();
}
public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "", bool createNoWindow = false)
public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "",
string arguments = "", string verb = "", bool createNoWindow = false)
{
var info = new ProcessStartInfo
{

View file

@ -50,11 +50,11 @@
<ItemGroup>
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
</ItemGroup>
</Project>

View file

@ -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

View file

@ -63,28 +63,5 @@ namespace Flow.Launcher.Test.Plugins
})
};
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
{
var camelText = JsonSerializer.Serialize(reference, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var pascalText = JsonSerializer.Serialize(reference);
var results1 = await QueryAsync(new Query { Search = camelText }, default);
var results2 = await QueryAsync(new Query { Search = pascalText }, default);
Assert.IsNotNull(results1);
Assert.IsNotNull(results2);
foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result))
{
Assert.AreEqual(result1, result2);
Assert.AreEqual(result1, referenceResult);
Assert.IsNotNull(result1);
Assert.IsNotNull(result1.AsyncAction);
}
}
}
}

View file

@ -54,6 +54,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}"

View file

@ -2,6 +2,7 @@
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using Flow.Launcher.Core;
namespace Flow.Launcher
{
@ -43,7 +44,7 @@ namespace Flow.Launcher
else
{
string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned");
MessageBox.Show(msg);
MessageBoxEx.Show(msg);
}
}
}

View file

@ -20,11 +20,17 @@
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ui:ThemeResources.ThemeDictionaries>
</ui:ThemeResources>
<ui:XamlControlsResources />
<ResourceDictionary Source="pack://application:,,,/Resources/CustomControlTemplate.xaml" />
<ResourceDictionary Source="pack://application:,,,/Themes/Win11System.xaml" />
<ResourceDictionary Source="pack://application:,,,/Resources/SettingWindowStyle.xaml" />
<ResourceDictionary Source="pack://application:,,,/Themes/Win11Light.xaml" />
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View file

@ -62,6 +62,7 @@ namespace Flow.Launcher
_settingsVM = new SettingWindowViewModel(_updater, _portable);
_settings = _settingsVM.Settings;
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
@ -70,6 +71,9 @@ namespace Flow.Launcher
StringMatcher.Instance = _stringMatcher;
_stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision;
InternationalizationManager.Instance.Settings = _settings;
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
PluginManager.LoadPlugins(_settings.PluginSettings);
_mainVM = new MainViewModel(_settings);
@ -80,7 +84,7 @@ namespace Flow.Launcher
await PluginManager.InitializePluginsAsync(API);
await imageLoadertask;
var window = new MainWindow(_settings, _mainVM);
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
@ -90,10 +94,6 @@ namespace Flow.Launcher
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);
// main windows needs initialized before theme change because of blur settings
ThemeManager.Instance.Settings = _settings;
ThemeManager.Instance.ChangeTheme(_settings.Theme);

View file

@ -3,39 +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)
{
return value switch
{
true => ImeConversionModeValues.Alphanumeric,
_ => 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)
{
return value switch
{
true => InputMethodState.Off,
_ => 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();
}
}

View file

@ -2,40 +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
{
return (value, parameter) switch
{
(true, not null) => Visibility.Collapsed,
(_, not null) => Visibility.Visible,
(true, not null) => Visibility.Collapsed,
(_, not null) => Visibility.Visible,
(true, null) => Visibility.Visible,
(_, null) => 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)
{
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();
}
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();
}

View file

@ -7,43 +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 is not [double width, double height, CornerRadius radius])
{
return DependencyProperty.UnsetValue;
}
Path myPath = new Path();
if (width < Double.Epsilon || height < Double.Epsilon)
{
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;
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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -5,45 +5,44 @@ 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();
if (value.Length < 2)
return new Run(string.Empty);
for (var i = 0; i < text.Length; i++)
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 currentCharacter = text.Substring(i, 1);
var run = new Run(currentCharacter)
{
var currentCharacter = text.Substring(i, 1);
var run = new Run(currentCharacter)
{
Style = ShouldHighlight(highlightData, i) ? highlightStyle : null
};
textBlock.Inlines.Add(run);
}
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);
}
}

View file

@ -2,20 +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 is not [double size, bool isIconCircular])
throw new ArgumentException("IconRadiusConverter must have 2 parameters: [double, bool]");
namespace Flow.Launcher.Converters;
return isIconCircular ? size / 2 : size;
}
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();
}
}

View file

@ -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 InvalidOperationException();
return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
}

View file

@ -3,23 +3,22 @@ using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
namespace Flow.Launcher.Converters
namespace Flow.Launcher.Converters;
public class OrdinalConverter : IValueConverter
{
public class OrdinalConverter : 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 not ListBoxItem listBoxItem
|| ItemsControl.ItemsControlFromItemContainer(listBoxItem) is not ListBox listBox)
{
if (value is not ListBoxItem listBoxItem
|| ItemsControl.ItemsControlFromItemContainer(listBoxItem) is not ListBox listBox)
{
return 0;
}
var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
return res == 10 ? 0 : res; // 10th item => HOTKEY+0
return 0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new 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();
}

View file

@ -6,63 +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
{
// 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)
)
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;
if (values[1] is not ResultViewModel selectedItem)
return Binding.DoNothing;
try
{
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);
// 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.DefaultThreadCurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black);
// 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;
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);
if (ft.Width + offset > queryTextBox.ActualWidth || queryTextBox.HorizontalOffset != 0)
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();
}
}

View file

@ -3,28 +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)
{
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
{
"key" => key?.Key,
"modifiers" => key?.Modifiers,
_ => 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();
}
}

View file

@ -4,28 +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();
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);
}
namespace Flow.Launcher.Converters;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new 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();
}

View file

@ -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>

View file

@ -6,6 +6,7 @@ using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using Flow.Launcher.Core;
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,18 +56,20 @@ 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"));
MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
Close();
return;
}
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);
}
}
}

View file

@ -1,34 +1,35 @@
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel;
using System;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.SettingPages.ViewModels;
using Flow.Launcher.Core;
namespace Flow.Launcher
{
public partial class CustomShortcutSetting : Window
{
private SettingWindowViewModel viewModel;
private readonly SettingsPaneHotkeyViewModel _hotkeyVm;
public string Key { get; set; } = String.Empty;
public string Value { get; set; } = String.Empty;
private string originalKey { get; init; } = null;
private string originalValue { get; init; } = null;
private bool update { get; init; } = false;
private string originalKey { get; } = null;
private string originalValue { get; } = null;
private bool update { get; } = false;
public CustomShortcutSetting(SettingWindowViewModel vm)
public CustomShortcutSetting(SettingsPaneHotkeyViewModel vm)
{
viewModel = vm;
_hotkeyVm = vm;
InitializeComponent();
}
public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
public CustomShortcutSetting(string key, string value, SettingsPaneHotkeyViewModel vm)
{
viewModel = vm;
Key = key;
Value = value;
originalKey = key;
originalValue = value;
update = true;
_hotkeyVm = vm;
InitializeComponent();
}
@ -42,14 +43,13 @@ namespace Flow.Launcher
{
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
return;
}
// Check if key is modified or adding a new one
if (((update && originalKey != Key) || !update)
&& viewModel.ShortcutExists(Key))
if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
return;
}
DialogResult = !update || originalKey != Key || originalValue != Value;

View file

@ -4,7 +4,7 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<UseWindowsForms>false</UseWindowsForms>
<StartupObject>Flow.Launcher.App</StartupObject>
<ApplicationIcon>Resources\app.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
@ -83,20 +83,24 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Fody" Version="6.5.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
<PackageReference Include="NHotkey.Wpf" Version="2.1.1" />
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SharpVectors" Version="1.8.2" />
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.8" />
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
@ -111,6 +115,12 @@
</Content>
</ItemGroup>
<ItemGroup>
<None Update="Resources\dev.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="taskkill /f /fi &quot;IMAGENAME eq Flow.Launcher.exe&quot;" />
</Target>

View file

@ -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;
}
}
}

View file

@ -1,73 +1,66 @@
using System;
using System.Drawing.Printing;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Controls;
namespace Flow.Launcher.Helper
namespace Flow.Launcher.Helper;
public class DwmDropShadow
{
public class DwmDropShadow
/// <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
{
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
{
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;
}
}
catch (Exception)
{
// Probably dwmapi.dll not found (incompatible OS)
return false;
}
}
}
}
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 unsafe bool DropShadow(Window window)
{
try
{
WindowInteropHelper helper = new WindowInteropHelper(window);
int val = 2;
var ret1 = PInvoke.DwmSetWindowAttribute(new (helper.Handle), DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY, &val, 4);
if (ret1 == HRESULT.S_OK)
{
var m = new MARGINS { cyBottomHeight = 0, cxLeftWidth = 0, cxRightWidth = 0, cyTopHeight = 0 };
var ret2 = PInvoke.DwmExtendFrameIntoClientArea(new(helper.Handle), &m);
return ret2 == HRESULT.S_OK;
}
return false;
}
catch (Exception)
{
// Probably dwmapi.dll not found (incompatible OS)
return false;
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -4,100 +4,99 @@ using System;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.Core.Resource;
using System.Windows;
using Flow.Launcher.ViewModel;
using Flow.Launcher.Core;
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");
MessageBoxEx.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;
}
}

View file

@ -1,11 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.IO.Pipes;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@ -14,172 +8,6 @@ using System.Windows;
// modified to allow single instace restart
namespace Flow.Launcher.Helper
{
internal enum WM
{
NULL = 0x0000,
CREATE = 0x0001,
DESTROY = 0x0002,
MOVE = 0x0003,
SIZE = 0x0005,
ACTIVATE = 0x0006,
SETFOCUS = 0x0007,
KILLFOCUS = 0x0008,
ENABLE = 0x000A,
SETREDRAW = 0x000B,
SETTEXT = 0x000C,
GETTEXT = 0x000D,
GETTEXTLENGTH = 0x000E,
PAINT = 0x000F,
CLOSE = 0x0010,
QUERYENDSESSION = 0x0011,
QUIT = 0x0012,
QUERYOPEN = 0x0013,
ERASEBKGND = 0x0014,
SYSCOLORCHANGE = 0x0015,
SHOWWINDOW = 0x0018,
ACTIVATEAPP = 0x001C,
SETCURSOR = 0x0020,
MOUSEACTIVATE = 0x0021,
CHILDACTIVATE = 0x0022,
QUEUESYNC = 0x0023,
GETMINMAXINFO = 0x0024,
WINDOWPOSCHANGING = 0x0046,
WINDOWPOSCHANGED = 0x0047,
CONTEXTMENU = 0x007B,
STYLECHANGING = 0x007C,
STYLECHANGED = 0x007D,
DISPLAYCHANGE = 0x007E,
GETICON = 0x007F,
SETICON = 0x0080,
NCCREATE = 0x0081,
NCDESTROY = 0x0082,
NCCALCSIZE = 0x0083,
NCHITTEST = 0x0084,
NCPAINT = 0x0085,
NCACTIVATE = 0x0086,
GETDLGCODE = 0x0087,
SYNCPAINT = 0x0088,
NCMOUSEMOVE = 0x00A0,
NCLBUTTONDOWN = 0x00A1,
NCLBUTTONUP = 0x00A2,
NCLBUTTONDBLCLK = 0x00A3,
NCRBUTTONDOWN = 0x00A4,
NCRBUTTONUP = 0x00A5,
NCRBUTTONDBLCLK = 0x00A6,
NCMBUTTONDOWN = 0x00A7,
NCMBUTTONUP = 0x00A8,
NCMBUTTONDBLCLK = 0x00A9,
SYSKEYDOWN = 0x0104,
SYSKEYUP = 0x0105,
SYSCHAR = 0x0106,
SYSDEADCHAR = 0x0107,
COMMAND = 0x0111,
SYSCOMMAND = 0x0112,
MOUSEMOVE = 0x0200,
LBUTTONDOWN = 0x0201,
LBUTTONUP = 0x0202,
LBUTTONDBLCLK = 0x0203,
RBUTTONDOWN = 0x0204,
RBUTTONUP = 0x0205,
RBUTTONDBLCLK = 0x0206,
MBUTTONDOWN = 0x0207,
MBUTTONUP = 0x0208,
MBUTTONDBLCLK = 0x0209,
MOUSEWHEEL = 0x020A,
XBUTTONDOWN = 0x020B,
XBUTTONUP = 0x020C,
XBUTTONDBLCLK = 0x020D,
MOUSEHWHEEL = 0x020E,
CAPTURECHANGED = 0x0215,
ENTERSIZEMOVE = 0x0231,
EXITSIZEMOVE = 0x0232,
IME_SETCONTEXT = 0x0281,
IME_NOTIFY = 0x0282,
IME_CONTROL = 0x0283,
IME_COMPOSITIONFULL = 0x0284,
IME_SELECT = 0x0285,
IME_CHAR = 0x0286,
IME_REQUEST = 0x0288,
IME_KEYDOWN = 0x0290,
IME_KEYUP = 0x0291,
NCMOUSELEAVE = 0x02A2,
DWMCOMPOSITIONCHANGED = 0x031E,
DWMNCRENDERINGCHANGED = 0x031F,
DWMCOLORIZATIONCOLORCHANGED = 0x0320,
DWMWINDOWMAXIMIZEDCHANGE = 0x0321,
#region Windows 7
DWMSENDICONICTHUMBNAIL = 0x0323,
DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326,
#endregion
USER = 0x0400,
// This is the hard-coded message value used by WinForms for Shell_NotifyIcon.
// It's relatively safe to reuse.
TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024
APP = 0x8000
}
[SuppressUnmanagedCodeSecurity]
internal static class NativeMethods
{
/// <summary>
/// Delegate declaration that matches WndProc signatures.
/// </summary>
public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled);
[DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
[DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)]
private static extern IntPtr _LocalFree(IntPtr hMem);
public static string[] CommandLineToArgvW(string cmdLine)
{
IntPtr argv = IntPtr.Zero;
try
{
int numArgs = 0;
argv = _CommandLineToArgvW(cmdLine, out numArgs);
if (argv == IntPtr.Zero)
{
throw new Win32Exception();
}
var result = new string[numArgs];
for (int i = 0; i < numArgs; i++)
{
IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
result[i] = Marshal.PtrToStringUni(currArg);
}
return result;
}
finally
{
IntPtr p = _LocalFree(argv);
// Otherwise LocalFree failed.
// Assert.AreEqual(IntPtr.Zero, p);
}
}
}
public interface ISingleInstanceApp
{
void OnSecondAppStarted();
@ -219,10 +47,6 @@ namespace Flow.Launcher.Helper
#endregion
#region Public Properties
#endregion
#region Public Methods
/// <summary>
@ -264,56 +88,6 @@ namespace Flow.Launcher.Helper
#region Private Methods
/// <summary>
/// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved.
/// </summary>
/// <returns>List of command line arg strings.</returns>
private static IList<string> GetCommandLineArgs( string uniqueApplicationName )
{
string[] args = null;
try
{
// The application was not clickonce deployed, get args from standard API's
args = Environment.GetCommandLineArgs();
}
catch (NotSupportedException)
{
// The application was clickonce deployed
// Clickonce deployed apps cannot recieve traditional commandline arguments
// As a workaround commandline arguments can be written to a shared location before
// the app is launched and the app can obtain its commandline arguments from the
// shared location
string appFolderPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName);
string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt");
if (File.Exists(cmdLinePath))
{
try
{
using (TextReader reader = new StreamReader(cmdLinePath, Encoding.Unicode))
{
args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd());
}
File.Delete(cmdLinePath);
}
catch (IOException)
{
}
}
}
if (args == null)
{
args = new string[] { };
}
return new List<string>(args);
}
/// <summary>
/// Creates a remote server pipe for communication.
/// Once receives signal from client, will activate first instance.

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -2,47 +2,45 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Documents;
using System.Windows.Media;
using Microsoft.Win32;
using Windows.Win32;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Flow.Launcher.Helper
namespace Flow.Launcher.Helper;
public static class WallpaperPathRetrieval
{
public static class WallpaperPathRetrieval
private static readonly int MAX_PATH = 260;
public static unsafe 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 wallpaperPtr = stackalloc char[MAX_PATH];
PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, (uint)MAX_PATH,
wallpaperPtr,
0);
var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr);
return wallpaper.ToString();
}
public static string GetWallpaperPath()
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;
}
}

View file

@ -1,163 +1,151 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows.Media;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using Point = System.Windows.Point;
namespace Flow.Launcher.Helper
namespace Flow.Launcher.Helper;
public class WindowsInteropHelper
{
public class WindowsInteropHelper
private static HWND _hwnd_shell;
private static HWND _hwnd_desktop;
//Accessors for shell and desktop handlers
//Will set the variables once and then will return them
private static HWND 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 != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow();
}
private static IntPtr HWND_DESKTOP
}
private static HWND HWND_DESKTOP
{
get
{
get
{
return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow();
}
return _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.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);
const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
const string WINDOW_CLASS_WINTAB = "Flip3D";
const string WINDOW_CLASS_PROGMAN = "Progman";
const string WINDOW_CLASS_WORKERW = "WorkerW";
[DllImport("user32.dll")]
internal static extern IntPtr GetForegroundWindow();
public unsafe static bool IsWindowFullscreen()
{
//get current active window
var hWnd = PInvoke.GetForegroundWindow();
[DllImport("user32.dll")]
internal static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
internal static extern IntPtr GetShellWindow();
[DllImport("user32.dll", SetLastError = true)]
internal static extern int GetWindowRect(IntPtr hwnd, out RECT rc);
[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";
public static bool IsWindowFullscreen()
if (hWnd.Equals(HWND.Null))
{
//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)
string windowClass;
const int capacity = 256;
Span<char> buffer = stackalloc char[capacity];
int validLength;
fixed (char* pBuffer = buffer)
{
Matrix matrix;
var source = PresentationSource.FromVisual(visual);
if (source != null)
{
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));
validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity);
}
windowClass = buffer[..validLength].ToString();
[StructLayout(LayoutKind.Sequential)]
public struct RECT
//for Win+Tab (Flip3D)
if (windowClass == WINDOW_CLASS_WINTAB)
{
public int Left;
public int Top;
public int Right;
public int Bottom;
return false;
}
PInvoke.GetWindowRect(hWnd, out var 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)
{
var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
if (hWndDesktop.Value != (IntPtr.Zero))
{
return false;
}
}
Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds;
return (appBounds.bottom - appBounds.top) == screenBounds.Height &&
(appBounds.right - appBounds.left) == screenBounds.Width;
}
/// <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 HWND(new WindowInteropHelper(win).Handle);
var style = PInvoke.GetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
if (style == 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
style &= ~(int)WINDOW_STYLE.WS_SYSMENU;
var previousStyle = PInvoke.SetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE,
style);
if (previousStyle == 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
}
/// <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)
{
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));
}
}

View 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;
}
}

View file

@ -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>

View file

@ -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);
}
}
}

View 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="&#xf167;" />
<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>

View 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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Some files were not shown because too many files have changed in this diff Show more