[New PowerToy] PowerAccent (#19212)

* add poweraccent (draft) for PR

* removing french text for Spell checking job

* add 'poweraccent' to spell checker

* add 'damienleroy' to spell checker file

* adding RuntimeIdentifiers for PowerAccent project

* duplicate image for settings

* update commandline arguments for launch settings

* Removing WndProc for testing with inter-process connection

* add PowerAccent sources for PowerToys

* fix spellcheck

* fixing stylecop conventions

* Remove StyleCop.Analyzers because of duplicate

* fixing command line reference

* Fixing CS8012 for PowerAccent.

* ARM64 processor

* - Modify PowerAccent fluenticon for dark mode
- Try fix arm64 release

* Remove taskbar

* init Oobe view

* - added POwerAccent to App.xaml.cs
- change style to markdown in Oobe display

* - fixing poweraccent crash
- change Oobe LearnMore link

* Installer and signing

* Cleanup
Add settings

* Issue template

* Add some more characters

* Disabled by default

* Proper ToUnicodeEx calling and remove hacks

* Fix spellcheck

* Remove CommandLine dependency and debug prints. Add logs

* fix signing

* Fix binary metadata with version

* Fix the added space bug

* Only type space if it was the trigger method

* Take account of InputTime for displaying UI

* Fix code styling

* Remove the Trace WriteLine hack and add a delay instead

* Reinstate logs

* Better explanations

* Add telemetry for showing the menu

* Update src/settings-ui/Settings.UI/Strings/en-us/Resources.resw

* Update src/settings-ui/Settings.UI/Strings/en-us/Resources.resw

* Update src/modules/poweraccent/PowerAccent.Core/Tools/KeyboardListener.cs

* Update src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs

* Add accented characters for S

* Default to both activation methods

* Update src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs

* Update src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs

* Update src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs

* Update src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs

* Update src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs

* Update src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs

Co-authored-by: Damien LEROY <dleroy@veepee.com>
This commit is contained in:
damienleroy
2022-08-26 18:01:50 +02:00
committed by GitHub
parent 785160653c
commit d9c0af232b
66 changed files with 2836 additions and 7 deletions

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Vanara.PInvoke;
namespace PowerAccent.Core;
public enum LetterKey
{
A = User32.VK.VK_A,
C = User32.VK.VK_C,
E = User32.VK.VK_E,
I = User32.VK.VK_I,
N = User32.VK.VK_N,
O = User32.VK.VK_O,
S = User32.VK.VK_S,
U = User32.VK.VK_U,
Y = User32.VK.VK_Y,
}
public enum TriggerKey
{
Left = User32.VK.VK_LEFT,
Right = User32.VK.VK_RIGHT,
Space = User32.VK.VK_SPACE,
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerAccent.Core;
public struct Point
{
public Point()
{
X = 0;
Y = 0;
}
public Point(double x, double y)
{
X = x;
Y = y;
}
public Point(int x, int y)
{
X = x;
Y = y;
}
public Point(System.Drawing.Point point)
{
X = point.X;
Y = point.Y;
}
public double X { get; init; }
public double Y { get; init; }
public static implicit operator Point(System.Drawing.Point point) => new Point(point.X, point.Y);
public static Point operator /(Point point, double divider)
{
if (divider == 0)
{
throw new DivideByZeroException();
}
return new Point(point.X / divider, point.Y / divider);
}
public static Point operator /(Point point, Point divider)
{
if (divider.X == 0 || divider.Y == 0)
{
throw new DivideByZeroException();
}
return new Point(point.X / divider.X, point.Y / divider.Y);
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerAccent.Core;
public struct Rect
{
public Rect()
{
X = 0;
Y = 0;
Width = 0;
Height = 0;
}
public Rect(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
public Rect(double x, double y, double width, double height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
public Rect(Point coord, Size size)
{
X = coord.X;
Y = coord.Y;
Width = size.Width;
Height = size.Height;
}
public double X { get; init; }
public double Y { get; init; }
public double Width { get; init; }
public double Height { get; init; }
public static Rect operator /(Rect rect, double divider)
{
if (divider == 0)
{
throw new DivideByZeroException();
}
return new Rect(rect.X / divider, rect.Y / divider, rect.Width / divider, rect.Height / divider);
}
public static Rect operator /(Rect rect, Rect divider)
{
if (divider.X == 0 || divider.Y == 0)
{
throw new DivideByZeroException();
}
return new Rect(rect.X / divider.X, rect.Y / divider.Y, rect.Width / divider.Width, rect.Height / divider.Height);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerAccent.Core;
public struct Size
{
public Size()
{
Width = 0;
Height = 0;
}
public Size(double width, double height)
{
Width = width;
Height = height;
}
public Size(int width, int height)
{
Width = width;
Height = height;
}
public double Width { get; init; }
public double Height { get; init; }
public static implicit operator Size(System.Drawing.Size size) => new Size(size.Width, size.Height);
public static Size operator /(Size size, double divider)
{
if (divider == 0)
{
throw new DivideByZeroException();
}
return new Size(size.Width / divider, size.Height / divider);
}
public static Size operator /(Size size, Size divider)
{
if (divider.Width == 0 || divider.Height == 0 || divider.Width == 0 || divider.Height == 0)
{
throw new DivideByZeroException();
}
return new Size(size.Width / divider.Width, size.Height / divider.Height);
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageReference Include="Vanara.PInvoke.User32" Version="3.3.15" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using PowerAccent.Core.Services;
using PowerAccent.Core.Tools;
namespace PowerAccent.Core;
public class PowerAccent : IDisposable
{
private readonly SettingsService _settingService = new SettingsService();
private readonly KeyboardListener _keyboardListener = new KeyboardListener();
private LetterKey? letterPressed;
private bool _visible;
private char[] _characters = Array.Empty<char>();
private int _selectedIndex = -1;
private Stopwatch _stopWatch;
private bool _triggeredWithSpace;
public event Action<bool, char[]> OnChangeDisplay;
public event Action<int, char> OnSelectCharacter;
public PowerAccent()
{
_keyboardListener.KeyDown += PowerAccent_KeyDown;
_keyboardListener.KeyUp += PowerAccent_KeyUp;
}
private bool PowerAccent_KeyDown(object sender, KeyboardListener.RawKeyEventArgs args)
{
if (Enum.IsDefined(typeof(LetterKey), (int)args.Key))
{
_stopWatch = Stopwatch.StartNew();
letterPressed = (LetterKey)args.Key;
}
TriggerKey? triggerPressed = null;
if (letterPressed.HasValue)
{
if (Enum.IsDefined(typeof(TriggerKey), (int)args.Key))
{
triggerPressed = (TriggerKey)args.Key;
if ((triggerPressed == TriggerKey.Space && _settingService.ActivationKey == PowerAccentActivationKey.LeftRightArrow) ||
((triggerPressed == TriggerKey.Left || triggerPressed == TriggerKey.Right) && _settingService.ActivationKey == PowerAccentActivationKey.Space))
{
triggerPressed = null;
}
}
}
if (!_visible && letterPressed.HasValue && triggerPressed.HasValue)
{
// Keep track if it was triggered with space so that it can be typed on false starts.
_triggeredWithSpace = triggerPressed.Value == TriggerKey.Space;
_visible = true;
_characters = WindowsFunctions.IsCapitalState() ? ToUpper(_settingService.GetLetterKey(letterPressed.Value)) : _settingService.GetLetterKey(letterPressed.Value);
Task.Delay(_settingService.InputTime).ContinueWith(
t =>
{
if (_visible)
{
OnChangeDisplay?.Invoke(true, _characters);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
if (_visible && triggerPressed.HasValue)
{
if (_selectedIndex == -1)
{
if (triggerPressed.Value == TriggerKey.Left)
{
_selectedIndex = (_characters.Length / 2) - 1;
}
if (triggerPressed.Value == TriggerKey.Right)
{
_selectedIndex = _characters.Length / 2;
}
if (triggerPressed.Value == TriggerKey.Space)
{
_selectedIndex = 0;
}
if (_selectedIndex < 0)
{
_selectedIndex = 0;
}
if (_selectedIndex > _characters.Length - 1)
{
_selectedIndex = _characters.Length - 1;
}
OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]);
return false;
}
if (triggerPressed.Value == TriggerKey.Space)
{
if (_selectedIndex < _characters.Length - 1)
{
++_selectedIndex;
}
else
{
_selectedIndex = 0;
}
}
if (triggerPressed.Value == TriggerKey.Left && _selectedIndex > 0)
{
--_selectedIndex;
}
if (triggerPressed.Value == TriggerKey.Right && _selectedIndex < _characters.Length - 1)
{
++_selectedIndex;
}
OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]);
return false;
}
return true;
}
private bool PowerAccent_KeyUp(object sender, KeyboardListener.RawKeyEventArgs args)
{
if (Enum.IsDefined(typeof(LetterKey), (int)args.Key))
{
letterPressed = null;
_stopWatch.Stop();
if (_visible)
{
if (_stopWatch.ElapsedMilliseconds < _settingService.InputTime)
{
/* Debug.WriteLine("Insert before inputTime - " + _stopWatch.ElapsedMilliseconds); */
// False start, we should output the space if it was the trigger.
if (_triggeredWithSpace)
{
WindowsFunctions.Insert(' ');
}
OnChangeDisplay?.Invoke(false, null);
_selectedIndex = -1;
_visible = false;
return false;
}
/* Debug.WriteLine("Insert after inputTime - " + _stopWatch.ElapsedMilliseconds); */
OnChangeDisplay?.Invoke(false, null);
if (_selectedIndex != -1)
{
WindowsFunctions.Insert(_characters[_selectedIndex], true);
}
_selectedIndex = -1;
_visible = false;
}
}
return true;
}
public Point GetDisplayCoordinates(Size window)
{
var activeDisplay = WindowsFunctions.GetActiveDisplay();
Rect screen = new Rect(activeDisplay.Location, activeDisplay.Size) / activeDisplay.Dpi;
Position position = _settingService.Position;
/* Debug.WriteLine("Dpi: " + activeDisplay.Dpi); */
return Calculation.GetRawCoordinatesFromPosition(position, screen, window);
}
public char[] GetLettersFromKey(LetterKey letter)
{
return _settingService.GetLetterKey(letter);
}
public void Dispose()
{
_keyboardListener.Dispose();
GC.SuppressFinalize(this);
}
public static char[] ToUpper(char[] array)
{
char[] result = new char[array.Length];
for (int i = 0; i < array.Length; i++)
{
result[i] = char.ToUpper(array[i], System.Globalization.CultureInfo.InvariantCulture);
}
return result;
}
}

View File

@@ -0,0 +1,180 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerAccent.Core.Services;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using System.IO.Abstractions;
using System.Text.Json;
public class SettingsService
{
private const string PowerAccentModuleName = "PowerAccent";
private readonly ISettingsUtils _settingsUtils;
private readonly IFileSystemWatcher _watcher;
private readonly object _loadingSettingsLock = new object();
public SettingsService()
{
_settingsUtils = new SettingsUtils();
ReadSettings();
_watcher = Helper.GetFileWatcher(PowerAccentModuleName, "settings.json", () => { ReadSettings(); });
}
private void ReadSettings()
{
// TODO this IO call should by Async, update GetFileWatcher helper to support async
lock (_loadingSettingsLock)
{
{
try
{
if (!_settingsUtils.SettingsExists(PowerAccentModuleName))
{
Logger.LogInfo("PowerAccent settings.json was missing, creating a new one");
var defaultSettings = new PowerAccentSettings();
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
_settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), PowerAccentModuleName);
}
var settings = _settingsUtils.GetSettingsOrDefault<PowerAccentSettings>(PowerAccentModuleName);
if (settings != null)
{
ActivationKey = settings.Properties.ActivationKey;
switch (settings.Properties.ToolbarPosition.Value)
{
case "Top center":
Position = Position.Top;
break;
case "Bottom center":
Position = Position.Bottom;
break;
case "Left":
Position = Position.Left;
break;
case "Right":
Position = Position.Right;
break;
case "Top right corner":
Position = Position.TopRight;
break;
case "Top left corner":
Position = Position.TopLeft;
break;
case "Bottom right corner":
Position = Position.BottomRight;
break;
case "Bottom left corner":
Position = Position.BottomLeft;
break;
case "Center":
Position = Position.Center;
break;
}
}
}
catch (Exception ex)
{
Logger.LogError("Failed to read changed settings", ex);
}
}
}
}
private PowerAccentActivationKey _activationKey = PowerAccentActivationKey.Both;
public PowerAccentActivationKey ActivationKey
{
get
{
return _activationKey;
}
set
{
_activationKey = value;
}
}
private Position _position = Position.Top;
public Position Position
{
get
{
return _position;
}
set
{
_position = value;
}
}
private int _inputTime = 200;
public int InputTime
{
get
{
return _inputTime;
}
set
{
_inputTime = value;
}
}
public char[] GetLetterKey(LetterKey letter)
{
return GetDefaultLetterKey(letter);
}
public static char[] GetDefaultLetterKey(LetterKey letter)
{
switch (letter)
{
case LetterKey.A:
return new char[] { 'à', 'â', 'á', 'ä', 'ã', 'å', 'æ' };
case LetterKey.C:
return new char[] { 'ć', 'ĉ', 'č', 'ċ', 'ç', 'ḉ' };
case LetterKey.E:
return new char[] { 'é', 'è', 'ê', 'ë', 'ē', 'ė', '€' };
case LetterKey.I:
return new char[] { 'î', 'ï', 'í', 'ì', 'ī' };
case LetterKey.N:
return new char[] { 'ñ', 'ń' };
case LetterKey.O:
return new char[] { 'ô', 'ö', 'ó', 'ò', 'õ', 'ø', 'œ' };
case LetterKey.S:
return new char[] { 'š', 'ß', 'ś' };
case LetterKey.U:
return new char[] { 'û', 'ù', 'ü', 'ú', 'ū' };
case LetterKey.Y:
return new char[] { 'ÿ', 'ý' };
}
throw new ArgumentException("Letter {0} is missing", letter.ToString());
}
}
public enum Position
{
Top,
Bottom,
Left,
Right,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center,
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace PowerAccent.Core.Telemetry
{
[EventData]
public class PowerAccentShowAccentMenuEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using PowerAccent.Core.Services;
namespace PowerAccent.Core.Tools
{
internal static class Calculation
{
public static Point GetRawCoordinatesFromCaret(Point caret, Rect screen, Size window)
{
var left = caret.X - (window.Width / 2);
var top = caret.Y - window.Height - 20;
return new Point(
left < screen.X ? screen.X : (left + window.Width > (screen.X + screen.Width) ? (screen.X + screen.Width) - window.Width : left),
top < screen.Y ? caret.Y + 20 : top);
}
public static Point GetRawCoordinatesFromPosition(Position position, Rect screen, Size window)
{
int offset = 10;
double pointX = position switch
{
Position.Top or Position.Bottom or Position.Center
=> screen.X + (screen.Width / 2) - (window.Width / 2),
Position.TopLeft or Position.Left or Position.BottomLeft
=> screen.X + offset,
Position.TopRight or Position.Right or Position.BottomRight
=> screen.X + screen.Width - (window.Width + offset),
_ => throw new NotImplementedException(),
};
double pointY = position switch
{
Position.TopLeft or Position.Top or Position.TopRight
=> screen.Y + offset,
Position.Left or Position.Center or Position.Right
=> screen.Y + (screen.Height / 2) - (window.Height / 2),
Position.BottomLeft or Position.Bottom or Position.BottomRight
=> screen.Y + screen.Height - (window.Height + offset),
_ => throw new NotImplementedException(),
};
return new Point(pointX, pointY);
}
}
}

View File

@@ -0,0 +1,359 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable SA1310 // FieldNamesMustNotContainUnderscore
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace PowerAccent.Core.Tools;
internal class KeyboardListener : IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="KeyboardListener"/> class.
/// Creates global keyboard listener.
/// </summary>
public KeyboardListener()
{
// We have to store the LowLevelKeyboardProc, so that it is not garbage collected by runtime
_hookedLowLevelKeyboardProc = LowLevelKeyboardProc;
// Set the hook
_hookId = InterceptKeys.SetHook(_hookedLowLevelKeyboardProc);
// Assign the asynchronous callback event
hookedKeyboardCallbackAsync = new KeyboardCallbackAsync(KeyboardListener_KeyboardCallbackAsync);
}
/// <summary>
/// Fired when any of the keys is pressed down.
/// </summary>
public event RawKeyEventHandler KeyDown;
/// <summary>
/// Fired when any of the keys is released.
/// </summary>
public event RawKeyEventHandler KeyUp;
/// <summary>
/// Hook ID
/// </summary>
private readonly IntPtr _hookId = IntPtr.Zero;
/// <summary>
/// Contains the hooked callback in runtime.
/// </summary>
private readonly InterceptKeys.LowLevelKeyboardProc _hookedLowLevelKeyboardProc;
/// <summary>
/// Event to be invoked asynchronously (BeginInvoke) each time key is pressed.
/// </summary>
private KeyboardCallbackAsync hookedKeyboardCallbackAsync;
/// <summary>
/// Raw keyevent handler.
/// </summary>
/// <param name="sender">sender</param>
/// <param name="args">raw keyevent arguments</param>
public delegate bool RawKeyEventHandler(object sender, RawKeyEventArgs args);
/// <summary>
/// Asynchronous callback hook.
/// </summary>
/// <param name="keyEvent">Keyboard event</param>
/// <param name="vkCode">VKCode</param>
/// <param name="character">Character</param>
private delegate bool KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character);
/// <summary>
/// Actual callback hook.
/// <remarks>Calls asynchronously the asyncCallback.</remarks>
/// </summary>
/// <param name="nCode">VKCode</param>
/// <param name="wParam">wParam</param>
/// <param name="lParam">lParam</param>
[MethodImpl(MethodImplOptions.NoInlining)]
private IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
if (wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN ||
wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYUP)
{
// Captures the character(s) pressed only on WM_KEYDOWN
var chars = InterceptKeys.VKCodeToString(
(uint)Marshal.ReadInt32(lParam),
wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN);
if (!hookedKeyboardCallbackAsync.Invoke((InterceptKeys.KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), chars))
{
return (IntPtr)1;
}
}
}
return InterceptKeys.CallNextHookEx(_hookId, nCode, wParam, lParam);
}
/// <summary>
/// HookCallbackAsync procedure that calls accordingly the KeyDown or KeyUp events.
/// </summary>
/// <param name="keyEvent">Keyboard event</param>
/// <param name="vkCode">VKCode</param>
/// <param name="character">Character as string.</param>
private bool KeyboardListener_KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character)
{
switch (keyEvent)
{
// KeyDown events
case InterceptKeys.KeyEvent.WM_KEYDOWN:
if (KeyDown != null)
{
return KeyDown.Invoke(this, new RawKeyEventArgs(vkCode, character));
}
break;
// KeyUp events
case InterceptKeys.KeyEvent.WM_KEYUP:
if (KeyUp != null)
{
return KeyUp.Invoke(this, new RawKeyEventArgs(vkCode, character));
}
break;
default:
break;
}
return true;
}
public void Dispose()
{
InterceptKeys.UnhookWindowsHookEx(_hookId);
}
/// <summary>
/// Raw KeyEvent arguments.
/// </summary>
public class RawKeyEventArgs : EventArgs
{
/// <summary>
/// WPF Key of the key.
/// </summary>
#pragma warning disable SA1401 // Fields should be private
public uint Key;
#pragma warning restore SA1401 // Fields should be private
/// <summary>
/// Convert to string.
/// </summary>
/// <returns>Returns string representation of this key, if not possible empty string is returned.</returns>
public override string ToString()
{
return character;
}
/// <summary>
/// Unicode character of key pressed.
/// </summary>
private string character;
/// <summary>
/// Initializes a new instance of the <see cref="RawKeyEventArgs"/> class.
/// Create raw keyevent arguments.
/// </summary>
/// <param name="vKCode">VKCode</param>
/// <param name="character">Character</param>
public RawKeyEventArgs(int vKCode, string character)
{
this.character = character;
Key = (uint)vKCode; // User32.MapVirtualKey((uint)VKCode, User32.MAPVK.MAPVK_VK_TO_VSC_EX);
}
}
}
/// <summary>
/// Winapi Key interception helper class.
/// </summary>
internal static class InterceptKeys
{
public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
private const int WH_KEYBOARD_LL = 13;
/// <summary>
/// Key event
/// </summary>
public enum KeyEvent : int
{
/// <summary>
/// Key down
/// </summary>
WM_KEYDOWN = 256,
/// <summary>
/// Key up
/// </summary>
WM_KEYUP = 257,
/// <summary>
/// System key up
/// </summary>
WM_SYSKEYUP = 261,
/// <summary>
/// System key down
/// </summary>
WM_SYSKEYDOWN = 260,
}
public static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, (IntPtr)0, 0);
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc 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.Unicode, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
// Note: Sometimes single VKCode represents multiple chars, thus string.
// E.g. typing "^1" (notice that when pressing 1 the both characters appear,
// because of this behavior, "^" is called dead key)
[DllImport("user32.dll")]
#pragma warning disable CA1838 // Éviter les paramètres 'StringBuilder' pour les P/Invoke
private static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl);
#pragma warning restore CA1838 // Éviter les paramètres 'StringBuilder' pour les P/Invoke
[DllImport("user32.dll")]
private static extern bool GetKeyboardState(byte[] lpKeyState);
[DllImport("user32.dll")]
private static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl);
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
private static extern IntPtr GetKeyboardLayout(uint dwLayout);
[DllImport("User32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("User32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
private static uint lastVKCode;
private static uint lastScanCode;
private static byte[] lastKeyState = new byte[255];
/// <summary>
/// Convert VKCode to Unicode.
/// <remarks>isKeyDown is required for because of keyboard state inconsistencies!</remarks>
/// </summary>
/// <param name="vKCode">VKCode</param>
/// <param name="isKeyDown">Is the key down event?</param>
/// <returns>String representing single unicode character.</returns>
public static string VKCodeToString(uint vKCode, bool isKeyDown)
{
// ToUnicodeEx needs StringBuilder, it populates that during execution.
System.Text.StringBuilder sbString = new System.Text.StringBuilder(5);
byte[] bKeyState = new byte[255];
bool bKeyStateStatus;
// Gets the current windows window handle, threadID, processID
IntPtr currentHWnd = GetForegroundWindow();
uint currentProcessID;
uint currentWindowThreadID = GetWindowThreadProcessId(currentHWnd, out currentProcessID);
// This programs Thread ID
uint thisProgramThreadId = GetCurrentThreadId();
// Attach to active thread so we can get that keyboard state
if (AttachThreadInput(thisProgramThreadId, currentWindowThreadID, true))
{
// Current state of the modifiers in keyboard
bKeyStateStatus = GetKeyboardState(bKeyState);
// Detach
AttachThreadInput(thisProgramThreadId, currentWindowThreadID, false);
}
else
{
// Could not attach, perhaps it is this process?
bKeyStateStatus = GetKeyboardState(bKeyState);
}
// On failure we return empty string.
if (!bKeyStateStatus)
{
return string.Empty;
}
// Gets the layout of keyboard
IntPtr hkl = GetKeyboardLayout(currentWindowThreadID);
// Maps the virtual keycode
uint lScanCode = MapVirtualKeyEx(vKCode, 0, hkl);
// Keyboard state goes inconsistent if this is not in place. In other words, we need to call above commands in UP events also.
if (!isKeyDown)
{
return string.Empty;
}
// Converts the VKCode to unicode
const uint wFlags = 1 << 2; // If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer)
int relevantKeyCountInBuffer = ToUnicodeEx(vKCode, lScanCode, bKeyState, sbString, sbString.Capacity, wFlags, hkl);
string ret = string.Empty;
switch (relevantKeyCountInBuffer)
{
// dead key
case -1:
break;
case 0:
break;
// Single character in buffer
case 1:
ret = sbString.Length == 0 ? string.Empty : sbString[0].ToString();
break;
// Two or more (only two of them is relevant)
case 2:
default:
ret = sbString.ToString().Substring(0, 2);
break;
}
// Save these
lastScanCode = lScanCode;
lastVKCode = vKCode;
lastKeyState = (byte[])bKeyState.Clone();
return ret;
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using interop;
namespace PowerAccent.Core.Tools
{
public static class Logger
{
private static readonly IFileSystem _fileSystem = new FileSystem();
private static readonly string ApplicationLogPath = Path.Combine(Constants.AppDataPath(), "PowerAccent\\Logs");
static Logger()
{
if (!_fileSystem.Directory.Exists(ApplicationLogPath))
{
_fileSystem.Directory.CreateDirectory(ApplicationLogPath);
}
// Using InvariantCulture since this is used for a log file name
var logFilePath = _fileSystem.Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
Trace.AutoFlush = true;
}
public static void LogError(string message)
{
Log(message, "ERROR");
}
public static void LogError(string message, Exception ex)
{
Log(
message + Environment.NewLine +
ex?.Message + Environment.NewLine +
"Inner exception: " + Environment.NewLine +
ex?.InnerException?.Message + Environment.NewLine +
"Stack trace: " + Environment.NewLine +
ex?.StackTrace,
"ERROR");
}
public static void LogWarning(string message)
{
Log(message, "WARNING");
}
public static void LogInfo(string message)
{
Log(message, "INFO");
}
private static void Log(string message, string type)
{
Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay);
Trace.Indent();
Trace.WriteLine(GetCallerInfo());
Trace.WriteLine(message);
Trace.Unindent();
}
private static string GetCallerInfo()
{
StackTrace stackTrace = new StackTrace();
var methodName = stackTrace.GetFrame(3)?.GetMethod();
var className = methodName?.DeclaringType?.Name;
return "[Method]: " + methodName?.Name + " [Class]: " + className;
}
}
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using Vanara.PInvoke;
namespace PowerAccent.Core.Tools;
internal static class WindowsFunctions
{
public static void Insert(char c, bool back = false)
{
unsafe
{
if (back)
{
// Split in 2 different SendInput (Powershell doesn't take back issue)
var inputsBack = new User32.INPUT[]
{
new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = (ushort)User32.VK.VK_BACK } },
new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = (ushort)User32.VK.VK_BACK, dwFlags = User32.KEYEVENTF.KEYEVENTF_KEYUP } },
};
var temp1 = User32.SendInput((uint)inputsBack.Length, inputsBack, sizeof(User32.INPUT));
System.Threading.Thread.Sleep(1); // Some apps, like Terminal, need a little wait to process the sent backspace or they'll ignore it.
}
// Letter
var inputsInsert = new User32.INPUT[1]
{
new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = 0, dwFlags = User32.KEYEVENTF.KEYEVENTF_UNICODE, wScan = c } },
};
var temp2 = User32.SendInput((uint)inputsInsert.Length, inputsInsert, sizeof(User32.INPUT));
}
}
public static Point GetCaretPosition()
{
User32.GUITHREADINFO guiInfo = new ();
guiInfo.cbSize = (uint)Marshal.SizeOf(guiInfo);
User32.GetGUIThreadInfo(0, ref guiInfo);
System.Drawing.Point caretPosition = new System.Drawing.Point(guiInfo.rcCaret.left, guiInfo.rcCaret.top);
User32.ClientToScreen(guiInfo.hwndCaret, ref caretPosition);
if (caretPosition.X == 0)
{
System.Drawing.Point testPoint;
User32.GetCaretPos(out testPoint);
return testPoint;
}
return caretPosition;
}
public static (Point Location, Size Size, double Dpi) GetActiveDisplay()
{
User32.GUITHREADINFO guiInfo = new ();
guiInfo.cbSize = (uint)Marshal.SizeOf(guiInfo);
User32.GetGUIThreadInfo(0, ref guiInfo);
var res = User32.MonitorFromWindow(guiInfo.hwndActive, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST);
User32.MONITORINFO monitorInfo = new ();
monitorInfo.cbSize = (uint)Marshal.SizeOf(monitorInfo);
User32.GetMonitorInfo(res, ref monitorInfo);
double dpi = User32.GetDpiForWindow(guiInfo.hwndActive) / 96d;
return (monitorInfo.rcWork.Location, monitorInfo.rcWork.Size, dpi);
}
public static bool IsCapitalState()
{
var capital = User32.GetKeyState((int)User32.VK.VK_CAPITAL);
var shift = User32.GetKeyState((int)User32.VK.VK_SHIFT);
return capital != 0 || shift < 0;
}
}

View File

@@ -0,0 +1,17 @@
<Application x:Class="PowerAccent.UI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PowerAccent"
StartupUri="Selector.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- MahApps.Metro resource dictionaries. Make sure that all file names are Case Sensitive! -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<!-- Theme setting -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading;
using System.Windows;
namespace PowerAccent.UI
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private static Mutex _mutex;
protected override void OnStartup(StartupEventArgs e)
{
const string appName = "PowerAccent";
bool createdNew;
_mutex = new Mutex(true, appName, out createdNew);
if (!createdNew)
{
// app is already running! Exiting the application
Application.Current.Shutdown();
}
base.OnStartup(e);
}
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located (used if a resource is not found in the page, or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is locate (used if a resource is not found in the page, app, or any theme specific resource dictionaries)
]

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<Nullable>disable</Nullable>
<UseWPF>true</UseWPF>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<ApplicationIcon>a-icon.ico</ApplicationIcon>
<AssemblyName>PowerAccent</AssemblyName>
<XamlDebuggingInformation>True</XamlDebuggingInformation>
</PropertyGroup>
<ItemGroup>
<None Remove="win11desktop.jpg" />
</ItemGroup>
<ItemGroup>
<Resource Include="a-icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="win11desktop.jpg" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="gong-wpf-dragdrop" Version="3.1.1" />
<PackageReference Include="MahApps.Metro" Version="2.4.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerAccent.Core\PowerAccent.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,55 @@
<Window x:Class="PowerAccent.UI.Selector"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PowerAccent"
mc:Ignorable="d" SizeToContent="WidthAndHeight" ShowInTaskbar="False" ResizeMode="NoResize"
Title="MainWindow" Height="50" Width="50" Visibility="Collapsed" WindowStyle="None">
<Grid>
<ListBox x:Name="characters" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsHitTestVisible="False"
BorderThickness="1" BorderBrush="SlateGray">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsItemsHost="False" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock TextAlignment="Center" VerticalAlignment="Center" Text="{Binding}" FontSize="18" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style x:Name="ItemStyle" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="myBorder"
Padding="0" Margin="0"
SnapsToDevicePixels="true"
Style="{DynamicResource borderContent}">
<ContentPresenter />
</Border>
<ControlTemplate.Resources>
<Style x:Key="borderContent" TargetType="Border">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Background" Value="#ECECEC"/>
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="50"/>
</Style>
</ControlTemplate.Resources>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="myBorder" Property="Background" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</Window>

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows;
using Point = PowerAccent.Core.Point;
using Size = PowerAccent.Core.Size;
namespace PowerAccent.UI;
public partial class Selector : Window, IDisposable
{
private Core.PowerAccent _powerAccent = new Core.PowerAccent();
public Selector()
{
InitializeComponent();
Application.Current.MainWindow.ShowActivated = false;
Application.Current.MainWindow.Topmost = true;
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
_powerAccent.OnChangeDisplay += PowerAccent_OnChangeDisplay;
_powerAccent.OnSelectCharacter += PowerAccent_OnSelectionCharacter;
this.Visibility = Visibility.Hidden;
}
private void PowerAccent_OnSelectionCharacter(int index, char character)
{
characters.SelectedIndex = index;
}
private void PowerAccent_OnChangeDisplay(bool isActive, char[] chars)
{
this.Visibility = isActive ? Visibility.Visible : Visibility.Collapsed;
if (isActive)
{
CenterWindow();
characters.ItemsSource = chars;
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new PowerAccent.Core.Telemetry.PowerAccentShowAccentMenuEvent());
}
}
private void MenuExit_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
private void CenterWindow()
{
UpdateLayout();
Size window = new Size(((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualWidth, ((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualHeight);
Point position = _powerAccent.GetDisplayCoordinates(window);
this.Left = position.X;
this.Top = position.Y;
}
protected override void OnClosed(EventArgs e)
{
_powerAccent.Dispose();
base.OnClosed(e);
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<UseWPF>true</UseWPF>
<Nullable>disable</Nullable>
<StartupObject>PowerAccent.Program</StartupObject>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\modules\PowerAccent</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.PowerAccent</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\PowerAccent.UI\PowerAccent.UI.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable SA1310 // FieldNamesMustNotContainUnderscore
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using interop;
using ManagedCommon;
using PowerAccent.Core.Tools;
using PowerAccent.UI;
namespace PowerAccent;
internal static class Program
{
private const string PROGRAM_NAME = "PowerAccent";
private const string PROGRAM_APP_NAME = "PowerToys.PowerAccent";
private static App _application;
private static int _powerToysRunnerPid;
private static CancellationTokenSource _tokenSource = new CancellationTokenSource();
[STAThread]
public static void Main(string[] args)
{
_ = new Mutex(true, PROGRAM_APP_NAME, out bool instantiated);
if (instantiated)
{
Arguments(args);
InitEvents();
_application = new App();
_application.InitializeComponent();
_application.Run();
}
else
{
Logger.LogWarning("Another running PowerAccent instance was detected. Exiting PowerAccent");
}
}
private static void InitEvents()
{
Task.Run(
() =>
{
EventWaitHandle eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.PowerAccentExitEvent());
if (eventHandle.WaitOne())
{
Terminate();
}
}, _tokenSource.Token);
}
private static void Arguments(string[] args)
{
if (args?.Length > 0)
{
try
{
_ = int.TryParse(args[0], out _powerToysRunnerPid);
Logger.LogInfo($"PowerAccent started from the PowerToys Runner. Runner pid={_powerToysRunnerPid}");
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting PowerAccent");
Terminate();
});
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
else
{
Logger.LogInfo($"PowerAccent started detached from PowerToys Runner.");
_powerToysRunnerPid = -1;
}
}
private static void Terminate()
{
Application.Current.Dispatcher.BeginInvoke(() =>
{
_tokenSource.Cancel();
Application.Current.Shutdown();
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,7 @@
#include <string>
namespace PowerAccentConstants
{
// Name of the powertoy module.
inline const std::wstring ModuleKey = L"PowerAccent";
}

View File

@@ -0,0 +1,108 @@
// Microsoft Visual C++ generated resource script.
//
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// String Table
//
STRINGTABLE
BEGIN
IDS_POWERACCENT_NAME "PowerAccent"
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{34A354C5-23C7-4343-916C-C52DAF4FC39D}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>PowerAccent</RootNamespace>
<ProjectName>PowerAccentModuleInterface</ProjectName>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\PowerAccent\</OutDir>
</PropertyGroup>
<PropertyGroup >
<TargetName>PowerToys.$(ProjectName)</TargetName>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="PowerAccentConstants.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerAccentModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="PowerAccentConstants.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Generated Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Header Files">
<UniqueIdentifier>{e8ef1c4e-cc50-4ce5-b00d-4e3ac5c1a7db}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{fbd9cdd2-e7d5-4417-9b52-25e345ae9562}</UniqueIdentifier>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{c2a23a2b-5846-440f-b29e-eea748dba12d}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files">
<UniqueIdentifier>{77f1702b-da7f-4ff6-90a3-19db515cf963}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerAccentModuleInterface.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,186 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/interop/shared_constants.h>
#include "trace.h"
#include "resource.h"
#include "PowerAccentConstants.h"
#include <common/logger/logger.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/elevation.h>
#include <common/utils/process_path.h>
#include <common/utils/resources.h>
#include <common/utils/os-detect.h>
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
#include <filesystem>
#include <set>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
const static wchar_t* MODULE_NAME = L"PowerAccent";
const static wchar_t* MODULE_DESC = L"A module that keeps your computer PowerAccent on-demand.";
class PowerAccent : public PowertoyModuleIface
{
std::wstring app_name;
std::wstring app_key;
private:
bool m_enabled = false;
HANDLE m_hInvokeEvent;
PROCESS_INFORMATION p_info;
bool is_process_running()
{
return WaitForSingleObject(p_info.hProcess, 0) == WAIT_TIMEOUT;
}
void launch_process()
{
Logger::trace(L"Launching PowerToys PowerAccent process");
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"" + std::to_wstring(powertoys_pid);
std::wstring application_path = L"modules\\PowerAccent\\PowerToys.PowerAccent.exe";
std::wstring full_command_path = application_path + L" " + executable_args.data();
Logger::trace(L"PowerToys PowerAccent launching: " + full_command_path);
STARTUPINFO info = { sizeof(info) };
if (!CreateProcess(application_path.c_str(), full_command_path.data(), NULL, NULL, true, NULL, NULL, NULL, &info, &p_info))
{
DWORD error = GetLastError();
std::wstring message = L"PowerToys PowerAccent failed to start with error: ";
message += std::to_wstring(error);
Logger::error(message);
}
}
public:
PowerAccent()
{
app_name = MODULE_NAME;
app_key = PowerAccentConstants::ModuleKey;
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "PowerAccent");
Logger::info("Launcher object is constructing");
};
virtual void destroy() override
{
delete this;
}
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
// If you don't need to do any custom processing of the settings, proceed
// to persists the values.
values.save_to_settings_file();
}
catch (std::exception&)
{
// Improper JSON.
}
}
virtual void enable()
{
ResetEvent(m_hInvokeEvent);
launch_process();
m_enabled = true;
Trace::EnablePowerAccent(true);
};
virtual void disable()
{
if (m_enabled)
{
Logger::trace(L"Disabling PowerAccent... {}", m_enabled);
ResetEvent(m_hInvokeEvent);
auto exitEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::POWERACCENT_EXIT_EVENT);
if (!exitEvent)
{
Logger::warn(L"Failed to create exit event for PowerToys PowerAccent. {}", get_last_error_or_default(GetLastError()));
}
else
{
Logger::trace(L"Signaled exit event for PowerToys PowerAccent.");
if (!SetEvent(exitEvent))
{
Logger::warn(L"Failed to signal exit event for PowerToys PowerAccent. {}", get_last_error_or_default(GetLastError()));
// For some reason, we couldn't process the signal correctly, so we still
// need to terminate the PowerAccent process.
TerminateProcess(p_info.hProcess, 1);
}
ResetEvent(exitEvent);
CloseHandle(exitEvent);
CloseHandle(p_info.hProcess);
}
}
m_enabled = false;
Trace::EnablePowerAccent(false);
}
virtual bool is_enabled() override
{
return m_enabled;
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new PowerAccent();
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.200729.8" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,7 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <ProjectTelemetry.h>
#include <shellapi.h>
#include <Shlwapi.h>

View File

@@ -0,0 +1,26 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by PowerAccent.rc
//
#define IDS_POWERACCENT_NAME 101
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys PowerAccent Module"
#define INTERNAL_NAME "PowerToys.PowerAccentModuleInterface"
#define ORIGINAL_FILENAME "PowerToys.PowerAccentModuleInterface.dll"
// Non-localizable
//////////////////////////////
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View File

@@ -0,0 +1,29 @@
#include "pch.h"
#include "trace.h"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider()
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider()
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::EnablePowerAccent(const bool enabled) noexcept
{
TraceLoggingWrite(
g_hProvider,
"PowerAccent_EnablePowerAccent",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}

View File

@@ -0,0 +1,11 @@
#pragma once
class Trace
{
public:
static void RegisterProvider();
static void UnregisterProvider();
// Log if the user has PowerAccent enabled or disabled
static void EnablePowerAccent(const bool enabled) noexcept;
};