Compare commits
34 Commits
dev/featur
...
v0.84.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fac6ed582 | ||
|
|
ae5a61edeb | ||
|
|
a59a07278a | ||
|
|
39741f492f | ||
|
|
d42cd4bd3b | ||
|
|
6408898cbe | ||
|
|
42cd02b20b | ||
|
|
f0a6a8462c | ||
|
|
62a8a9be52 | ||
|
|
31abbd54a4 | ||
|
|
663f26943b | ||
|
|
e882487d32 | ||
|
|
4413089af4 | ||
|
|
3c006f0abb | ||
|
|
12f21da35e | ||
|
|
320182dd89 | ||
|
|
d676064be5 | ||
|
|
fae78ae054 | ||
|
|
2189e7e1b9 | ||
|
|
2abd1058fa | ||
|
|
ed23e7eeb6 | ||
|
|
a5757fd525 | ||
|
|
579619952d | ||
|
|
2a8e211cfd | ||
|
|
bfa35d65a4 | ||
|
|
744c53cfcd | ||
|
|
4b9bb2f5a9 | ||
|
|
a163bbedc1 | ||
|
|
9f491c8f73 | ||
|
|
45ad7ebc5e | ||
|
|
808e6220bc | ||
|
|
f8269af125 | ||
|
|
1f5f43b154 | ||
|
|
9af757f5ce |
3
.github/actions/spell-check/allow/names.txt
vendored
@@ -28,7 +28,7 @@ videoconference
|
||||
|
||||
# USERS
|
||||
|
||||
8LWXpg
|
||||
LWXpg # (number eight)LWXpg is actual user name but spell checker throws error with a numeric leading value ... which is kinda odd
|
||||
Adoumie
|
||||
Advaith
|
||||
alekhyareddy
|
||||
@@ -62,6 +62,7 @@ DHowett
|
||||
ductdo
|
||||
Essey
|
||||
ethanfangg
|
||||
ferraridavide
|
||||
frankychen
|
||||
gabime
|
||||
Galaxi
|
||||
|
||||
1
.github/actions/spell-check/excludes.txt
vendored
@@ -117,6 +117,7 @@
|
||||
^\Qsrc/modules/previewpane/UnitTests-StlThumbnailProvider/HelperFiles/sample.stl\E$
|
||||
^\Qtools/project_template/ModuleTemplate/resource.h\E$
|
||||
^doc/devdocs/akaLinks\.md$
|
||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||
^src/modules/MouseWithoutBorders/App/.*/NativeMethods\.cs$
|
||||
^src/modules/MouseWithoutBorders/App/Form/.*\.Designer\.cs$
|
||||
^src/modules/MouseWithoutBorders/App/Form/.*\.resx$
|
||||
|
||||
14
.github/actions/spell-check/expect.txt
vendored
@@ -192,7 +192,6 @@ CLIPBOARDUPDATE
|
||||
CLIPCHILDREN
|
||||
CLIPSIBLINGS
|
||||
closesocket
|
||||
clrcall
|
||||
CLSCTX
|
||||
Clusion
|
||||
cmder
|
||||
@@ -203,8 +202,8 @@ CMINVOKECOMMANDINFO
|
||||
CMINVOKECOMMANDINFOEX
|
||||
CMock
|
||||
CMONITORS
|
||||
cmph
|
||||
cmpgt
|
||||
cmph
|
||||
cne
|
||||
CNF
|
||||
coclass
|
||||
@@ -251,10 +250,7 @@ countof
|
||||
cph
|
||||
CPower
|
||||
cppblog
|
||||
cppruntime
|
||||
cppstd
|
||||
cppwinrt
|
||||
CProj
|
||||
createdump
|
||||
CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
@@ -609,7 +605,6 @@ hmenu
|
||||
hmodule
|
||||
hmonitor
|
||||
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
||||
HOOKPROC
|
||||
Hostbackdropbrush
|
||||
hotkeycontrol
|
||||
hotkeys
|
||||
@@ -677,7 +672,6 @@ imageresizerinput
|
||||
imageresizersettings
|
||||
imagingdevices
|
||||
ime
|
||||
imperialounce
|
||||
inetcpl
|
||||
Infobar
|
||||
INFOEXAMPLE
|
||||
@@ -939,7 +933,6 @@ MRT
|
||||
mru
|
||||
mrw
|
||||
msc
|
||||
msclr
|
||||
mscorlib
|
||||
msdata
|
||||
msedge
|
||||
@@ -1119,6 +1112,7 @@ PATINVERT
|
||||
PATPAINT
|
||||
PAUDIO
|
||||
pbc
|
||||
pbi
|
||||
PBlob
|
||||
pcb
|
||||
pcch
|
||||
@@ -1132,6 +1126,7 @@ pdo
|
||||
pdto
|
||||
pdtobj
|
||||
pdw
|
||||
Peb
|
||||
pef
|
||||
PElems
|
||||
Pels
|
||||
@@ -1514,7 +1509,6 @@ STATICEDGE
|
||||
STATSTG
|
||||
stdafx
|
||||
STDAPI
|
||||
stdcpp
|
||||
stdcpplatest
|
||||
STDMETHODCALLTYPE
|
||||
STDMETHODIMP
|
||||
@@ -1686,7 +1680,6 @@ Userenv
|
||||
USESHOWWINDOW
|
||||
USESIZE
|
||||
USESTDHANDLES
|
||||
usounce
|
||||
USRDLL
|
||||
UType
|
||||
uuidv
|
||||
@@ -1723,6 +1716,7 @@ VIDEOINFOHEADER
|
||||
viewmodel
|
||||
vih
|
||||
VIRTUALDESK
|
||||
VISEGRADRELAY
|
||||
visiblecolorformats
|
||||
Visibletrue
|
||||
visualeffects
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.50.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="WinUIEx" Version="2.2.0" />
|
||||
<PackageVersion Include="WPF-UI" Version="3.0.0" />
|
||||
|
||||
@@ -1363,7 +1363,7 @@ EXHIBIT A -Mozilla Public License.
|
||||
- System.Text.Encoding.CodePages 8.0.0
|
||||
- System.Text.Json 8.0.4
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 5.50.0
|
||||
- UnitsNet 5.56.0
|
||||
- UTF.Unknown 2.5.1
|
||||
- WinUIEx 2.2.0
|
||||
- WPF-UI 3.0.0
|
||||
|
||||
@@ -10,12 +10,23 @@ The build ID can be found in `Core\Constants.cs` in the `BuildId` variable - it
|
||||
|
||||
The build ID moniker is made up of two components - a reference to a [Halo](https://en.wikipedia.org/wiki/Halo_(franchise)) character, and the date when the work on the specific build started in the format of `MMDDYYYY`.
|
||||
|
||||
| Build ID | Build Date |
|
||||
|:----------------------------------------------------------|:-----------------|
|
||||
| [`DAISY023_04102024`](#DAISY023_04102024-april-10-2024) | April 10, 2024 |
|
||||
| [`ATRIOX_04132023`](#ATRIOX_04132023-april-13-2023) | April 13, 2023 |
|
||||
| [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 |
|
||||
| `ARBITER_01312022` | January 31, 2022 |
|
||||
| Build ID | Build Date |
|
||||
|:-------------------------------------------------------------------|:----------------|
|
||||
| [`VISEGRADRELAY_08152024`](#VISEGRADRELAY_08152024-august-15-2024) | August 15, 2024 |
|
||||
| [`DAISY023_04102024`](#DAISY023_04102024-april-10-2024) | April 10, 2024 |
|
||||
| [`ATRIOX_04132023`](#ATRIOX_04132023-april-13-2023) | April 13, 2023 |
|
||||
| [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 |
|
||||
| `ARBITER_01312022` | January 31, 2022 |
|
||||
|
||||
### `VISEGRADRELAY_08152024` (August 15, 2024)
|
||||
|
||||
>[!NOTE]
|
||||
>See pull request: [Awake - `VISEGRADRELAY_08152024`](https://github.com/microsoft/PowerToys/pull/34316)
|
||||
|
||||
- [#34148](https://github.com/microsoft/PowerToys/issues/34148) Fixes the issue where the Awake icon is not displayed.
|
||||
- [#17969](https://github.com/microsoft/PowerToys/issues/17969) Add the ability to bind the process target to the parent of the Awake launcher.
|
||||
- PID binding now correctly ignores irrelevant parameters (e.g., expiration, interval) and only works for indefinite periods.
|
||||
- Amending the native API surface to make sure that the Win32 error is set correctly.
|
||||
|
||||
### `DAISY023_04102024` (April 10, 2024)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
|
||||
| [PowerHexInspector](https://github.com/NaroZeol/PowerHexInspector) | [NaroZeol](https://github.com/NaroZeol) | Peek other forms of an input number |
|
||||
| [GitHubRepo](https://github.com/8LWXpg/PowerToysRun-GitHubRepo) | [8LWXpg](https://github.com/8LWXpg) | Search and open GitHub repositories |
|
||||
| [ProcessKiller](https://github.com/8LWXpg/PowerToysRun-ProcessKiller) | [8LWXpg](https://github.com/8LWXpg) | Search and kill processes |
|
||||
| [ChatGPT](https://github.com/ferraridavide/ChatGPTPowerToys) | [ferraridavide](https://github.com/ferraridavide) | Ask a question to ChatGPT |
|
||||
|
||||
## Extending software plugins
|
||||
|
||||
|
||||
@@ -51,17 +51,21 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::SHOW_COLOR_PICKER_SHARED_EVENT;
|
||||
}
|
||||
hstring Constants::ShowAdvancedPasteSharedEvent()
|
||||
hstring Constants::AdvancedPasteShowUIMessage()
|
||||
{
|
||||
return CommonSharedConstants::SHOW_ADVANCED_PASTE_SHARED_EVENT;
|
||||
return CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE;
|
||||
}
|
||||
hstring Constants::AdvancedPasteMarkdownEvent()
|
||||
hstring Constants::AdvancedPasteMarkdownMessage()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_EVENT;
|
||||
return CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_MESSAGE;
|
||||
}
|
||||
hstring Constants::AdvancedPasteJsonEvent()
|
||||
hstring Constants::AdvancedPasteJsonMessage()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_JSON_EVENT;
|
||||
return CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE;
|
||||
}
|
||||
hstring Constants::AdvancedPasteCustomActionMessage()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;
|
||||
}
|
||||
hstring Constants::ShowPowerOCRSharedEvent()
|
||||
{
|
||||
@@ -147,4 +151,8 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT;
|
||||
}
|
||||
hstring Constants::WorkspacesHotkeyEvent()
|
||||
{
|
||||
return CommonSharedConstants::WORKSPACES_HOTKEY_EVENT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring FZEToggleEvent();
|
||||
static hstring ColorPickerSendSettingsTelemetryEvent();
|
||||
static hstring ShowColorPickerSharedEvent();
|
||||
static hstring ShowAdvancedPasteSharedEvent();
|
||||
static hstring AdvancedPasteMarkdownEvent();
|
||||
static hstring AdvancedPasteJsonEvent();
|
||||
static hstring AdvancedPasteShowUIMessage();
|
||||
static hstring AdvancedPasteMarkdownMessage();
|
||||
static hstring AdvancedPasteJsonMessage();
|
||||
static hstring AdvancedPasteCustomActionMessage();
|
||||
static hstring ShowPowerOCRSharedEvent();
|
||||
static hstring MouseJumpShowPreviewEvent();
|
||||
static hstring AwakeExitEvent();
|
||||
@@ -40,6 +41,7 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring ShowEnvironmentVariablesSharedEvent();
|
||||
static hstring ShowEnvironmentVariablesAdminSharedEvent();
|
||||
static hstring WorkspacesLaunchEditorEvent();
|
||||
static hstring WorkspacesHotkeyEvent();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ namespace PowerToys
|
||||
static String FZEToggleEvent();
|
||||
static String ColorPickerSendSettingsTelemetryEvent();
|
||||
static String ShowColorPickerSharedEvent();
|
||||
static String ShowAdvancedPasteSharedEvent();
|
||||
static String AdvancedPasteMarkdownEvent();
|
||||
static String AdvancedPasteJsonEvent();
|
||||
static String AdvancedPasteShowUIMessage();
|
||||
static String AdvancedPasteMarkdownMessage();
|
||||
static String AdvancedPasteJsonMessage();
|
||||
static String AdvancedPasteCustomActionMessage();
|
||||
static String ShowPowerOCRSharedEvent();
|
||||
static String MouseJumpShowPreviewEvent();
|
||||
static String AwakeExitEvent();
|
||||
@@ -37,6 +38,7 @@ namespace PowerToys
|
||||
static String ShowEnvironmentVariablesSharedEvent();
|
||||
static String ShowEnvironmentVariablesAdminSharedEvent();
|
||||
static String WorkspacesLaunchEditorEvent();
|
||||
static String WorkspacesHotkeyEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,14 @@ namespace CommonSharedConstants
|
||||
|
||||
const wchar_t COLOR_PICKER_SEND_SETTINGS_TELEMETRY_EVENT[] = L"Local\\ColorPickerSettingsTelemetryEvent-6c7071d8-4014-46ec-b687-913bd8a422f1";
|
||||
|
||||
// Path to the event used to show Advanced Paste UI
|
||||
const wchar_t SHOW_ADVANCED_PASTE_SHARED_EVENT[] = L"Local\\ShowAdvancedPasteEvent-9a46be2a-3e05-4186-b56b-4ae986ef2526";
|
||||
// IPC Messages used in Advanced Paste
|
||||
const wchar_t ADVANCED_PASTE_SHOW_UI_MESSAGE[] = L"ShowUI";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_MARKDOWN_EVENT[] = L"Local\\AdvancedPasteJsonEvent-a18c0798-3ee6-4fc5-bb9f-114c57ac0d47";
|
||||
const wchar_t ADVANCED_PASTE_MARKDOWN_MESSAGE[] = L"PasteMarkdown";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_JSON_EVENT[] = L"Local\\AdvancedPasteJsonEvent-9ed021ab-b711-4cf3-9f33-135a698a9d21";
|
||||
const wchar_t ADVANCED_PASTE_JSON_MESSAGE[] = L"PasteJson";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
|
||||
|
||||
// Path to the event used to show Color Picker
|
||||
const wchar_t SHOW_COLOR_PICKER_SHARED_EVENT[] = L"Local\\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525";
|
||||
@@ -41,7 +43,9 @@ namespace CommonSharedConstants
|
||||
|
||||
const wchar_t FANCY_ZONES_EDITOR_TOGGLE_EVENT[] = L"Local\\FancyZones-ToggleEditorEvent-1e174338-06a3-472b-874d-073b21c62f14";
|
||||
|
||||
// Path to the event used by Workspaces
|
||||
const wchar_t WORKSPACES_LAUNCH_EDITOR_EVENT[] = L"Local\\Workspaces-LaunchEditorEvent-a55ff427-cf62-4994-a2cd-9f72139296bf";
|
||||
const wchar_t WORKSPACES_HOTKEY_EVENT[] = L"Local\\PowerToys-Workspaces-HotkeyEvent-2625C3C8-BAC9-4DB3-BCD6-3B4391A26FD0";
|
||||
|
||||
const wchar_t SHOW_HOSTS_EVENT[] = L"Local\\Hosts-ShowHostsEvent-5a0c0aae-5ff5-40f5-95c2-20e37ed671f0";
|
||||
|
||||
@@ -98,8 +102,6 @@ namespace CommonSharedConstants
|
||||
const wchar_t SHOW_ENVIRONMENT_VARIABLES_EVENT[] = L"Local\\PowerToysEnvironmentVariables-ShowEnvironmentVariablesEvent-1021f616-e951-4d64-b231-a8f972159978";
|
||||
const wchar_t SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT[] = L"Local\\PowerToysEnvironmentVariables-EnvironmentVariablesAdminEvent-8c95d2ad-047c-49a2-9e8b-b4656326cfb2";
|
||||
|
||||
const wchar_t WORKSPACES_EXIT_EVENT[] = L"Local\\PowerToys-Workspaces-ExitEvent-29a1566f-f4f8-4d56-9435-d2a437f727c6";
|
||||
|
||||
// Max DWORD for key code to disable keys.
|
||||
const DWORD VK_DISABLED = 0x100;
|
||||
}
|
||||
|
||||
@@ -24,4 +24,17 @@ properties:
|
||||
FancyzonesEditorHotkey: "Shift+Ctrl+Alt+F"
|
||||
FileLocksmith:
|
||||
Enabled: false
|
||||
ImageResizer:
|
||||
ImageResizerSizes:
|
||||
- Name: Square2x
|
||||
Width: 200
|
||||
Height: 200
|
||||
Unit: "Percent"
|
||||
Fit: "Stretch"
|
||||
- Name: MyInchSize
|
||||
Width: 1024
|
||||
Height: 1024
|
||||
Unit: "Inch"
|
||||
Fit: "Fit"
|
||||
|
||||
configurationVersion: 0.2.0
|
||||
|
||||
@@ -21,7 +21,7 @@ internal sealed class DSCGeneration
|
||||
public string Type;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, AdditionalPropertiesInfo> AdditionalPropertiesInfoPerModule = new Dictionary<string, AdditionalPropertiesInfo> { { "PowerLauncher", new AdditionalPropertiesInfo { Name = "Plugins", Type = "Hashtable[]" } } };
|
||||
private static readonly Dictionary<string, AdditionalPropertiesInfo> AdditionalPropertiesInfoPerModule = new Dictionary<string, AdditionalPropertiesInfo> { { "PowerLauncher", new AdditionalPropertiesInfo { Name = "Plugins", Type = "Hashtable[]" } }, { "ImageResizer", new AdditionalPropertiesInfo { Name = "ImageresizerSizes", Type = "Hashtable[]" } } };
|
||||
|
||||
private static string EmitEnumDefinition(Type type)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License. -->
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.11" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.12" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<displayName>PowerToys</displayName>
|
||||
<description>PowerToys</description>
|
||||
<resources>
|
||||
@@ -10,7 +10,6 @@
|
||||
<string id="InstallerUpdates">Installer and Updates</string>
|
||||
<string id="PowerToysRun">PowerToys Run</string>
|
||||
<string id="AdvancedPaste">Advanced Paste</string>
|
||||
<string id="Workspaces">Workspaces</string>
|
||||
<string id="MouseWithoutBorders">Mouse Without Borders</string>
|
||||
<string id="GeneralSettings">General settings</string>
|
||||
|
||||
@@ -112,12 +111,6 @@ If you don't configure this setting, users are able to enable or disable the plu
|
||||
You can override this policy for individual plugins using the policy "Configure enabled state for individual plugins".
|
||||
|
||||
Note: Changes require a restart of PowerToys Run.
|
||||
</string>
|
||||
<string id="ConfigureEnabledUtilityWorkspaces">This policy configures the enabled disable state for the Workspaces utility.
|
||||
|
||||
If you enable or don't configure this policy, the user takes control over the enabled state of the Workspaces utility.
|
||||
|
||||
If you disable this policy, the user won't be able to enable Enable and use the Workspaces utility.
|
||||
</string>
|
||||
<string id="PowerToysRunIndividualPluginEnabledStateDescription">With this policy you can configure an individual enabled state for each PowerToys Run plugin that you add to the list.
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.ViewModels;
|
||||
@@ -14,6 +18,7 @@ using Microsoft.UI.Xaml;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
using static AdvancedPaste.Helpers.NativeMethods;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
@@ -26,12 +31,13 @@ namespace AdvancedPaste
|
||||
{
|
||||
public IHost Host { get; private set; }
|
||||
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly OptionsViewModel viewModel;
|
||||
|
||||
private MainWindow window;
|
||||
|
||||
private nint windowHwnd;
|
||||
|
||||
private OptionsViewModel viewModel;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +80,7 @@ namespace AdvancedPaste
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
if (cmdArgs?.Length > 1)
|
||||
@@ -88,9 +94,44 @@ namespace AdvancedPaste
|
||||
}
|
||||
}
|
||||
|
||||
NativeEventWaiter.WaitForEventLoop(PowerToys.Interop.Constants.ShowAdvancedPasteSharedEvent(), OnAdvancedPasteHotkey);
|
||||
NativeEventWaiter.WaitForEventLoop(PowerToys.Interop.Constants.AdvancedPasteMarkdownEvent(), OnAdvancedPasteMarkdownHotkey);
|
||||
NativeEventWaiter.WaitForEventLoop(PowerToys.Interop.Constants.AdvancedPasteJsonEvent(), OnAdvancedPasteJsonHotkey);
|
||||
if (cmdArgs?.Length > 2)
|
||||
{
|
||||
ProcessNamedPipe(cmdArgs[2]);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessNamedPipe(string pipeName)
|
||||
{
|
||||
void OnMessage(string message) => _dispatcherQueue.TryEnqueue(() => OnNamedPipeMessage(message));
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var connectTimeout = TimeSpan.FromSeconds(10);
|
||||
await NamedPipeProcessor.ProcessNamedPipeAsync(pipeName, connectTimeout, OnMessage, CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnNamedPipeMessage(string message)
|
||||
{
|
||||
var messageParts = message.Split();
|
||||
var messageType = messageParts.First();
|
||||
|
||||
if (messageType == PowerToys.Interop.Constants.AdvancedPasteShowUIMessage())
|
||||
{
|
||||
OnAdvancedPasteHotkey();
|
||||
}
|
||||
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteMarkdownMessage())
|
||||
{
|
||||
OnAdvancedPasteMarkdownHotkey();
|
||||
}
|
||||
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteJsonMessage())
|
||||
{
|
||||
OnAdvancedPasteJsonHotkey();
|
||||
}
|
||||
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteCustomActionMessage())
|
||||
{
|
||||
OnAdvancedPasteCustomActionHotkey(messageParts);
|
||||
}
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
@@ -100,17 +141,43 @@ namespace AdvancedPaste
|
||||
|
||||
private void OnAdvancedPasteJsonHotkey()
|
||||
{
|
||||
viewModel.GetClipboardData();
|
||||
viewModel.ReadClipboard();
|
||||
viewModel.ToJsonFunction(true);
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteMarkdownHotkey()
|
||||
{
|
||||
viewModel.GetClipboardData();
|
||||
viewModel.ReadClipboard();
|
||||
viewModel.ToMarkdownFunction(true);
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteHotkey()
|
||||
{
|
||||
ShowWindow();
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteCustomActionHotkey(string[] messageParts)
|
||||
{
|
||||
if (messageParts.Length != 2)
|
||||
{
|
||||
Logger.LogWarning("Unexpected custom action message");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!int.TryParse(messageParts[1], CultureInfo.InvariantCulture, out int customActionId))
|
||||
{
|
||||
Logger.LogWarning($"Unexpected custom action message id {messageParts[1]}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowWindow();
|
||||
viewModel.ReadClipboard();
|
||||
viewModel.ExecuteCustomActionWithPaste(customActionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowWindow()
|
||||
{
|
||||
viewModel.OnShow();
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:converters="using:AdvancedPaste.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:AdvancedPaste.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
@@ -323,7 +324,12 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter
|
||||
x:Key="BoolToInvertedVisibilityConverter"
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
<converters:CountToVisibilityConverter x:Key="CountToVisibilityConverter" />
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid x:Name="PromptBoxGrid" Loaded="Grid_Loaded">
|
||||
@@ -340,13 +346,12 @@
|
||||
x:Name="InputTxtBox"
|
||||
HorizontalAlignment="Stretch"
|
||||
x:FieldModifier="public"
|
||||
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}"
|
||||
KeyDown="InputTxtBox_KeyDown"
|
||||
PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}"
|
||||
Style="{StaticResource CustomTextBoxStyle}"
|
||||
TabIndex="0"
|
||||
Text="{x:Bind Prompt, Mode=TwoWay}"
|
||||
TextChanging="InputTxtBox_TextChanging">
|
||||
Text="{x:Bind ViewModel.Query, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="InputTxtBoxTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
@@ -531,48 +536,63 @@
|
||||
<animations:OffsetAnimation Duration="0:0:1" />
|
||||
</animations:Implicit.Animations>
|
||||
</Button>-->
|
||||
<Button
|
||||
x:Name="SendBtn"
|
||||
x:Uid="SendButtonAutomation"
|
||||
<Grid
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="0,0,4,0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Command="{x:Bind GenerateCustomCommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
TabIndex="1"
|
||||
Visibility="Collapsed">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="SendBtnToolTip" TextWrapping="WrapWholeWords" />
|
||||
</ToolTipService.ToolTip>
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:ScaleAnimation
|
||||
From="0.4"
|
||||
To="1"
|
||||
Duration="0:0:0.167" />
|
||||
<animations:OpacityAnimation
|
||||
From="0.0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.167" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:ScaleAnimation
|
||||
From="1"
|
||||
To="0.4"
|
||||
Duration="0:0:0.167" />
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0.0"
|
||||
Duration="0:0:0.167" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</Button>
|
||||
<!--</StackPanel>-->
|
||||
VerticalAlignment="Stretch">
|
||||
<Button
|
||||
x:Name="SendBtn"
|
||||
x:Uid="SendButtonAutomation"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Command="{x:Bind GenerateCustomCommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
TabIndex="1"
|
||||
Visibility="{x:Bind ViewModel.Query.Length, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="SendBtnToolTip" TextWrapping="WrapWholeWords" />
|
||||
</ToolTipService.ToolTip>
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:ScaleAnimation
|
||||
From="0.4"
|
||||
To="1"
|
||||
Duration="0:0:0.167" />
|
||||
<animations:OpacityAnimation
|
||||
From="0.0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.167" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:ScaleAnimation
|
||||
From="1"
|
||||
To="0.4"
|
||||
Duration="0:0:0.167" />
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0.0"
|
||||
Duration="0:0:0.167" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</Button>
|
||||
<!-- Transparent overlay to show tooltip -->
|
||||
<Grid
|
||||
x:Name="SendBtnOverlay"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
Visibility="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<ToolTipService.ToolTip>
|
||||
<ToolTip Content="{x:Bind ViewModel.GeneralErrorText}" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</local:AnimatedContentControl>
|
||||
<ContentPresenter
|
||||
@@ -618,7 +638,7 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.InputTxtBoxErrorText, Mode=OneWay}" />
|
||||
Text="{x:Bind ViewModel.ApiErrorText, Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsBtn"
|
||||
Grid.Column="1"
|
||||
@@ -635,7 +655,6 @@
|
||||
<animations:OpacityAnimation To="0.0" Duration="0:0:0.167" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="DefaultState" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Net;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -18,23 +19,14 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
public sealed partial class PromptBox : Microsoft.UI.Xaml.Controls.UserControl
|
||||
{
|
||||
// Minimum time to show spinner when generating custom format using forcePasteCustom
|
||||
private static readonly TimeSpan MinTaskTime = TimeSpan.FromSeconds(2);
|
||||
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
public static readonly DependencyProperty PromptProperty = DependencyProperty.Register(
|
||||
nameof(Prompt),
|
||||
typeof(string),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: string.Empty));
|
||||
|
||||
public OptionsViewModel ViewModel { get; private set; }
|
||||
|
||||
public string Prompt
|
||||
{
|
||||
get => (string)GetValue(PromptProperty);
|
||||
set => SetValue(PromptProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
|
||||
nameof(PlaceholderText),
|
||||
typeof(string),
|
||||
@@ -66,6 +58,7 @@ namespace AdvancedPaste.Controls
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
|
||||
ViewModel = App.GetService<OptionsViewModel>();
|
||||
ViewModel.CustomActionActivated += (_, e) => GenerateCustom(e.ForcePasteCustom);
|
||||
}
|
||||
|
||||
private void Grid_Loaded(object sender, RoutedEventArgs e)
|
||||
@@ -74,27 +67,30 @@ namespace AdvancedPaste.Controls
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GenerateCustom()
|
||||
private void GenerateCustom() => GenerateCustom(false);
|
||||
|
||||
private void GenerateCustom(bool forcePasteCustom)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
VisualStateManager.GoToState(this, "LoadingState", true);
|
||||
string inputInstructions = InputTxtBox.Text;
|
||||
string inputInstructions = ViewModel.Query;
|
||||
ViewModel.SaveQuery(inputInstructions);
|
||||
|
||||
var customFormatTask = ViewModel.GenerateCustomFunction(inputInstructions);
|
||||
|
||||
customFormatTask.ContinueWith(
|
||||
t =>
|
||||
var delayTask = forcePasteCustom ? Task.Delay(MinTaskTime) : Task.CompletedTask;
|
||||
Task.WhenAll(customFormatTask, delayTask)
|
||||
.ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
ViewModel.CustomFormatResult = t.Result;
|
||||
ViewModel.CustomFormatResult = customFormatTask.Result;
|
||||
|
||||
if (ViewModel.ApiRequestStatus == (int)HttpStatusCode.OK)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "DefaultState", true);
|
||||
if (_userSettings.ShowCustomPreview)
|
||||
if (_userSettings.ShowCustomPreview && !forcePasteCustom)
|
||||
{
|
||||
PreviewGrid.Width = InputTxtBox.ActualWidth;
|
||||
PreviewFlyout.ShowAt(InputTxtBox);
|
||||
@@ -130,14 +126,9 @@ namespace AdvancedPaste.Controls
|
||||
ClipboardHelper.SetClipboardTextContent(lastQuery.ClipboardData);
|
||||
}
|
||||
|
||||
private void InputTxtBox_TextChanging(Microsoft.UI.Xaml.Controls.TextBox sender, TextBoxTextChangingEventArgs args)
|
||||
{
|
||||
SendBtn.Visibility = InputTxtBox.Text.Length > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0)
|
||||
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIEnabled)
|
||||
{
|
||||
GenerateCustom();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace AdvancedPaste.Converters;
|
||||
|
||||
public sealed class CountToDoubleConverter : IValueConverter
|
||||
{
|
||||
public double ValueIfZero { get; set; }
|
||||
|
||||
public double ValueIfNonZero { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
bool hasCount = ((value is int intValue) && intValue > 0) || (value is IEnumerable collection && collection.GetEnumerator().MoveNext());
|
||||
|
||||
return hasCount ? ValueIfNonZero : ValueIfZero;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
||||
@@ -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;
|
||||
using System.Collections;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace AdvancedPaste.Converters;
|
||||
|
||||
public sealed class CountToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
bool hasCount = ((value is int intValue) && intValue > 0) || (value is IEnumerable collection && collection.GetEnumerator().MoveNext());
|
||||
|
||||
if (targetType == typeof(Visibility))
|
||||
{
|
||||
return hasCount ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
else if (targetType == typeof(bool))
|
||||
{
|
||||
return hasCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(targetType));
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace AdvancedPaste.Converters
|
||||
{
|
||||
public sealed class ListViewIndexConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
var presenter = value as ListViewItemPresenter;
|
||||
var item = VisualTreeHelper.GetParent(presenter) as ListViewItem;
|
||||
|
||||
var listView = ItemsControl.ItemsControlFromItemContainer(item);
|
||||
int index = listView.IndexFromContainer(item) + 1;
|
||||
#pragma warning disable CA1305 // Specify IFormatProvider
|
||||
return index.ToString();
|
||||
#pragma warning restore CA1305 // Specify IFormatProvider
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,18 @@ namespace AdvancedPaste
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
|
||||
var baseHeight = MinHeight;
|
||||
|
||||
void UpdateHeight()
|
||||
{
|
||||
var trimmedCustomActionCount = Math.Min(_userSettings.CustomActions.Count, 5);
|
||||
Height = MinHeight = baseHeight + (trimmedCustomActionCount * 40);
|
||||
}
|
||||
|
||||
UpdateHeight();
|
||||
|
||||
_userSettings.CustomActions.CollectionChanged += (_, _) => UpdateHeight();
|
||||
|
||||
AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico");
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetTitleBar(titleBar);
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
xmlns:controls="using:AdvancedPaste.Controls"
|
||||
xmlns:converters="using:AdvancedPaste.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:library="using:Microsoft.PowerToys.Settings.UI.Library"
|
||||
xmlns:local="using:AdvancedPaste.Models"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
KeyDown="Page_KeyDown"
|
||||
KeyboardAcceleratorPlacementMode="Hidden"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<converters:ListViewIndexConverter x:Name="listViewIndexConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" />
|
||||
<converters:CountToVisibilityConverter x:Name="countToVisibilityConverter" />
|
||||
<converters:CountToDoubleConverter
|
||||
x:Name="customActionsCountToMinHeightConverter"
|
||||
ValueIfNonZero="40"
|
||||
ValueIfZero="0" />
|
||||
<Style
|
||||
x:Key="PaddingLessFlyoutPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
@@ -21,6 +28,38 @@
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
<DataTemplate x:Key="PasteFormatTemplate" x:DataType="local:PasteFormat">
|
||||
<Grid>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{x:Bind ToolTip}" />
|
||||
</ToolTipService.ToolTip>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="{x:Bind IconGlyph}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Text="{x:Bind Name}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ShortcutText, Mode=OneWay}"
|
||||
Visibility="{x:Bind ShortcutText.Length, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
<Page.KeyboardAccelerators>
|
||||
<KeyboardAccelerator Key="Escape" Invoked="KeyboardAccelerator_Invoked" />
|
||||
@@ -36,6 +75,30 @@
|
||||
Key="Number3"
|
||||
Invoked="KeyboardAccelerator_Invoked"
|
||||
Modifiers="Control" />
|
||||
<KeyboardAccelerator
|
||||
Key="Number4"
|
||||
Invoked="KeyboardAccelerator_Invoked"
|
||||
Modifiers="Control" />
|
||||
<KeyboardAccelerator
|
||||
Key="Number5"
|
||||
Invoked="KeyboardAccelerator_Invoked"
|
||||
Modifiers="Control" />
|
||||
<KeyboardAccelerator
|
||||
Key="Number6"
|
||||
Invoked="KeyboardAccelerator_Invoked"
|
||||
Modifiers="Control" />
|
||||
<KeyboardAccelerator
|
||||
Key="Number7"
|
||||
Invoked="KeyboardAccelerator_Invoked"
|
||||
Modifiers="Control" />
|
||||
<KeyboardAccelerator
|
||||
Key="Number8"
|
||||
Invoked="KeyboardAccelerator_Invoked"
|
||||
Modifiers="Control" />
|
||||
<KeyboardAccelerator
|
||||
Key="Number9"
|
||||
Invoked="KeyboardAccelerator_Invoked"
|
||||
Modifiers="Control" />
|
||||
</Page.KeyboardAccelerators>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -103,73 +166,55 @@
|
||||
BorderThickness="0,1,0,0"
|
||||
RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsCountToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteOptionsListView_ItemClick"
|
||||
ItemClick="ListView_Click"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemsSource="{x:Bind pasteFormats, Mode=OneWay}"
|
||||
ItemTemplate="{StaticResource PasteFormatTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
TabIndex="1">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:PasteFormat">
|
||||
<Grid>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock>
|
||||
<Run Text="{x:Bind Name}" />
|
||||
<Run Text="(" /><Run Text="Ctrl" /><Run Text="+" /><Run Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource listViewIndexConverter}}" /><Run Text=")" />
|
||||
</TextBlock>
|
||||
</ToolTipService.ToolTip>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Viewbox
|
||||
x:Name="IconHolderBox"
|
||||
MaxWidth="16"
|
||||
MaxHeight="16"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
<ContentPresenter
|
||||
x:Name="IconHolder"
|
||||
x:Phase="2"
|
||||
Content="{x:Bind Icon}" />
|
||||
</Viewbox>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Text="{x:Bind Name}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}">
|
||||
<Run Text="Ctrl" /><Run Text="+" /><Run Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource listViewIndexConverter}}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
TabIndex="1" />
|
||||
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
|
||||
<ListView
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_Click"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplate="{StaticResource PasteFormatTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
|
||||
<Rectangle
|
||||
Grid.Row="3"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
<!-- x:Uid="ClipboardHistoryButton" -->
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Row="4"
|
||||
Height="32"
|
||||
Margin="4,0,4,4"
|
||||
Padding="{StaticResource ButtonPadding}"
|
||||
|
||||
@@ -6,7 +6,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
@@ -25,8 +24,8 @@ namespace AdvancedPaste.Pages
|
||||
public sealed partial class MainPage : Page
|
||||
{
|
||||
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
|
||||
private readonly ObservableCollection<PasteFormat> pasteFormats;
|
||||
private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
|
||||
private (VirtualKey Key, DateTime Timestamp) _lastKeyEvent = (VirtualKey.None, DateTime.MinValue);
|
||||
|
||||
public OptionsViewModel ViewModel { get; private set; }
|
||||
|
||||
@@ -34,13 +33,6 @@ namespace AdvancedPaste.Pages
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
pasteFormats =
|
||||
[
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE8E9" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsPlainText"), Format = PasteFormats.PlainText },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\ue8a5" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsMarkdown"), Format = PasteFormats.Markdown },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE943" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsJson"), Format = PasteFormats.Json },
|
||||
];
|
||||
|
||||
ViewModel = App.GetService<OptionsViewModel>();
|
||||
|
||||
clipboardHistory = new ObservableCollection<ClipboardItem>();
|
||||
@@ -121,6 +113,8 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private static MainWindow GetMainWindow() => (App.Current as App)?.GetMainWindow();
|
||||
|
||||
private void ClipboardHistoryItemDeleteButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
@@ -135,83 +129,49 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void PasteAsPlain()
|
||||
{
|
||||
ViewModel.ToPlainTextFunction();
|
||||
}
|
||||
|
||||
private void PasteAsMarkdown()
|
||||
{
|
||||
ViewModel.ToMarkdownFunction();
|
||||
}
|
||||
|
||||
private void PasteAsJson()
|
||||
{
|
||||
ViewModel.ToJsonFunction();
|
||||
}
|
||||
|
||||
private void PasteOptionsListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
private void ListView_Click(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is PasteFormat format)
|
||||
{
|
||||
switch (format.Format)
|
||||
{
|
||||
case PasteFormats.PlainText:
|
||||
{
|
||||
PasteAsPlain();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.PlainText));
|
||||
break;
|
||||
}
|
||||
|
||||
case PasteFormats.Markdown:
|
||||
{
|
||||
PasteAsMarkdown();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.Markdown));
|
||||
break;
|
||||
}
|
||||
|
||||
case PasteFormats.Json:
|
||||
{
|
||||
PasteAsJson();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.Json));
|
||||
break;
|
||||
}
|
||||
}
|
||||
ViewModel.ExecutePasteFormat(format);
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyboardAccelerator_Invoked(Microsoft.UI.Xaml.Input.KeyboardAccelerator sender, Microsoft.UI.Xaml.Input.KeyboardAcceleratorInvokedEventArgs args)
|
||||
{
|
||||
if (GetMainWindow()?.Visible is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogTrace();
|
||||
|
||||
var thisKeyEvent = (sender.Key, Timestamp: DateTime.Now);
|
||||
if (thisKeyEvent.Key == _lastKeyEvent.Key && (thisKeyEvent.Timestamp - _lastKeyEvent.Timestamp) < TimeSpan.FromMilliseconds(200))
|
||||
{
|
||||
// Sometimes, multiple keyboard accelerator events are raised for a single Ctrl + VirtualKey press.
|
||||
return;
|
||||
}
|
||||
|
||||
_lastKeyEvent = thisKeyEvent;
|
||||
|
||||
switch (sender.Key)
|
||||
{
|
||||
case VirtualKey.Escape:
|
||||
{
|
||||
(App.Current as App).GetMainWindow().Close();
|
||||
break;
|
||||
}
|
||||
GetMainWindow()?.Close();
|
||||
break;
|
||||
|
||||
case VirtualKey.Number1:
|
||||
{
|
||||
PasteAsPlain();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(PasteFormats.PlainText));
|
||||
break;
|
||||
}
|
||||
|
||||
case VirtualKey.Number2:
|
||||
{
|
||||
PasteAsMarkdown();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(PasteFormats.Markdown));
|
||||
break;
|
||||
}
|
||||
|
||||
case VirtualKey.Number3:
|
||||
{
|
||||
PasteAsJson();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(PasteFormats.Json));
|
||||
break;
|
||||
}
|
||||
case VirtualKey.Number4:
|
||||
case VirtualKey.Number5:
|
||||
case VirtualKey.Number6:
|
||||
case VirtualKey.Number7:
|
||||
case VirtualKey.Number8:
|
||||
case VirtualKey.Number9:
|
||||
ViewModel.ExecutePasteFormat(sender.Key);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
@@ -222,7 +182,7 @@ namespace AdvancedPaste.Pages
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
(App.Current as App).GetMainWindow().Close();
|
||||
GetMainWindow()?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Settings
|
||||
{
|
||||
public interface IUserSettings
|
||||
@@ -11,5 +14,7 @@ namespace AdvancedPaste.Settings
|
||||
public bool SendPasteKeyCombination { get; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
|
||||
public ObservableCollection<AdvancedPasteCustomAction> CustomActions { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AdvancedPaste.Helpers;
|
||||
|
||||
public static class NamedPipeProcessor
|
||||
{
|
||||
public static async Task ProcessNamedPipeAsync(string pipeName, TimeSpan connectTimeout, Action<string> messageHandler, CancellationToken cancellationToken)
|
||||
{
|
||||
using NamedPipeClientStream pipeClient = new(".", pipeName, PipeDirection.In);
|
||||
|
||||
await pipeClient.ConnectAsync(connectTimeout, cancellationToken);
|
||||
|
||||
using StreamReader streamReader = new(pipeClient, Encoding.Unicode);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var message = await streamReader.ReadLineAsync(cancellationToken);
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
messageHandler(message);
|
||||
}
|
||||
|
||||
var intraMessageDelay = TimeSpan.FromMilliseconds(10);
|
||||
await Task.Delay(intraMessageDelay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace AdvancedPaste.Helpers
|
||||
{
|
||||
public static class NativeEventWaiter
|
||||
{
|
||||
public static void WaitForEventLoop(string eventName, Action callback)
|
||||
{
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
new Thread(() =>
|
||||
{
|
||||
var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
|
||||
while (true)
|
||||
{
|
||||
if (eventHandle.WaitOne())
|
||||
{
|
||||
dispatcherQueue.TryEnqueue(() => callback());
|
||||
}
|
||||
}
|
||||
}).Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,37 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO.Abstractions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||
|
||||
namespace AdvancedPaste.Settings
|
||||
{
|
||||
internal sealed class UserSettings : IUserSettings
|
||||
internal sealed class UserSettings : IUserSettings, IDisposable
|
||||
{
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private readonly TaskScheduler _taskScheduler;
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
private readonly object _loadingSettingsLock = new object();
|
||||
private readonly object _loadingSettingsLock = new();
|
||||
|
||||
private const string AdvancedPasteModuleName = "AdvancedPaste";
|
||||
private const int MaxNumberOfRetry = 5;
|
||||
|
||||
private bool _disposedValue;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
public bool ShowCustomPreview { get; private set; }
|
||||
|
||||
public bool SendPasteKeyCombination { get; private set; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
|
||||
public ObservableCollection<AdvancedPasteCustomAction> CustomActions { get; private set; }
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
@@ -33,10 +41,25 @@ namespace AdvancedPaste.Settings
|
||||
ShowCustomPreview = true;
|
||||
SendPasteKeyCombination = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
CustomActions = [];
|
||||
|
||||
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
LoadSettingsFromJson();
|
||||
|
||||
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", () => LoadSettingsFromJson());
|
||||
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", OnSettingsFileChanged);
|
||||
}
|
||||
|
||||
private void OnSettingsFileChanged()
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
Task.Delay(TimeSpan.FromMilliseconds(500))
|
||||
.ContinueWith(_ => LoadSettingsFromJson(), _cancellationTokenSource.Token, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSettingsFromJson()
|
||||
@@ -62,9 +85,25 @@ namespace AdvancedPaste.Settings
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteModuleName);
|
||||
if (settings != null)
|
||||
{
|
||||
ShowCustomPreview = settings.Properties.ShowCustomPreview;
|
||||
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
|
||||
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
|
||||
void UpdateSettings()
|
||||
{
|
||||
ShowCustomPreview = settings.Properties.ShowCustomPreview;
|
||||
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
|
||||
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
|
||||
|
||||
CustomActions.Clear();
|
||||
foreach (var customAction in settings.Properties.CustomActions.Value)
|
||||
{
|
||||
if (customAction.IsShown && customAction.IsValid)
|
||||
{
|
||||
CustomActions.Add(customAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task.Factory
|
||||
.StartNew(UpdateSettings, CancellationToken.None, TaskCreationOptions.None, _taskScheduler)
|
||||
.Wait();
|
||||
}
|
||||
|
||||
retry = false;
|
||||
@@ -82,5 +121,30 @@ namespace AdvancedPaste.Settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_cancellationTokenSource.Dispose();
|
||||
_watcher.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
~UserSettings()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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;
|
||||
|
||||
namespace AdvancedPaste.Models;
|
||||
|
||||
public sealed class CustomActionActivatedEventArgs(string text, bool forcePasteCustom) : EventArgs
|
||||
{
|
||||
public string Text { get; private set; } = text;
|
||||
|
||||
public bool ForcePasteCustom { get; private set; } = forcePasteCustom;
|
||||
}
|
||||
@@ -2,16 +2,38 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Models
|
||||
namespace AdvancedPaste.Models;
|
||||
|
||||
public partial class PasteFormat : ObservableObject
|
||||
{
|
||||
public class PasteFormat
|
||||
[ObservableProperty]
|
||||
private string _shortcutText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _toolTip = string.Empty;
|
||||
|
||||
public PasteFormat()
|
||||
{
|
||||
public IconElement Icon { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public PasteFormats Format { get; set; }
|
||||
}
|
||||
|
||||
public PasteFormat(AdvancedPasteCustomAction customAction, string shortcutText)
|
||||
{
|
||||
IconGlyph = "\uE945";
|
||||
Name = customAction.Name;
|
||||
Prompt = customAction.Prompt;
|
||||
Format = PasteFormats.Custom;
|
||||
ShortcutText = shortcutText;
|
||||
ToolTip = customAction.Prompt;
|
||||
}
|
||||
|
||||
public string IconGlyph { get; init; }
|
||||
|
||||
public string Name { get; init; }
|
||||
|
||||
public PasteFormats Format { get; init; }
|
||||
|
||||
public string Prompt { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -59,10 +59,7 @@
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root"
|
||||
xmlns=""
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
@@ -228,4 +225,7 @@
|
||||
<data name="OpenAIGpoDisabled" xml:space="preserve">
|
||||
<value>To custom with AI is disabled by your organization</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="CtrlKey" xml:space="preserve">
|
||||
<value>Ctrl</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -15,49 +16,73 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Win32;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.System;
|
||||
using WinUIEx;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
|
||||
namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
public partial class OptionsViewModel : ObservableObject
|
||||
public partial class OptionsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly DispatcherTimer _clipboardTimer;
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
private App app = App.Current as App;
|
||||
|
||||
private AICompletionsHelper aiHelper;
|
||||
private readonly AICompletionsHelper aiHelper;
|
||||
private readonly App app = App.Current as App;
|
||||
private readonly PasteFormat[] _allStandardPasteFormats;
|
||||
|
||||
public DataPackageView ClipboardData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
|
||||
private bool _isClipboardDataText;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
private bool _isCustomAIEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _clipboardHistoryEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxErrorText))]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
|
||||
private bool _isAllowedByGPO;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ApiErrorText))]
|
||||
private int _apiRequestStatus;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _query = string.Empty;
|
||||
|
||||
private bool _pasteFormatsDirty;
|
||||
|
||||
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
|
||||
|
||||
public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = [];
|
||||
|
||||
public bool IsCustomAIEnabled => IsAllowedByGPO && IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
|
||||
public event EventHandler<CustomActionActivatedEventArgs> CustomActionActivated;
|
||||
|
||||
public OptionsViewModel(IUserSettings userSettings)
|
||||
{
|
||||
aiHelper = new AICompletionsHelper();
|
||||
_userSettings = userSettings;
|
||||
|
||||
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
|
||||
ApiRequestStatus = (int)HttpStatusCode.OK;
|
||||
|
||||
_allStandardPasteFormats =
|
||||
[
|
||||
new PasteFormat { IconGlyph = "\uE8E9", Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsPlainText"), Format = PasteFormats.PlainText },
|
||||
new PasteFormat { IconGlyph = "\ue8a5", Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsMarkdown"), Format = PasteFormats.Markdown },
|
||||
new PasteFormat { IconGlyph = "\uE943", Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsJson"), Format = PasteFormats.Json },
|
||||
];
|
||||
|
||||
GeneratedResponses = new ObservableCollection<string>();
|
||||
GeneratedResponses.CollectionChanged += (s, e) =>
|
||||
{
|
||||
@@ -66,10 +91,87 @@ namespace AdvancedPaste.ViewModels
|
||||
};
|
||||
|
||||
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
|
||||
GetClipboardData();
|
||||
ReadClipboard();
|
||||
_clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
_clipboardTimer.Tick += ClipboardTimer_Tick;
|
||||
_clipboardTimer.Start();
|
||||
|
||||
RefreshPasteFormats();
|
||||
_userSettings.CustomActions.CollectionChanged += (_, _) => EnqueueRefreshPasteFormats();
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Query))
|
||||
{
|
||||
EnqueueRefreshPasteFormats();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void GetClipboardData()
|
||||
private void ClipboardTimer_Tick(object sender, object e)
|
||||
{
|
||||
if (app.GetMainWindow()?.Visible is true)
|
||||
{
|
||||
ReadClipboard();
|
||||
UpdateAllowedByGPO();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnqueueRefreshPasteFormats()
|
||||
{
|
||||
if (_pasteFormatsDirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pasteFormatsDirty = true;
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
RefreshPasteFormats();
|
||||
_pasteFormatsDirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
private void RefreshPasteFormats()
|
||||
{
|
||||
bool Filter(string text) => text.Contains(Query, StringComparison.CurrentCultureIgnoreCase);
|
||||
|
||||
var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey");
|
||||
int shortcutNum = 0;
|
||||
|
||||
string GetNextShortcutText()
|
||||
{
|
||||
shortcutNum++;
|
||||
return shortcutNum <= 9 ? $"{ctrlString}+{shortcutNum}" : string.Empty;
|
||||
}
|
||||
|
||||
StandardPasteFormats.Clear();
|
||||
foreach (var format in _allStandardPasteFormats)
|
||||
{
|
||||
if (Filter(format.Name))
|
||||
{
|
||||
format.ShortcutText = GetNextShortcutText();
|
||||
format.ToolTip = $"{format.Name} ({format.ShortcutText})";
|
||||
StandardPasteFormats.Add(format);
|
||||
}
|
||||
}
|
||||
|
||||
CustomActionPasteFormats.Clear();
|
||||
foreach (var customAction in _userSettings.CustomActions)
|
||||
{
|
||||
if (Filter(customAction.Name) || Filter(customAction.Prompt))
|
||||
{
|
||||
CustomActionPasteFormats.Add(new PasteFormat(customAction, GetNextShortcutText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_clipboardTimer.Stop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void ReadClipboard()
|
||||
{
|
||||
ClipboardData = Clipboard.GetContent();
|
||||
IsClipboardDataText = ClipboardData.Contains(StandardDataFormats.Text);
|
||||
@@ -77,14 +179,10 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
GetClipboardData();
|
||||
ReadClipboard();
|
||||
UpdateAllowedByGPO();
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
IsCustomAIEnabled = false;
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
}
|
||||
else
|
||||
if (IsAllowedByGPO)
|
||||
{
|
||||
var openAIKey = AICompletionsHelper.LoadOpenAIKey();
|
||||
var currentKey = aiHelper.GetKey();
|
||||
@@ -104,15 +202,12 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
app.GetMainWindow().FinishLoading(aiHelper.IsAIEnabled);
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
OnPropertyChanged(nameof(GeneralErrorText));
|
||||
OnPropertyChanged(nameof(IsCustomAIEnabled));
|
||||
});
|
||||
},
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
|
||||
@@ -152,47 +247,44 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
app.GetMainWindow().ClearInputText();
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
|
||||
}
|
||||
else if (!aiHelper.IsAIEnabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
|
||||
}
|
||||
else if (!IsClipboardDataText)
|
||||
return IsClipboardDataText ? ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText") : GeneralErrorText;
|
||||
}
|
||||
}
|
||||
|
||||
public string GeneralErrorText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsClipboardDataText)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataTypeMismatchWarning");
|
||||
}
|
||||
|
||||
if (!IsAllowedByGPO)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
|
||||
}
|
||||
|
||||
if (!aiHelper.IsAIEnabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string InputTxtBoxErrorText
|
||||
public string ApiErrorText
|
||||
{
|
||||
get
|
||||
get => (HttpStatusCode)ApiRequestStatus switch
|
||||
{
|
||||
if (ApiRequestStatus != (int)HttpStatusCode.OK)
|
||||
{
|
||||
if (ApiRequestStatus == (int)HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests");
|
||||
}
|
||||
else if (ApiRequestStatus == (int)HttpStatusCode.Unauthorized)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + ApiRequestStatus.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
HttpStatusCode.TooManyRequests => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests"),
|
||||
HttpStatusCode.Unauthorized => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized"),
|
||||
HttpStatusCode.OK => string.Empty,
|
||||
_ => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + ApiRequestStatus.ToString(CultureInfo.InvariantCulture),
|
||||
};
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -201,7 +293,12 @@ namespace AdvancedPaste.ViewModels
|
||||
[RelayCommand]
|
||||
public void PasteCustom()
|
||||
{
|
||||
PasteCustomFunction(GeneratedResponses[CurrentResponseIndex]);
|
||||
var text = GeneratedResponses.ElementAtOrDefault(CurrentResponseIndex);
|
||||
|
||||
if (text != null)
|
||||
{
|
||||
PasteCustomFunction(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Command to select the previous custom format
|
||||
@@ -306,6 +403,59 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal void ExecutePasteFormat(VirtualKey key)
|
||||
{
|
||||
var index = key - VirtualKey.Number1;
|
||||
var pasteFormat = StandardPasteFormats.ElementAtOrDefault(index) ?? CustomActionPasteFormats.ElementAtOrDefault(index - StandardPasteFormats.Count);
|
||||
|
||||
if (pasteFormat != null)
|
||||
{
|
||||
ExecutePasteFormat(pasteFormat);
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(pasteFormat.Format));
|
||||
}
|
||||
}
|
||||
|
||||
internal void ExecutePasteFormat(PasteFormat pasteFormat)
|
||||
{
|
||||
if (!IsClipboardDataText || (pasteFormat.Format == PasteFormats.Custom && !IsCustomAIEnabled))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pasteFormat.Format)
|
||||
{
|
||||
case PasteFormats.PlainText:
|
||||
ToPlainTextFunction();
|
||||
break;
|
||||
|
||||
case PasteFormats.Markdown:
|
||||
ToMarkdownFunction();
|
||||
break;
|
||||
|
||||
case PasteFormats.Json:
|
||||
ToJsonFunction();
|
||||
break;
|
||||
|
||||
case PasteFormats.Custom:
|
||||
Query = pasteFormat.Prompt;
|
||||
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ExecuteCustomActionWithPaste(int customActionId)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var customAction = _userSettings.CustomActions.FirstOrDefault(customAction => customAction.Id == customActionId);
|
||||
|
||||
if (customAction != null)
|
||||
{
|
||||
Query = customAction.Prompt;
|
||||
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(customAction.Prompt, true));
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<string> GenerateCustomFunction(string inputInstructions)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
@@ -315,7 +465,7 @@ namespace AdvancedPaste.ViewModels
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (ClipboardData == null || !ClipboardData.Contains(StandardDataFormats.Text))
|
||||
if (!IsClipboardDataText)
|
||||
{
|
||||
Logger.LogWarning("Clipboard does not contain text data");
|
||||
return string.Empty;
|
||||
@@ -416,5 +566,10 @@ namespace AdvancedPaste.ViewModels
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAllowedByGPO()
|
||||
{
|
||||
IsAllowedByGPO = PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
|
||||
#include <atlfile.h>
|
||||
#include <atlstr.h>
|
||||
#include <vector>
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
@@ -35,6 +39,10 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lp
|
||||
namespace
|
||||
{
|
||||
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
|
||||
const wchar_t JSON_KEY_CUSTOM_ACTIONS[] = L"custom-actions";
|
||||
const wchar_t JSON_KEY_SHORTCUT[] = L"shortcut";
|
||||
const wchar_t JSON_KEY_IS_SHOWN[] = L"isShown";
|
||||
const wchar_t JSON_KEY_ID[] = L"id";
|
||||
const wchar_t JSON_KEY_WIN[] = L"win";
|
||||
const wchar_t JSON_KEY_ALT[] = L"alt";
|
||||
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
|
||||
@@ -60,33 +68,30 @@ private:
|
||||
|
||||
HANDLE m_hProcess;
|
||||
|
||||
std::thread create_pipe_thread;
|
||||
std::unique_ptr<CAtlFile> m_write_pipe;
|
||||
|
||||
// Time to wait for process to close after sending WM_CLOSE signal
|
||||
static const int MAX_WAIT_MILLISEC = 10000;
|
||||
static const constexpr int MAX_WAIT_MILLISEC = 10000;
|
||||
|
||||
static const constexpr int NUM_DEFAULT_HOTKEYS = 4;
|
||||
|
||||
Hotkey m_paste_as_plain_hotkey = { .win = true, .ctrl = true, .shift = false, .alt = true, .key = 'V' };
|
||||
Hotkey m_advanced_paste_ui_hotkey = { .win = true, .ctrl = false, .shift = true, .alt = false, .key = 'V' };
|
||||
Hotkey m_paste_as_markdown_hotkey{};
|
||||
Hotkey m_paste_as_json_hotkey{};
|
||||
|
||||
std::vector<Hotkey> m_custom_action_hotkeys;
|
||||
std::vector<int> m_custom_action_ids;
|
||||
|
||||
bool m_preview_custom_format_output = true;
|
||||
|
||||
// Handle to event used to invoke AdvancedPaste
|
||||
HANDLE m_hShowUIEvent;
|
||||
HANDLE m_hPasteMarkdownEvent;
|
||||
HANDLE m_hPasteJsonEvent;
|
||||
|
||||
Hotkey parse_single_hotkey(const wchar_t* hotkey, const winrt::Windows::Data::Json::JsonObject& settingsObject)
|
||||
Hotkey parse_single_hotkey(const wchar_t* keyName, const winrt::Windows::Data::Json::JsonObject& settingsObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
Hotkey _temp_paste_as_plain;
|
||||
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(hotkey);
|
||||
_temp_paste_as_plain.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
|
||||
_temp_paste_as_plain.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
|
||||
_temp_paste_as_plain.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||
_temp_paste_as_plain.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||
_temp_paste_as_plain.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||
return _temp_paste_as_plain;
|
||||
const auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(keyName);
|
||||
return parse_single_hotkey(jsonHotkeyObject);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -96,6 +101,38 @@ private:
|
||||
return {};
|
||||
}
|
||||
|
||||
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
Hotkey hotkey;
|
||||
hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
|
||||
hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
|
||||
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||
return hotkey;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to initialize AdvancedPaste shortcut from settings. Value will keep unchanged.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static json::JsonObject to_json_object(const Hotkey& hotkey)
|
||||
{
|
||||
json::JsonObject jsonObject;
|
||||
jsonObject.SetNamedValue(JSON_KEY_WIN, json::value(hotkey.win));
|
||||
jsonObject.SetNamedValue(JSON_KEY_ALT, json::value(hotkey.alt));
|
||||
jsonObject.SetNamedValue(JSON_KEY_SHIFT, json::value(hotkey.shift));
|
||||
jsonObject.SetNamedValue(JSON_KEY_CTRL, json::value(hotkey.ctrl));
|
||||
jsonObject.SetNamedValue(JSON_KEY_CODE, json::value(hotkey.key));
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey)
|
||||
{
|
||||
const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
|
||||
@@ -131,7 +168,7 @@ private:
|
||||
{
|
||||
auto settingsObject = settings.get_raw_json();
|
||||
|
||||
// Migrate Paste As PLain text shortcut
|
||||
// Migrate Paste As Plain text shortcut
|
||||
Hotkey old_paste_as_plain_hotkey;
|
||||
bool old_data_migrated = migrate_data_and_remove_data_file(old_paste_as_plain_hotkey);
|
||||
if (old_data_migrated)
|
||||
@@ -139,12 +176,7 @@ private:
|
||||
m_paste_as_plain_hotkey = old_paste_as_plain_hotkey;
|
||||
|
||||
// override settings file
|
||||
json::JsonObject new_hotkey_value;
|
||||
new_hotkey_value.SetNamedValue(JSON_KEY_WIN, json::value(old_paste_as_plain_hotkey.win));
|
||||
new_hotkey_value.SetNamedValue(JSON_KEY_ALT, json::value(old_paste_as_plain_hotkey.alt));
|
||||
new_hotkey_value.SetNamedValue(JSON_KEY_SHIFT, json::value(old_paste_as_plain_hotkey.shift));
|
||||
new_hotkey_value.SetNamedValue(JSON_KEY_CTRL, json::value(old_paste_as_plain_hotkey.ctrl));
|
||||
new_hotkey_value.SetNamedValue(JSON_KEY_CODE, json::value(old_paste_as_plain_hotkey.key));
|
||||
const auto new_hotkey_value = to_json_object(old_paste_as_plain_hotkey);
|
||||
|
||||
if (!settingsObject.HasKey(JSON_KEY_PROPERTIES))
|
||||
{
|
||||
@@ -153,13 +185,7 @@ private:
|
||||
|
||||
settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).SetNamedValue(JSON_KEY_PASTE_AS_PLAIN_HOTKEY, new_hotkey_value);
|
||||
|
||||
|
||||
json::JsonObject ui_hotkey;
|
||||
ui_hotkey.SetNamedValue(JSON_KEY_WIN, json::value(m_advanced_paste_ui_hotkey.win));
|
||||
ui_hotkey.SetNamedValue(JSON_KEY_ALT, json::value(m_advanced_paste_ui_hotkey.alt));
|
||||
ui_hotkey.SetNamedValue(JSON_KEY_SHIFT, json::value(m_advanced_paste_ui_hotkey.shift));
|
||||
ui_hotkey.SetNamedValue(JSON_KEY_CTRL, json::value(m_advanced_paste_ui_hotkey.ctrl));
|
||||
ui_hotkey.SetNamedValue(JSON_KEY_CODE, json::value(m_advanced_paste_ui_hotkey.key));
|
||||
const auto ui_hotkey = to_json_object(m_advanced_paste_ui_hotkey);
|
||||
settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).SetNamedValue(JSON_KEY_ADVANCED_PASTE_UI_HOTKEY, ui_hotkey);
|
||||
|
||||
settings.save_to_settings_file();
|
||||
@@ -168,40 +194,56 @@ private:
|
||||
{
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_PASTE_AS_PLAIN_HOTKEY))
|
||||
const std::array<std::pair<Hotkey*, LPCWSTR>, NUM_DEFAULT_HOTKEYS> defaultHotkeys{
|
||||
{ { &m_paste_as_plain_hotkey, JSON_KEY_PASTE_AS_PLAIN_HOTKEY },
|
||||
{ &m_advanced_paste_ui_hotkey, JSON_KEY_ADVANCED_PASTE_UI_HOTKEY },
|
||||
{ &m_paste_as_markdown_hotkey, JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY },
|
||||
{ &m_paste_as_json_hotkey, JSON_KEY_PASTE_AS_JSON_HOTKEY } }
|
||||
};
|
||||
|
||||
for (auto& [hotkey, keyName] : defaultHotkeys)
|
||||
{
|
||||
m_paste_as_plain_hotkey = parse_single_hotkey(JSON_KEY_PASTE_AS_PLAIN_HOTKEY, settingsObject);
|
||||
*hotkey = parse_single_hotkey(keyName, settingsObject);
|
||||
}
|
||||
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_ADVANCED_PASTE_UI_HOTKEY))
|
||||
|
||||
m_custom_action_hotkeys.clear();
|
||||
m_custom_action_ids.clear();
|
||||
|
||||
if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
|
||||
{
|
||||
m_advanced_paste_ui_hotkey = parse_single_hotkey(JSON_KEY_ADVANCED_PASTE_UI_HOTKEY, settingsObject);
|
||||
}
|
||||
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY))
|
||||
{
|
||||
m_paste_as_markdown_hotkey = parse_single_hotkey(JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY, settingsObject);
|
||||
}
|
||||
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_PASTE_AS_JSON_HOTKEY))
|
||||
{
|
||||
m_paste_as_json_hotkey = parse_single_hotkey(JSON_KEY_PASTE_AS_JSON_HOTKEY, settingsObject);
|
||||
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS))
|
||||
{
|
||||
const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE);
|
||||
|
||||
for (const auto& customAction : customActions)
|
||||
{
|
||||
const auto object = customAction.GetObjectW();
|
||||
|
||||
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
|
||||
{
|
||||
m_custom_action_hotkeys.push_back(parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT)));
|
||||
m_custom_action_ids.push_back(static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_process_running()
|
||||
bool is_process_running() const
|
||||
{
|
||||
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
void launch_process(const std::wstring& arg = L"")
|
||||
void launch_process(const std::wstring& pipe_name)
|
||||
{
|
||||
Logger::trace(L"Starting AdvancedPaste process");
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
const unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
std::wstring executable_args = L"";
|
||||
executable_args.append(std::to_wstring(powertoys_pid));
|
||||
|
||||
executable_args += L" " + arg;
|
||||
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_name);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
|
||||
@@ -221,6 +263,55 @@ private:
|
||||
m_hProcess = sei.hProcess;
|
||||
}
|
||||
|
||||
|
||||
std::optional<std::wstring> get_pipe_name(const std::wstring& prefix) const
|
||||
{
|
||||
UUID temp_uuid;
|
||||
wchar_t* uuid_chars = nullptr;
|
||||
if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS)
|
||||
{
|
||||
const auto val = get_last_error_message(GetLastError());
|
||||
Logger::error(L"UuidCreate cannot create guid. {}", val.has_value() ? val.value() : L"");
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) != RPC_S_OK)
|
||||
{
|
||||
const auto val = get_last_error_message(GetLastError());
|
||||
Logger::error(L"UuidToString cannot convert to string. {}", val.has_value() ? val.value() : L"");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto pipe_name = std::format(L"{}{}", prefix, std::wstring(uuid_chars));
|
||||
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
|
||||
|
||||
return pipe_name;
|
||||
}
|
||||
|
||||
void launch_process_and_named_pipe()
|
||||
{
|
||||
const auto pipe_name = get_pipe_name(L"powertoys_advanced_paste_");
|
||||
|
||||
if (!pipe_name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
create_pipe_thread = std::thread([&] { start_named_pipe_server(pipe_name.value()); });
|
||||
launch_process(pipe_name.value());
|
||||
create_pipe_thread.join();
|
||||
}
|
||||
|
||||
void send_named_pipe_message(const std::wstring& message_type, const std::wstring& message_arg = L"")
|
||||
{
|
||||
if (m_write_pipe)
|
||||
{
|
||||
const auto message = message_arg.empty() ? std::format(L"{}\r\n", message_type) : std::format(L"{} {}\r\n", message_type, message_arg);
|
||||
|
||||
const CString file_name(message.c_str());
|
||||
m_write_pipe->Write(file_name, file_name.GetLength() * sizeof(TCHAR));
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings file.
|
||||
void init_settings()
|
||||
{
|
||||
@@ -258,7 +349,7 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void try_inject_modifier_key_restore(std::vector<INPUT> &inputs, short modifier)
|
||||
void try_inject_modifier_key_restore(std::vector<INPUT>& inputs, short modifier)
|
||||
{
|
||||
// Most significant bit is set if key is down
|
||||
if ((GetAsyncKeyState(static_cast<int>(modifier)) & 0x8000) != 0)
|
||||
@@ -487,15 +578,54 @@ private:
|
||||
EnumWindows(enum_windows, (LPARAM)m_hProcess);
|
||||
}
|
||||
|
||||
HRESULT start_named_pipe_server(const std::wstring& pipe_name)
|
||||
{
|
||||
const constexpr DWORD BUFSIZE = 4096 * 4;
|
||||
|
||||
const auto full_pipe_name = std::format(L"\\\\.\\pipe\\{}", pipe_name);
|
||||
|
||||
const auto hPipe = CreateNamedPipe(
|
||||
full_pipe_name.c_str(), // pipe name
|
||||
PIPE_ACCESS_OUTBOUND, // write access
|
||||
PIPE_TYPE_MESSAGE | // message type pipe
|
||||
PIPE_READMODE_MESSAGE | // message-read mode
|
||||
PIPE_WAIT, // blocking mode
|
||||
1, // max. instances
|
||||
BUFSIZE, // output buffer size
|
||||
0, // input buffer size
|
||||
0, // client time-out
|
||||
NULL); // default security attribute
|
||||
|
||||
if (hPipe == NULL || hPipe == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
// This call blocks until a client process connects to the pipe
|
||||
BOOL connected = ConnectNamedPipe(hPipe, NULL);
|
||||
if (!connected)
|
||||
{
|
||||
if (GetLastError() == ERROR_PIPE_CONNECTED)
|
||||
{
|
||||
return S_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseHandle(hPipe);
|
||||
}
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
m_write_pipe = std::make_unique<CAtlFile>(hPipe);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
public:
|
||||
AdvancedPaste()
|
||||
{
|
||||
app_name = GET_RESOURCE_STRING(IDS_ADVANCED_PASTE_NAME);
|
||||
app_key = AdvancedPasteConstants::ModuleKey;
|
||||
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "AdvancedPaste");
|
||||
m_hShowUIEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_ADVANCED_PASTE_SHARED_EVENT);
|
||||
m_hPasteMarkdownEvent = CreateDefaultEvent(CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_EVENT);
|
||||
m_hPasteJsonEvent = CreateDefaultEvent(CommonSharedConstants::ADVANCED_PASTE_JSON_EVENT);
|
||||
init_settings();
|
||||
}
|
||||
|
||||
@@ -559,7 +689,7 @@ public:
|
||||
|
||||
parse_hotkeys(values);
|
||||
|
||||
auto settingsObject = values.get_raw_json();
|
||||
const auto settingsObject = values.get_raw_json();
|
||||
if (settingsObject.GetView().Size() && settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
|
||||
{
|
||||
m_preview_custom_format_output = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
@@ -567,10 +697,10 @@ public:
|
||||
|
||||
// order of args matter
|
||||
Trace::AdvancedPaste_SettingsTelemetry(m_paste_as_plain_hotkey,
|
||||
m_advanced_paste_ui_hotkey,
|
||||
m_paste_as_markdown_hotkey,
|
||||
m_paste_as_json_hotkey,
|
||||
m_preview_custom_format_output);
|
||||
m_advanced_paste_ui_hotkey,
|
||||
m_paste_as_markdown_hotkey,
|
||||
m_paste_as_json_hotkey,
|
||||
m_preview_custom_format_output);
|
||||
|
||||
// If you don't need to do any custom processing of the settings, proceed
|
||||
// to persists the values calling:
|
||||
@@ -588,12 +718,9 @@ public:
|
||||
{
|
||||
Logger::trace("AdvancedPaste::enable()");
|
||||
Trace::AdvancedPaste_Enable(true);
|
||||
ResetEvent(m_hShowUIEvent);
|
||||
ResetEvent(m_hPasteMarkdownEvent);
|
||||
ResetEvent(m_hPasteJsonEvent);
|
||||
m_enabled = true;
|
||||
|
||||
launch_process();
|
||||
launch_process_and_named_pipe();
|
||||
};
|
||||
|
||||
virtual void disable()
|
||||
@@ -601,9 +728,8 @@ public:
|
||||
Logger::trace("AdvancedPaste::disable()");
|
||||
if (m_enabled)
|
||||
{
|
||||
ResetEvent(m_hShowUIEvent);
|
||||
ResetEvent(m_hPasteMarkdownEvent);
|
||||
ResetEvent(m_hPasteJsonEvent);
|
||||
m_write_pipe = nullptr;
|
||||
|
||||
TerminateProcess(m_hProcess, 1);
|
||||
Trace::AdvancedPaste_Enable(false);
|
||||
|
||||
@@ -622,13 +748,14 @@ public:
|
||||
if (!is_process_running())
|
||||
{
|
||||
Logger::trace(L"Launching new process");
|
||||
launch_process();
|
||||
launch_process_and_named_pipe();
|
||||
|
||||
Trace::AdvancedPaste_Invoked(L"AdvancedPasteUI");
|
||||
}
|
||||
|
||||
// hotkeyId in same order as set by get_hotkeys
|
||||
if (hotkeyId == 0) { // m_paste_as_plain_hotkey
|
||||
if (hotkeyId == 0)
|
||||
{ // m_paste_as_plain_hotkey
|
||||
Logger::trace(L"Paste as plain text hotkey pressed");
|
||||
|
||||
std::thread([=]() {
|
||||
@@ -641,21 +768,36 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hotkeyId == 1) { // m_advanced_paste_ui_hotkey
|
||||
if (hotkeyId == 1)
|
||||
{ // m_advanced_paste_ui_hotkey
|
||||
Logger::trace(L"Setting start up event");
|
||||
|
||||
bring_process_to_front();
|
||||
SetEvent(m_hShowUIEvent);
|
||||
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE);
|
||||
return true;
|
||||
}
|
||||
if (hotkeyId == 2) { // m_paste_as_markdown_hotkey
|
||||
if (hotkeyId == 2)
|
||||
{ // m_paste_as_markdown_hotkey
|
||||
Logger::trace(L"Starting paste as markdown directly");
|
||||
SetEvent(m_hPasteMarkdownEvent);
|
||||
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_MESSAGE);
|
||||
return true;
|
||||
}
|
||||
if (hotkeyId == 3) { // m_paste_as_json_hotkey
|
||||
if (hotkeyId == 3)
|
||||
{ // m_paste_as_json_hotkey
|
||||
Logger::trace(L"Starting paste as json directly");
|
||||
SetEvent(m_hPasteJsonEvent);
|
||||
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE);
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto custom_action_index = hotkeyId - NUM_DEFAULT_HOTKEYS;
|
||||
|
||||
if (custom_action_index < m_custom_action_ids.size())
|
||||
{
|
||||
const auto id = m_custom_action_ids.at(custom_action_index);
|
||||
|
||||
Logger::trace(L"Starting custom action id={}", id);
|
||||
|
||||
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE, std::to_wstring(id));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -665,14 +807,20 @@ public:
|
||||
|
||||
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||
{
|
||||
if (hotkeys && buffer_size >= 4)
|
||||
const size_t num_hotkeys = NUM_DEFAULT_HOTKEYS + m_custom_action_hotkeys.size();
|
||||
|
||||
if (hotkeys && buffer_size >= num_hotkeys)
|
||||
{
|
||||
hotkeys[0] = m_paste_as_plain_hotkey;
|
||||
hotkeys[1] = m_advanced_paste_ui_hotkey;
|
||||
hotkeys[2] = m_paste_as_markdown_hotkey;
|
||||
hotkeys[3] = m_paste_as_json_hotkey;
|
||||
const std::array default_hotkeys = { m_paste_as_plain_hotkey,
|
||||
m_advanced_paste_ui_hotkey,
|
||||
m_paste_as_markdown_hotkey,
|
||||
m_paste_as_json_hotkey };
|
||||
|
||||
std::copy(default_hotkeys.begin(), default_hotkeys.end(), hotkeys);
|
||||
std::copy(m_custom_action_hotkeys.begin(), m_custom_action_hotkeys.end(), hotkeys + NUM_DEFAULT_HOTKEYS);
|
||||
}
|
||||
return 4;
|
||||
|
||||
return num_hotkeys;
|
||||
}
|
||||
|
||||
virtual bool is_enabled() override
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.IO;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -248,25 +249,53 @@ namespace Hosts.Tests
|
||||
{
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
var hostsFile = new MockFileData(string.Empty);
|
||||
hostsFile.Attributes = System.IO.FileAttributes.ReadOnly;
|
||||
|
||||
var hostsFile = new MockFileData(string.Empty)
|
||||
{
|
||||
Attributes = FileAttributes.ReadOnly,
|
||||
};
|
||||
|
||||
fileSystem.AddFile(service.HostsFilePath, hostsFile);
|
||||
|
||||
await Assert.ThrowsExceptionAsync<ReadOnlyHostsException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>()));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Remove_ReadOnly()
|
||||
public void Remove_ReadOnly_Attribute()
|
||||
{
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
var hostsFile = new MockFileData(string.Empty);
|
||||
hostsFile.Attributes = System.IO.FileAttributes.ReadOnly;
|
||||
|
||||
var hostsFile = new MockFileData(string.Empty)
|
||||
{
|
||||
Attributes = FileAttributes.ReadOnly,
|
||||
};
|
||||
|
||||
fileSystem.AddFile(service.HostsFilePath, hostsFile);
|
||||
|
||||
service.RemoveReadOnly();
|
||||
var readOnly = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(System.IO.FileAttributes.ReadOnly);
|
||||
service.RemoveReadOnlyAttribute();
|
||||
|
||||
var readOnly = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(FileAttributes.ReadOnly);
|
||||
Assert.IsFalse(readOnly);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Save_Hidden_Hosts()
|
||||
{
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
|
||||
var hostsFile = new MockFileData(string.Empty)
|
||||
{
|
||||
Attributes = FileAttributes.Hidden,
|
||||
};
|
||||
|
||||
fileSystem.AddFile(service.HostsFilePath, hostsFile);
|
||||
|
||||
await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>());
|
||||
|
||||
var hidden = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden);
|
||||
Assert.IsTrue(hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace HostsUILib.Helpers
|
||||
public class HostsService : IHostsService, IDisposable
|
||||
{
|
||||
private const string _backupSuffix = $"_PowerToysBackup_";
|
||||
private const int _defaultBufferSize = 4096; // From System.IO.File source code
|
||||
|
||||
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
|
||||
private readonly IFileSystem _fileSystem;
|
||||
@@ -197,7 +198,16 @@ namespace HostsUILib.Helpers
|
||||
_backupDone = true;
|
||||
}
|
||||
|
||||
await _fileSystem.File.WriteAllLinesAsync(HostsFilePath, lines, Encoding);
|
||||
// FileMode.OpenOrCreate is necessary to prevent UnauthorizedAccessException when the hosts file is hidden
|
||||
using var stream = _fileSystem.FileStream.Create(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, _defaultBufferSize, FileOptions.Asynchronous);
|
||||
using var writer = new StreamWriter(stream, Encoding);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
await writer.WriteLineAsync(line.AsMemory());
|
||||
}
|
||||
|
||||
stream.SetLength(stream.Position);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -292,7 +302,7 @@ namespace HostsUILib.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveReadOnly()
|
||||
public void RemoveReadOnlyAttribute()
|
||||
{
|
||||
var fileInfo = _fileSystem.FileInfo.FromFileName(HostsFilePath);
|
||||
if (fileInfo.IsReadOnly)
|
||||
|
||||
@@ -25,6 +25,6 @@ namespace HostsUILib.Helpers
|
||||
|
||||
void OpenHostsFile();
|
||||
|
||||
void RemoveReadOnly();
|
||||
void RemoveReadOnlyAttribute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ namespace HostsUILib.ViewModels
|
||||
[RelayCommand]
|
||||
public void OverwriteHosts()
|
||||
{
|
||||
_hostsService.RemoveReadOnly();
|
||||
_hostsService.RemoveReadOnlyAttribute();
|
||||
_ = Task.Run(SaveAsync);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,40 @@
|
||||
#include "BoundsToolOverlayUI.h"
|
||||
#include "CoordinateSystemConversion.h"
|
||||
#include "Clipboard.h"
|
||||
#include "constants.h"
|
||||
|
||||
#include <common/utils/window.h>
|
||||
|
||||
#define MOUSEEVENTF_FROMTOUCH 0xFF515700
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
Measurement GetMeasurement(const CursorDrag& currentBounds, POINT cursorPos)
|
||||
{
|
||||
D2D1_RECT_F rect;
|
||||
std::tie(rect.left, rect.right) =
|
||||
std::minmax(static_cast<float>(cursorPos.x), currentBounds.startPos.x);
|
||||
std::tie(rect.top, rect.bottom) =
|
||||
std::minmax(static_cast<float>(cursorPos.y), currentBounds.startPos.y);
|
||||
|
||||
return Measurement(rect);
|
||||
}
|
||||
|
||||
void CopyToClipboard(HWND window, const BoundsToolState& toolState, POINT cursorPos)
|
||||
{
|
||||
std::vector<Measurement> allMeasurements;
|
||||
for (const auto& [handle, perScreen] : toolState.perScreen)
|
||||
{
|
||||
allMeasurements.append_range(perScreen.measurements);
|
||||
|
||||
if (handle == window && perScreen.currentBounds)
|
||||
{
|
||||
allMeasurements.push_back(GetMeasurement(*perScreen.currentBounds, cursorPos));
|
||||
}
|
||||
}
|
||||
|
||||
SetClipboardToMeasurements(allMeasurements, true, true, toolState.commonState->units);
|
||||
}
|
||||
|
||||
void ToggleCursor(const bool show)
|
||||
{
|
||||
if (show)
|
||||
@@ -52,22 +79,16 @@ namespace
|
||||
{
|
||||
ToggleCursor(true);
|
||||
ClipCursor(nullptr);
|
||||
CopyToClipboard(window, *toolState, cursorPos);
|
||||
|
||||
toolState->commonState->overlayBoxText.Read([](const OverlayBoxText& text) {
|
||||
SetClipBoardToText(text.buffer.data());
|
||||
});
|
||||
auto& perScreen = toolState->perScreen[window];
|
||||
|
||||
if (const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000; shiftPress && toolState->perScreen[window].currentBounds)
|
||||
if (const bool shiftPress = GetKeyState(VK_SHIFT) & 0x80000; shiftPress && perScreen.currentBounds)
|
||||
{
|
||||
D2D1_RECT_F rect;
|
||||
std::tie(rect.left, rect.right) =
|
||||
std::minmax(static_cast<float>(cursorPos.x), toolState->perScreen[window].currentBounds->startPos.x);
|
||||
std::tie(rect.top, rect.bottom) =
|
||||
std::minmax(static_cast<float>(cursorPos.y), toolState->perScreen[window].currentBounds->startPos.y);
|
||||
toolState->perScreen[window].measurements.push_back(Measurement{ rect });
|
||||
perScreen.measurements.push_back(GetMeasurement(*perScreen.currentBounds, cursorPos));
|
||||
}
|
||||
|
||||
toolState->perScreen[window].currentBounds = std::nullopt;
|
||||
perScreen.currentBounds = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,12 +107,17 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
|
||||
case WM_KEYUP:
|
||||
if (wparam == VK_ESCAPE)
|
||||
{
|
||||
if (const auto* toolState = GetWindowParam<BoundsToolState*>(window))
|
||||
{
|
||||
CopyToClipboard(window, *toolState, convert::FromSystemToWindow(window, toolState->commonState->cursorPosSystemSpace));
|
||||
}
|
||||
|
||||
PostMessageW(window, WM_CLOSE, {}, {});
|
||||
}
|
||||
break;
|
||||
case WM_LBUTTONDOWN:
|
||||
{
|
||||
const bool touchEvent = (GetMessageExtraInfo() & MOUSEEVENTF_FROMTOUCH) == MOUSEEVENTF_FROMTOUCH;
|
||||
const bool touchEvent = (GetMessageExtraInfo() & consts::MOUSEEVENTF_FROMTOUCH) == consts::MOUSEEVENTF_FROMTOUCH;
|
||||
if (touchEvent)
|
||||
break;
|
||||
|
||||
@@ -164,7 +190,7 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
|
||||
|
||||
case WM_MOUSEMOVE:
|
||||
{
|
||||
const bool touchEvent = (GetMessageExtraInfo() & MOUSEEVENTF_FROMTOUCH) == MOUSEEVENTF_FROMTOUCH;
|
||||
const bool touchEvent = (GetMessageExtraInfo() & consts::MOUSEEVENTF_FROMTOUCH) == consts::MOUSEEVENTF_FROMTOUCH;
|
||||
if (touchEvent)
|
||||
break;
|
||||
|
||||
@@ -180,7 +206,7 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
|
||||
|
||||
case WM_LBUTTONUP:
|
||||
{
|
||||
const bool touchEvent = (GetMessageExtraInfo() & MOUSEEVENTF_FROMTOUCH) == MOUSEEVENTF_FROMTOUCH;
|
||||
const bool touchEvent = (GetMessageExtraInfo() & consts::MOUSEEVENTF_FROMTOUCH) == consts::MOUSEEVENTF_FROMTOUCH;
|
||||
if (touchEvent)
|
||||
break;
|
||||
|
||||
@@ -195,24 +221,32 @@ LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPA
|
||||
}
|
||||
case WM_RBUTTONUP:
|
||||
{
|
||||
const bool touchEvent = (GetMessageExtraInfo() & MOUSEEVENTF_FROMTOUCH) == MOUSEEVENTF_FROMTOUCH;
|
||||
const bool touchEvent = (GetMessageExtraInfo() & consts::MOUSEEVENTF_FROMTOUCH) == consts::MOUSEEVENTF_FROMTOUCH;
|
||||
if (touchEvent)
|
||||
break;
|
||||
|
||||
ToggleCursor(true);
|
||||
|
||||
auto toolState = GetWindowParam<BoundsToolState*>(window);
|
||||
auto* toolState = GetWindowParam<BoundsToolState*>(window);
|
||||
if (!toolState)
|
||||
break;
|
||||
|
||||
if (toolState->perScreen[window].currentBounds)
|
||||
toolState->perScreen[window].currentBounds = std::nullopt;
|
||||
auto& perScreen = toolState->perScreen[window];
|
||||
|
||||
if (perScreen.currentBounds)
|
||||
{
|
||||
perScreen.currentBounds = std::nullopt;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (toolState->perScreen[window].measurements.empty())
|
||||
if (perScreen.measurements.empty())
|
||||
{
|
||||
PostMessageW(window, WM_CLOSE, {}, {});
|
||||
}
|
||||
else
|
||||
toolState->perScreen[window].measurements.clear();
|
||||
{
|
||||
perScreen.measurements.clear();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -242,10 +276,6 @@ namespace
|
||||
true,
|
||||
commonState.units);
|
||||
|
||||
commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
|
||||
v = text;
|
||||
});
|
||||
|
||||
D2D_POINT_2F textBoxPos;
|
||||
if (textBoxCenter)
|
||||
textBoxPos = *textBoxCenter;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "Clipboard.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
void SetClipBoardToText(const std::wstring_view text)
|
||||
{
|
||||
if (!OpenClipboard(nullptr))
|
||||
@@ -26,3 +28,25 @@ void SetClipBoardToText(const std::wstring_view text)
|
||||
SetClipboardData(CF_UNICODETEXT, handle.get());
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
void SetClipboardToMeasurements(const std::vector<Measurement>& measurements,
|
||||
bool printWidth,
|
||||
bool printHeight,
|
||||
Measurement::Unit units)
|
||||
{
|
||||
if (measurements.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::wostringstream stream;
|
||||
bool isFirst = true;
|
||||
|
||||
for (const auto& measurement : measurements)
|
||||
{
|
||||
measurement.PrintToStream(stream, !isFirst, printWidth, printHeight, units);
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
SetClipBoardToText(stream.str());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include "Measurement.h"
|
||||
|
||||
void SetClipBoardToText(const std::wstring_view text);
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
void SetClipBoardToText(const std::wstring_view text);
|
||||
|
||||
void SetClipboardToMeasurements(const std::vector<Measurement>& measurements,
|
||||
bool printWidth,
|
||||
bool printHeight,
|
||||
Measurement::Unit units);
|
||||
@@ -8,8 +8,51 @@
|
||||
|
||||
#include <common/utils/window.h>
|
||||
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::pair<bool, bool> GetHorizontalVerticalLines(MeasureToolState::Mode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case MeasureToolState::Mode::Cross:
|
||||
return { true, true };
|
||||
|
||||
case MeasureToolState::Mode::Vertical:
|
||||
return { false, true };
|
||||
|
||||
case MeasureToolState::Mode::Horizontal:
|
||||
return { true, false };
|
||||
|
||||
default:
|
||||
throw std::runtime_error("Unknown MeasureToolState Mode");
|
||||
}
|
||||
}
|
||||
|
||||
void CopyToClipboard(HWND window, const MeasureToolState& toolState)
|
||||
{
|
||||
std::vector<Measurement> allMeasurements;
|
||||
for (const auto& [handle, perScreen] : toolState.perScreen)
|
||||
{
|
||||
for (const auto& [_, measurement] : perScreen.prevMeasurements)
|
||||
{
|
||||
allMeasurements.push_back(measurement);
|
||||
}
|
||||
|
||||
if (handle == window && perScreen.measuredEdges)
|
||||
{
|
||||
allMeasurements.push_back(*perScreen.measuredEdges);
|
||||
}
|
||||
}
|
||||
|
||||
const auto [printWidth, printHeight] = GetHorizontalVerticalLines(toolState.global.mode);
|
||||
SetClipboardToMeasurements(allMeasurements, printWidth, printHeight, toolState.commonState->units);
|
||||
}
|
||||
|
||||
inline std::pair<D2D_POINT_2F, D2D_POINT_2F> ComputeCrossFeetLine(D2D_POINT_2F center, const bool horizontal)
|
||||
{
|
||||
D2D_POINT_2F start = center, end = center;
|
||||
@@ -27,6 +70,92 @@ namespace
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
bool HandleCursorUp(HWND window, MeasureToolState* toolState, const POINT cursorPos)
|
||||
{
|
||||
ClipCursor(nullptr);
|
||||
CopyToClipboard(window, *toolState);
|
||||
|
||||
auto& perScreen = toolState->perScreen[window];
|
||||
|
||||
const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000;
|
||||
if (shiftPress && perScreen.measuredEdges)
|
||||
{
|
||||
perScreen.prevMeasurements.push_back(MeasureToolState::PerScreen::PrevMeasurement(cursorPos, perScreen.measuredEdges.value()));
|
||||
}
|
||||
|
||||
perScreen.measuredEdges = std::nullopt;
|
||||
|
||||
return !shiftPress;
|
||||
}
|
||||
|
||||
void DrawMeasurement(const Measurement& measurement,
|
||||
D2DState& d2dState,
|
||||
bool drawFeetOnCross,
|
||||
MeasureToolState::Mode mode,
|
||||
POINT cursorPos,
|
||||
const CommonState& commonState,
|
||||
HWND window)
|
||||
{
|
||||
const auto [drawHorizontalCrossLine, drawVerticalCrossLine] = GetHorizontalVerticalLines(mode);
|
||||
|
||||
const float hMeasure = measurement.Width(Measurement::Unit::Pixel);
|
||||
const float vMeasure = measurement.Height(Measurement::Unit::Pixel);
|
||||
|
||||
d2dState.ToggleAliasedLinesMode(true);
|
||||
if (drawHorizontalCrossLine)
|
||||
{
|
||||
const D2D_POINT_2F hLineStart{ .x = measurement.rect.left, .y = static_cast<float>(cursorPos.y) };
|
||||
D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y };
|
||||
d2dState.dxgiWindowState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
|
||||
|
||||
if (drawFeetOnCross)
|
||||
{
|
||||
// To fill all pixels which are close, we call DrawLine with end point one pixel too far, since
|
||||
// it doesn't get filled, i.e. end point of the range is excluded. However, we want to draw cross
|
||||
// feet *on* the last pixel row, so we must subtract 1px from the corresponding axis.
|
||||
hLineEnd.x -= 1.f;
|
||||
const auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false);
|
||||
const auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false);
|
||||
d2dState.dxgiWindowState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
|
||||
d2dState.dxgiWindowState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
|
||||
}
|
||||
}
|
||||
|
||||
if (drawVerticalCrossLine)
|
||||
{
|
||||
const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = measurement.rect.top };
|
||||
D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure };
|
||||
d2dState.dxgiWindowState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
|
||||
|
||||
if (drawFeetOnCross)
|
||||
{
|
||||
vLineEnd.y -= 1.f;
|
||||
const auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true);
|
||||
const auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true);
|
||||
d2dState.dxgiWindowState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
|
||||
d2dState.dxgiWindowState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
|
||||
}
|
||||
}
|
||||
|
||||
d2dState.ToggleAliasedLinesMode(false);
|
||||
|
||||
OverlayBoxText text;
|
||||
|
||||
const auto [crossSymbolPos, measureStringBufLen] =
|
||||
measurement.Print(text.buffer.data(),
|
||||
text.buffer.size(),
|
||||
drawHorizontalCrossLine,
|
||||
drawVerticalCrossLine,
|
||||
commonState.units);
|
||||
|
||||
d2dState.DrawTextBox(text.buffer.data(),
|
||||
measureStringBufLen,
|
||||
crossSymbolPos,
|
||||
D2D_POINT_2F{ static_cast<float>(cursorPos.x), static_cast<float>(cursorPos.y) },
|
||||
true,
|
||||
window);
|
||||
}
|
||||
}
|
||||
|
||||
winrt::com_ptr<ID2D1Bitmap> ConvertID3D11Texture2DToD2D1Bitmap(winrt::com_ptr<ID2D1RenderTarget> rt,
|
||||
@@ -85,17 +214,29 @@ LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LP
|
||||
}
|
||||
break;
|
||||
case WM_RBUTTONUP:
|
||||
{
|
||||
PostMessageW(window, WM_CLOSE, {}, {});
|
||||
break;
|
||||
}
|
||||
case WM_LBUTTONUP:
|
||||
{
|
||||
bool shouldClose = true;
|
||||
|
||||
if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
|
||||
{
|
||||
state->Read([](const MeasureToolState& s) { s.commonState->overlayBoxText.Read([](const OverlayBoxText& text) {
|
||||
SetClipBoardToText(text.buffer.data());
|
||||
}); });
|
||||
state->Access([&](MeasureToolState& s) {
|
||||
shouldClose = HandleCursorUp(window,
|
||||
&s,
|
||||
convert::FromSystemToWindow(window, s.commonState->cursorPosSystemSpace));
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldClose)
|
||||
{
|
||||
PostMessageW(window, WM_CLOSE, {}, {});
|
||||
}
|
||||
PostMessageW(window, WM_CLOSE, {}, {});
|
||||
break;
|
||||
}
|
||||
case WM_MOUSEWHEEL:
|
||||
if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
|
||||
{
|
||||
@@ -119,29 +260,29 @@ void DrawMeasureToolTick(const CommonState& commonState,
|
||||
{
|
||||
bool continuousCapture = {};
|
||||
bool drawFeetOnCross = {};
|
||||
bool drawHorizontalCrossLine = true;
|
||||
bool drawVerticalCrossLine = true;
|
||||
|
||||
Measurement measuredEdges{};
|
||||
std::optional<Measurement> measuredEdges{};
|
||||
MeasureToolState::Mode mode = {};
|
||||
winrt::com_ptr<ID2D1Bitmap> backgroundBitmap;
|
||||
const MappedTextureView* backgroundTextureToConvert = nullptr;
|
||||
|
||||
bool gotMeasurement = false;
|
||||
std::vector<MeasureToolState::PerScreen::PrevMeasurement> prevMeasurements;
|
||||
toolState.Read([&](const MeasureToolState& state) {
|
||||
continuousCapture = state.global.continuousCapture;
|
||||
drawFeetOnCross = state.global.drawFeetOnCross;
|
||||
mode = state.global.mode;
|
||||
if (auto it = state.perScreen.find(window); it != end(state.perScreen))
|
||||
|
||||
if (const auto it = state.perScreen.find(window); it != end(state.perScreen))
|
||||
{
|
||||
const auto& perScreen = it->second;
|
||||
|
||||
prevMeasurements = perScreen.prevMeasurements;
|
||||
|
||||
if (!perScreen.measuredEdges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
gotMeasurement = true;
|
||||
measuredEdges = *perScreen.measuredEdges;
|
||||
measuredEdges = perScreen.measuredEdges;
|
||||
|
||||
if (continuousCapture)
|
||||
return;
|
||||
@@ -157,23 +298,9 @@ void DrawMeasureToolTick(const CommonState& commonState,
|
||||
}
|
||||
});
|
||||
|
||||
if (!gotMeasurement)
|
||||
return;
|
||||
|
||||
switch (mode)
|
||||
if (!measuredEdges && prevMeasurements.empty())
|
||||
{
|
||||
case MeasureToolState::Mode::Cross:
|
||||
drawHorizontalCrossLine = true;
|
||||
drawVerticalCrossLine = true;
|
||||
break;
|
||||
case MeasureToolState::Mode::Vertical:
|
||||
drawHorizontalCrossLine = false;
|
||||
drawVerticalCrossLine = true;
|
||||
break;
|
||||
case MeasureToolState::Mode::Horizontal:
|
||||
drawHorizontalCrossLine = true;
|
||||
drawVerticalCrossLine = false;
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert)
|
||||
@@ -189,73 +316,23 @@ void DrawMeasureToolTick(const CommonState& commonState,
|
||||
}
|
||||
|
||||
if (continuousCapture || !backgroundBitmap)
|
||||
{
|
||||
d2dState.dxgiWindowState.rt->Clear();
|
||||
|
||||
const float hMeasure = measuredEdges.Width(Measurement::Unit::Pixel);
|
||||
const float vMeasure = measuredEdges.Height(Measurement::Unit::Pixel);
|
||||
}
|
||||
|
||||
if (!continuousCapture && backgroundBitmap)
|
||||
{
|
||||
d2dState.dxgiWindowState.rt->DrawBitmap(backgroundBitmap.get());
|
||||
}
|
||||
|
||||
const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
|
||||
|
||||
d2dState.ToggleAliasedLinesMode(true);
|
||||
if (drawHorizontalCrossLine)
|
||||
for (const auto& [prevCursorPos, prevMeasurement] : prevMeasurements)
|
||||
{
|
||||
const D2D_POINT_2F hLineStart{ .x = measuredEdges.rect.left, .y = static_cast<float>(cursorPos.y) };
|
||||
D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y };
|
||||
d2dState.dxgiWindowState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
|
||||
|
||||
if (drawFeetOnCross)
|
||||
{
|
||||
// To fill all pixels which are close, we call DrawLine with end point one pixel too far, since
|
||||
// it doesn't get filled, i.e. end point of the range is excluded. However, we want to draw cross
|
||||
// feet *on* the last pixel row, so we must subtract 1px from the corresponding axis.
|
||||
hLineEnd.x -= 1.f;
|
||||
auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false);
|
||||
auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false);
|
||||
d2dState.dxgiWindowState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
|
||||
d2dState.dxgiWindowState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
|
||||
}
|
||||
DrawMeasurement(prevMeasurement, d2dState, drawFeetOnCross, mode, prevCursorPos, commonState, window);
|
||||
}
|
||||
|
||||
if (drawVerticalCrossLine)
|
||||
if (measuredEdges)
|
||||
{
|
||||
const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = measuredEdges.rect.top };
|
||||
D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure };
|
||||
d2dState.dxgiWindowState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
|
||||
|
||||
if (drawFeetOnCross)
|
||||
{
|
||||
vLineEnd.y -= 1.f;
|
||||
auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true);
|
||||
auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true);
|
||||
d2dState.dxgiWindowState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
|
||||
d2dState.dxgiWindowState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
|
||||
}
|
||||
const auto cursorPos = convert::FromSystemToWindow(window, commonState.cursorPosSystemSpace);
|
||||
DrawMeasurement(*measuredEdges, d2dState, drawFeetOnCross, mode, cursorPos, commonState, window);
|
||||
}
|
||||
|
||||
d2dState.ToggleAliasedLinesMode(false);
|
||||
|
||||
OverlayBoxText text;
|
||||
|
||||
const auto [crossSymbolPos, measureStringBufLen] =
|
||||
measuredEdges.Print(text.buffer.data(),
|
||||
text.buffer.size(),
|
||||
drawHorizontalCrossLine,
|
||||
drawVerticalCrossLine,
|
||||
commonState.units);
|
||||
|
||||
commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
|
||||
v = text;
|
||||
});
|
||||
|
||||
d2dState.DrawTextBox(text.buffer.data(),
|
||||
measureStringBufLen,
|
||||
crossSymbolPos,
|
||||
D2D_POINT_2F{ static_cast<float>(cursorPos.x), static_cast<float>(cursorPos.y) },
|
||||
true,
|
||||
window);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "Measurement.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
Measurement::Measurement(RECT winRect)
|
||||
{
|
||||
rect.left = static_cast<float>(winRect.left);
|
||||
@@ -89,3 +91,41 @@ Measurement::PrintResult Measurement::Print(wchar_t* buf,
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Measurement::PrintToStream(std::wostream& stream,
|
||||
const bool prependNewLine,
|
||||
const bool printWidth,
|
||||
const bool printHeight,
|
||||
const Unit units) const
|
||||
{
|
||||
if (prependNewLine)
|
||||
{
|
||||
stream << std::endl;
|
||||
}
|
||||
|
||||
if (printWidth)
|
||||
{
|
||||
stream << Width(units);
|
||||
if (printHeight)
|
||||
{
|
||||
stream << L" \x00D7 ";
|
||||
}
|
||||
}
|
||||
|
||||
if (printHeight)
|
||||
{
|
||||
stream << Height(units);
|
||||
}
|
||||
|
||||
switch (units)
|
||||
{
|
||||
case Measurement::Unit::Inch:
|
||||
stream << L" in";
|
||||
|
||||
break;
|
||||
case Measurement::Unit::Centimetre:
|
||||
stream << L" cm";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <dcommon.h>
|
||||
#include <windef.h>
|
||||
#include <iosfwd>
|
||||
|
||||
struct Measurement
|
||||
{
|
||||
@@ -35,4 +36,10 @@ struct Measurement
|
||||
const bool printWidth,
|
||||
const bool printHeight,
|
||||
const Unit units) const;
|
||||
|
||||
void PrintToStream(std::wostream& stream,
|
||||
const bool prependNewLine,
|
||||
const bool printWidth,
|
||||
const bool printHeight,
|
||||
const Unit units) const;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,6 @@ struct CommonState
|
||||
|
||||
Measurement::Unit units = Measurement::Unit::Pixel;
|
||||
|
||||
mutable Serialized<OverlayBoxText> overlayBoxText;
|
||||
POINT cursorPosSystemSpace = {}; // updated atomically
|
||||
std::atomic_bool closeOnOtherMonitors = false;
|
||||
};
|
||||
@@ -77,9 +76,13 @@ struct MeasureToolState
|
||||
|
||||
struct PerScreen
|
||||
{
|
||||
using PrevMeasurement = std::pair<POINT, Measurement>;
|
||||
|
||||
bool cursorInLeftScreenHalf = false;
|
||||
bool cursorInTopScreenHalf = false;
|
||||
std::optional<Measurement> measuredEdges;
|
||||
std::vector<PrevMeasurement> prevMeasurements;
|
||||
|
||||
// While not in a continuous capturing mode, we need to draw captured backgrounds. These are passed
|
||||
// directly from a capturing thread.
|
||||
const MappedTextureView* capturedScreenTexture = nullptr;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <windef.h>
|
||||
#include <chrono>
|
||||
|
||||
namespace consts
|
||||
@@ -18,4 +19,6 @@ namespace consts
|
||||
/* Offset to not try not to use the cursor immediate pixels in measuring, but it seems only necessary for continuous mode. */
|
||||
constexpr inline long CURSOR_OFFSET_AMOUNT_X = 4;
|
||||
constexpr inline long CURSOR_OFFSET_AMOUNT_Y = 4;
|
||||
|
||||
constexpr inline LPARAM MOUSEEVENTF_FROMTOUCH = 0xFF515700;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,20 @@ namespace WorkspacesEditor
|
||||
return;
|
||||
}
|
||||
|
||||
var args = e?.Args;
|
||||
int powerToysRunnerPid;
|
||||
if (args?.Length > 0)
|
||||
{
|
||||
_ = int.TryParse(args[0], out powerToysRunnerPid);
|
||||
|
||||
Logger.LogInfo($"WorkspacesEditor started from the PowerToys Runner. Runner pid={powerToysRunnerPid}");
|
||||
RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
|
||||
{
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting WorkspacesEditor");
|
||||
Dispatcher.Invoke(Shutdown);
|
||||
});
|
||||
}
|
||||
|
||||
ThemeManager = new ThemeManager(this);
|
||||
|
||||
if (_mainViewModel == null)
|
||||
@@ -89,6 +103,7 @@ namespace WorkspacesEditor
|
||||
}
|
||||
|
||||
Dispose();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
|
||||
|
||||
@@ -16,14 +16,6 @@ namespace WorkspacesEditor.Data
|
||||
}
|
||||
}
|
||||
|
||||
public static string LaunchFile
|
||||
{
|
||||
get
|
||||
{
|
||||
return FolderUtils.DataFolder() + "\\tempLaunch-workspaces.json";
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeleteTempFile()
|
||||
{
|
||||
if (System.IO.File.Exists(File))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Page
|
||||
<Page
|
||||
x:Class="WorkspacesEditor.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
@@ -198,7 +198,7 @@
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="110" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel
|
||||
Margin="12,14,10,10"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using ManagedCommon;
|
||||
@@ -14,10 +15,12 @@ namespace WorkspacesEditor
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
public partial class MainWindow : Window, IDisposable
|
||||
{
|
||||
public MainViewModel MainViewModel { get; set; }
|
||||
|
||||
private CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
|
||||
private static MainPage _mainPage;
|
||||
|
||||
public MainWindow(MainViewModel mainViewModel)
|
||||
@@ -41,10 +44,36 @@ namespace WorkspacesEditor
|
||||
|
||||
MaxWidth = SystemParameters.PrimaryScreenWidth;
|
||||
MaxHeight = SystemParameters.PrimaryScreenHeight;
|
||||
|
||||
Common.UI.NativeEventWaiter.WaitForEventLoop(
|
||||
PowerToys.Interop.Constants.WorkspacesHotkeyEvent(),
|
||||
() =>
|
||||
{
|
||||
if (ApplicationIsInFocus())
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (WindowState == WindowState.Minimized)
|
||||
{
|
||||
WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
// Get the window handle of the Workspaces Editor window
|
||||
IntPtr handle = new WindowInteropHelper(this).Handle;
|
||||
WindowHelpers.BringToForeground(handle);
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
},
|
||||
Application.Current.Dispatcher,
|
||||
cancellationToken.Token);
|
||||
}
|
||||
|
||||
private void OnClosing(object sender, EventArgs e)
|
||||
{
|
||||
cancellationToken.Dispose();
|
||||
App.Current.Shutdown();
|
||||
}
|
||||
|
||||
@@ -67,5 +96,25 @@ namespace WorkspacesEditor
|
||||
{
|
||||
ContentFrame.GoBack();
|
||||
}
|
||||
|
||||
public static bool ApplicationIsInFocus()
|
||||
{
|
||||
var activatedHandle = NativeMethods.GetForegroundWindow();
|
||||
if (activatedHandle == IntPtr.Zero)
|
||||
{
|
||||
return false; // No window is currently activated
|
||||
}
|
||||
|
||||
var procId = Environment.ProcessId;
|
||||
int activeProcId;
|
||||
_ = NativeMethods.GetWindowThreadProcessId(activatedHandle, out activeProcId);
|
||||
|
||||
return activeProcId == procId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,9 +207,9 @@ namespace WorkspacesEditor.Models
|
||||
private BitmapImage _previewImage;
|
||||
private double _previewImageWidth;
|
||||
|
||||
public Project(Project selectedProject, string newId = "")
|
||||
public Project(Project selectedProject)
|
||||
{
|
||||
Id = newId == string.Empty ? selectedProject.Id : newId;
|
||||
Id = selectedProject.Id;
|
||||
Name = selectedProject.Name;
|
||||
PreviewIcons = selectedProject.PreviewIcons;
|
||||
PreviewImage = selectedProject.PreviewImage;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
Title="{x:Static props:Resources.SnapshotWindowTitle}"
|
||||
Width="350"
|
||||
Height="140"
|
||||
ui:TitleBar.Background="{DynamicResource PrimaryBackgroundBrush}"
|
||||
ui:TitleBar.InactiveBackground="{DynamicResource TertiaryBackgroundBrush}"
|
||||
ui:WindowHelper.UseModernWindowStyle="True"
|
||||
@@ -18,6 +17,7 @@
|
||||
BorderThickness="5"
|
||||
Closing="Window_Closing"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="5" Background="Transparent">
|
||||
@@ -31,9 +31,10 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="5"
|
||||
Margin="5,5,5,15"
|
||||
Foreground="{DynamicResource PrimaryForegroundBrush}"
|
||||
Text="{x:Static props:Resources.SnapshotDescription}" />
|
||||
Text="{x:Static props:Resources.SnapshotDescription}"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
Grid.Row="1"
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace WorkspacesEditor.Utils
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId);
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern uint GetCurrentThreadId();
|
||||
|
||||
@@ -15,13 +15,6 @@ namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public class WorkspacesEditorIO
|
||||
{
|
||||
public enum StorageFile
|
||||
{
|
||||
Common,
|
||||
Temporal,
|
||||
TemporalLaunch,
|
||||
}
|
||||
|
||||
public WorkspacesEditorIO()
|
||||
{
|
||||
}
|
||||
@@ -79,7 +72,7 @@ namespace WorkspacesEditor.Utils
|
||||
}
|
||||
}
|
||||
|
||||
public void SerializeWorkspaces(List<Project> workspaces, StorageFile storageFile = StorageFile.Common)
|
||||
public void SerializeWorkspaces(List<Project> workspaces, bool useTempFile = false)
|
||||
{
|
||||
WorkspacesData serializer = new WorkspacesData();
|
||||
WorkspacesData.WorkspacesListWrapper workspacesWrapper = new WorkspacesData.WorkspacesListWrapper { };
|
||||
@@ -155,14 +148,7 @@ namespace WorkspacesEditor.Utils
|
||||
try
|
||||
{
|
||||
IOUtils ioUtils = new IOUtils();
|
||||
string fileName = storageFile switch
|
||||
{
|
||||
StorageFile.Temporal => TempProjectData.File,
|
||||
StorageFile.TemporalLaunch => TempProjectData.LaunchFile,
|
||||
_ => serializer.File,
|
||||
};
|
||||
|
||||
ioUtils.WriteFile(fileName, serializer.Serialize(workspacesWrapper));
|
||||
ioUtils.WriteFile(useTempFile ? TempProjectData.File : serializer.File, serializer.Serialize(workspacesWrapper));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -190,22 +176,7 @@ namespace WorkspacesEditor.Utils
|
||||
|
||||
internal void SerializeTempProject(Project project)
|
||||
{
|
||||
SerializeWorkspaces(new List<Project>() { project }, StorageFile.Temporal);
|
||||
}
|
||||
|
||||
internal void RemoveFile(StorageFile storageFile)
|
||||
{
|
||||
string fileName = storageFile switch
|
||||
{
|
||||
StorageFile.Temporal => TempProjectData.File,
|
||||
StorageFile.TemporalLaunch => TempProjectData.LaunchFile,
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (File.Exists(fileName))
|
||||
{
|
||||
File.Delete(fileName);
|
||||
}
|
||||
SerializeWorkspaces(new List<Project>() { project }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,13 +509,7 @@ namespace WorkspacesEditor.ViewModels
|
||||
|
||||
internal async void LaunchAndEdit(Project project)
|
||||
{
|
||||
// the project might contain removed apps, creating a temporal copy of it (without removed apps) and launching the copy.
|
||||
Project launchProject = new Project(project, Guid.NewGuid().ToString());
|
||||
_workspacesEditorIO.SerializeWorkspaces(new List<Project>() { launchProject }, WorkspacesEditorIO.StorageFile.TemporalLaunch);
|
||||
|
||||
await Task.Run(() => RunLauncher(launchProject.Id, InvokePoint.LaunchAndEdit));
|
||||
|
||||
_workspacesEditorIO.RemoveFile(WorkspacesEditorIO.StorageFile.TemporalLaunch);
|
||||
await Task.Run(() => RunLauncher(project.Id, InvokePoint.LaunchAndEdit));
|
||||
projectBeforeLaunch = new Project(project);
|
||||
EnterSnapshotMode(true);
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
</StackPanel>
|
||||
<controls:ResetIsEnabled Grid.Column="4">
|
||||
<Button
|
||||
Width="120"
|
||||
Width="Auto"
|
||||
Margin="10,5"
|
||||
Padding="24,6"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Delete}"
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">System</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 456 KiB |
@@ -240,6 +240,8 @@ namespace
|
||||
|
||||
bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, ErrorList& launchErrors)
|
||||
{
|
||||
std::wstring dir = std::filesystem::path(appPath).parent_path();
|
||||
|
||||
SHELLEXECUTEINFO sei = { 0 };
|
||||
sei.cbSize = sizeof(SHELLEXECUTEINFO);
|
||||
sei.hwnd = nullptr;
|
||||
@@ -247,7 +249,7 @@ bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs,
|
||||
sei.lpVerb = elevated ? L"runas" : L"open";
|
||||
sei.lpFile = appPath.c_str();
|
||||
sei.lpParameters = commandLineArgs.c_str();
|
||||
sei.lpDirectory = nullptr;
|
||||
sei.lpDirectory = dir.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
|
||||
if (!ShellExecuteEx(&sei))
|
||||
|
||||
@@ -24,7 +24,7 @@ const std::wstring internalPath = L"";
|
||||
int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cmdShow)
|
||||
{
|
||||
LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesLauncherLoggerName);
|
||||
InitUnhandledExceptionHandler();
|
||||
InitUnhandledExceptionHandler();
|
||||
|
||||
if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
|
||||
{
|
||||
@@ -41,7 +41,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
GetModuleFileNameW(nullptr, exe_path.get(), exe_path_size);
|
||||
|
||||
const auto modulePath = get_module_folderpath();
|
||||
|
||||
|
||||
std::string cmdLineStr(cmdline);
|
||||
std::wstring cmdLineWStr(cmdLineStr.begin(), cmdLineStr.end());
|
||||
|
||||
@@ -57,7 +57,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
}
|
||||
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
|
||||
|
||||
std::string cmdLineStr(cmdline);
|
||||
auto cmdArgs = split(cmdLineStr, " ");
|
||||
if (cmdArgs.size() < 1)
|
||||
@@ -66,7 +66,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
MessageBox(NULL, GET_RESOURCE_STRING(IDS_INCORRECT_ARGS).c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
std::wstring id(cmdArgs[0].begin(), cmdArgs[0].end());
|
||||
if (id.empty())
|
||||
{
|
||||
@@ -94,103 +94,45 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
WorkspacesData::WorkspacesProject projectToLaunch{};
|
||||
if (invokePoint == InvokePoint::LaunchAndEdit)
|
||||
{
|
||||
// check the temp launch file in case the project is launched from the editor
|
||||
if (std::filesystem::exists(WorkspacesData::TempLaunchWorkspacesFile()))
|
||||
// check the temp file in case the project is just created and not saved to the workspaces.json yet
|
||||
if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile()))
|
||||
{
|
||||
try
|
||||
{
|
||||
auto savedWorkspacesJson = json::from_file(WorkspacesData::TempLaunchWorkspacesFile());
|
||||
auto savedWorkspacesJson = json::from_file(WorkspacesData::TempWorkspacesFile());
|
||||
if (savedWorkspacesJson.has_value())
|
||||
{
|
||||
auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value());
|
||||
auto savedWorkspaces = WorkspacesData::WorkspacesProjectJSON::FromJson(savedWorkspacesJson.value());
|
||||
if (savedWorkspaces.has_value())
|
||||
{
|
||||
workspaces = savedWorkspaces.value();
|
||||
projectToLaunch = savedWorkspaces.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::critical("Incorrect temporal launch Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempLaunchWorkspacesFile());
|
||||
Logger::critical("Incorrect Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::critical("Incorrect temporal launch Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempLaunchWorkspacesFile());
|
||||
Logger::critical("Incorrect Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
catch (std::exception ex)
|
||||
{
|
||||
Logger::critical("Exception on reading temporal launch Workspaces file: {}", ex.what());
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::TempLaunchWorkspacesFile());
|
||||
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (workspaces.empty())
|
||||
{
|
||||
Logger::warn("Temp Launch Workspaces file is empty");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), WorkspacesData::TempLaunchWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (const auto& proj : workspaces)
|
||||
{
|
||||
if (proj.id == id)
|
||||
{
|
||||
projectToLaunch = proj;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (projectToLaunch.id.empty())
|
||||
{
|
||||
// check the temp file in case the project is just created and not saved to the workspaces.json yet
|
||||
if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile()))
|
||||
{
|
||||
try
|
||||
{
|
||||
auto savedWorkspacesJson = json::from_file(WorkspacesData::TempWorkspacesFile());
|
||||
if (savedWorkspacesJson.has_value())
|
||||
{
|
||||
auto savedWorkspaces = WorkspacesData::WorkspacesProjectJSON::FromJson(savedWorkspacesJson.value());
|
||||
if (savedWorkspaces.has_value())
|
||||
{
|
||||
projectToLaunch = savedWorkspaces.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::critical("Incorrect temporal Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::critical("Incorrect temporal Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
catch (std::exception ex)
|
||||
{
|
||||
Logger::critical("Exception on reading temporal Workspaces file: {}", ex.what());
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (projectToLaunch.id.empty())
|
||||
{
|
||||
try
|
||||
@@ -259,7 +201,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
std::vector<std::pair<std::wstring, std::wstring>> launchErrors{};
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
bool launchedSuccessfully = Launch(projectToLaunch, monitors, launchErrors);
|
||||
|
||||
|
||||
// update last-launched time
|
||||
if (invokePoint != InvokePoint::LaunchAndEdit)
|
||||
{
|
||||
|
||||
@@ -3,22 +3,17 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using ManagedCommon;
|
||||
using Microsoft.VisualBasic.Devices;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Management.Deployment;
|
||||
|
||||
namespace WorkspacesLauncherUI.Models
|
||||
@@ -34,6 +29,8 @@ namespace WorkspacesLauncherUI.Models
|
||||
|
||||
public string AppPath { get; set; }
|
||||
|
||||
public bool Loading => LaunchState == "waiting";
|
||||
|
||||
private Icon _icon;
|
||||
|
||||
public Icon Icon
|
||||
@@ -72,14 +69,23 @@ namespace WorkspacesLauncherUI.Models
|
||||
|
||||
public string LaunchState { get; set; }
|
||||
|
||||
public string StateImageSource
|
||||
public string StateGlyph
|
||||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
"waiting" => "../images/spinner.gif",
|
||||
"launched" => "../images/checkmark.png",
|
||||
"failed" => "../images/failed.png",
|
||||
_ => "../images/spinner.gif",
|
||||
"launched" => "\U0000F78C",
|
||||
"failed" => "\U0000EF2C",
|
||||
_ => "\U0000EF2C",
|
||||
};
|
||||
}
|
||||
|
||||
public System.Windows.Media.Brush StateColor
|
||||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
"launched" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)),
|
||||
"failed" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
|
||||
_ => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace WorkspacesLauncherUI.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your project is launching. Waiting on ....
|
||||
/// Looks up a localized string similar to Your workspace is launching. Waiting on ....
|
||||
/// </summary>
|
||||
public static string LauncherWindowTitle {
|
||||
get {
|
||||
|
||||
@@ -124,6 +124,6 @@
|
||||
<value>Dismiss</value>
|
||||
</data>
|
||||
<data name="LauncherWindowTitle" xml:space="preserve">
|
||||
<value>Your project is launching. Waiting on ...</value>
|
||||
<value>Your workspace is launching. Waiting on ...</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -2,6 +2,7 @@
|
||||
x:Class="WorkspacesLauncherUI.StatusWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:WorkspacesLauncherUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:WorkspacesLauncherUI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -21,6 +22,11 @@
|
||||
Topmost="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<converters:BooleanToInvertedVisibilityConverter x:Key="BooleanToInvertedVisibilityConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="5" Background="Transparent">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="1*" />
|
||||
@@ -55,14 +61,28 @@
|
||||
FontWeight="Normal"
|
||||
Foreground="{DynamicResource PrimaryForegroundBrush}"
|
||||
Text="{Binding Name, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Image
|
||||
<ui:ProgressRing
|
||||
x:Name="ProgressRing"
|
||||
Grid.Column="2"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Source="{Binding StateImageSource}" />
|
||||
IsActive="True"
|
||||
Visibility="{Binding Loading, Mode=OneWay, Converter={StaticResource BoolToVis}, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
FontSize="20"
|
||||
Foreground="{Binding StateColor}"
|
||||
Text="{Binding StateGlyph}"
|
||||
Visibility="{Binding Loading, Mode=OneWay, Converter={StaticResource BooleanToInvertedVisibilityConverter}, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
|
||||
@@ -30,10 +30,7 @@
|
||||
<AssemblyName>PowerToys.WorkspacesLauncherUI</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="images\checkmark.png" />
|
||||
<None Remove="images\DefaultIcon.ico" />
|
||||
<None Remove="images\failed.png" />
|
||||
<None Remove="images\spinner.gif" />
|
||||
<None Remove="images\Workspaces.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -87,18 +84,6 @@
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="images\checkmark.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="images\failed.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="images\spinner.gif">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 49 KiB |
@@ -22,12 +22,6 @@ namespace WorkspacesData
|
||||
return settingsFolderPath + L"\\temp-workspaces.json";
|
||||
}
|
||||
|
||||
std::wstring TempLaunchWorkspacesFile()
|
||||
{
|
||||
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
|
||||
return settingsFolderPath + L"\\tempLaunch-workspaces.json";
|
||||
}
|
||||
|
||||
std::wstring LaunchWorkspacesFile()
|
||||
{
|
||||
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace WorkspacesData
|
||||
{
|
||||
std::wstring WorkspacesFile();
|
||||
std::wstring TempWorkspacesFile();
|
||||
std::wstring TempLaunchWorkspacesFile();
|
||||
std::wstring LaunchWorkspacesFile();
|
||||
|
||||
struct WorkspacesProject
|
||||
|
||||
@@ -75,7 +75,14 @@ public:
|
||||
|
||||
virtual void OnHotkeyEx() override
|
||||
{
|
||||
launch_editor();
|
||||
if (is_process_running())
|
||||
{
|
||||
sendHotkeyEvent();
|
||||
}
|
||||
else
|
||||
{
|
||||
launch_editor();
|
||||
}
|
||||
}
|
||||
|
||||
// Return the configured status for the gpo policy for the module
|
||||
@@ -157,6 +164,12 @@ public:
|
||||
m_toggleEditorEvent = nullptr;
|
||||
}
|
||||
|
||||
if (m_hotkeyEvent)
|
||||
{
|
||||
CloseHandle(m_hotkeyEvent);
|
||||
m_hotkeyEvent = nullptr;
|
||||
}
|
||||
|
||||
delete this;
|
||||
}
|
||||
|
||||
@@ -171,6 +184,12 @@ public:
|
||||
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "Workspaces");
|
||||
init_settings();
|
||||
|
||||
m_hotkeyEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::WORKSPACES_HOTKEY_EVENT);
|
||||
if (!m_hotkeyEvent)
|
||||
{
|
||||
Logger::warn(L"Failed to create hotkey event. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
m_toggleEditorEvent = CreateDefaultEvent(CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT);
|
||||
if (!m_toggleEditorEvent)
|
||||
{
|
||||
@@ -203,23 +222,12 @@ private:
|
||||
executable_args.append(std::to_wstring(powertoys_pid));
|
||||
}
|
||||
|
||||
void SendCloseEvent()
|
||||
void sendHotkeyEvent()
|
||||
{
|
||||
auto exitEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::WORKSPACES_EXIT_EVENT);
|
||||
if (!exitEvent)
|
||||
Logger::trace(L"Signaled hotkey event");
|
||||
if (!SetEvent(m_hotkeyEvent))
|
||||
{
|
||||
Logger::warn(L"Failed to create exitEvent. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"Signaled exitEvent");
|
||||
if (!SetEvent(exitEvent))
|
||||
{
|
||||
Logger::warn(L"Failed to signal exitEvent. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
ResetEvent(exitEvent);
|
||||
CloseHandle(exitEvent);
|
||||
Logger::warn(L"Failed to signal hotkey event. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,10 +245,14 @@ private:
|
||||
ResetEvent(m_toggleEditorEvent);
|
||||
}
|
||||
|
||||
if (m_hotkeyEvent)
|
||||
{
|
||||
ResetEvent(m_hotkeyEvent);
|
||||
}
|
||||
|
||||
if (m_hProcess)
|
||||
{
|
||||
TerminateProcess(m_hProcess, 0);
|
||||
SendCloseEvent();
|
||||
m_hProcess = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -302,15 +314,15 @@ private:
|
||||
{
|
||||
Logger::trace(L"Starting Workspaces Editor");
|
||||
|
||||
/*unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
std::wstring executable_args = L"";
|
||||
executable_args.append(std::to_wstring(powertoys_pid));*/
|
||||
executable_args.append(std::to_wstring(powertoys_pid));
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
sei.lpFile = L"PowerToys.WorkspacesEditor.exe";
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
//sei.lpParameters = executable_args.data();
|
||||
sei.lpParameters = executable_args.data();
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::trace("Successfully started the Workspaces Editor");
|
||||
@@ -323,6 +335,11 @@ private:
|
||||
m_hProcess = sei.hProcess;
|
||||
}
|
||||
|
||||
bool is_process_running() const
|
||||
{
|
||||
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
std::wstring app_name;
|
||||
//contains the non localized key of the powertoy
|
||||
std::wstring app_key;
|
||||
@@ -333,10 +350,13 @@ private:
|
||||
// Handle to event used to invoke Workspaces Editor
|
||||
HANDLE m_toggleEditorEvent;
|
||||
|
||||
// Handle to event used when hotkey is invoked
|
||||
HANDLE m_hotkeyEvent;
|
||||
|
||||
// Hotkey to invoke the module
|
||||
HotkeyEx m_hotkey{
|
||||
.modifiersMask = MOD_SHIFT | MOD_WIN,
|
||||
.vkCode = 0x4F, // O key;
|
||||
.modifiersMask = MOD_CONTROL | MOD_WIN,
|
||||
.vkCode = 0xC0, // VK_OEM_3 key; usually `~
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,6 @@ namespace Awake.Core
|
||||
// Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY
|
||||
// is representative of the date when the last change was made before
|
||||
// the pull request is issued.
|
||||
internal const string BuildId = "DAISY023_04102024";
|
||||
internal const string BuildId = "VISEGRADRELAY_08152024";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -62,7 +64,7 @@ namespace Awake.Core
|
||||
{
|
||||
Thread monitorThread = new(() =>
|
||||
{
|
||||
Thread.CurrentThread.IsBackground = true;
|
||||
Thread.CurrentThread.IsBackground = false;
|
||||
while (true)
|
||||
{
|
||||
ExecutionState state = _stateQueue.Take();
|
||||
@@ -126,10 +128,18 @@ namespace Awake.Core
|
||||
_stateQueue.Add(ExecutionState.ES_CONTINUOUS);
|
||||
|
||||
// Next, make sure that any existing background threads are terminated.
|
||||
_tokenSource.Cancel();
|
||||
_tokenSource.Dispose();
|
||||
if (_tokenSource != null)
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
_tokenSource.Dispose();
|
||||
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("The token source was null.");
|
||||
}
|
||||
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
Logger.LogInfo("Instantiating of new token source and thread token completed.");
|
||||
}
|
||||
|
||||
@@ -137,12 +147,11 @@ namespace Awake.Core
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent());
|
||||
|
||||
CancelExistingThread();
|
||||
|
||||
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
|
||||
|
||||
TrayHelper.SetShellIcon(TrayHelper.HiddenWindowHandle, $"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_INDEFINITE}]", _indefiniteIcon, TrayIconAction.Update);
|
||||
|
||||
CancelExistingThread();
|
||||
_stateQueue.Add(ComputeAwakeState(keepDisplayOn));
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
try
|
||||
@@ -417,5 +426,18 @@ namespace Awake.Core
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Process? GetParentProcess()
|
||||
{
|
||||
return GetParentProcess(Process.GetCurrentProcess().Handle);
|
||||
}
|
||||
|
||||
private static Process? GetParentProcess(IntPtr handle)
|
||||
{
|
||||
ProcessBasicInformation pbi = default;
|
||||
int status = Bridge.NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf<ProcessBasicInformation>(), out _);
|
||||
|
||||
return status != 0 ? null : Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
|
||||
namespace Awake.Core.Models
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct ProcessBasicInformation
|
||||
{
|
||||
public IntPtr ExitStatus;
|
||||
public IntPtr PebAddress;
|
||||
public IntPtr AffinityMask;
|
||||
public IntPtr BasePriority;
|
||||
public IntPtr UniquePID;
|
||||
public IntPtr InheritedFromUniqueProcessId;
|
||||
}
|
||||
}
|
||||
@@ -56,13 +56,13 @@ namespace Awake.Core.Native
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool DestroyMenu(IntPtr hMenu);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool DestroyWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern void PostQuitMessage(int nExitCode);
|
||||
|
||||
[DllImport("shell32.dll")]
|
||||
[DllImport("shell32.dll", SetLastError = true)]
|
||||
internal static extern bool Shell_NotifyIcon(int dwMessage, ref NotifyIconData pnid);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
@@ -83,14 +83,14 @@ namespace Awake.Core.Native
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool GetCursorPos(out Point lpPoint);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
@@ -100,7 +100,10 @@ namespace Awake.Core.Native
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool SetMenuInfo(IntPtr hMenu, ref MenuInfo lpcmi);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
internal static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ProcessBasicInformation processInformation, int processInformationLength, out int returnLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,6 @@ namespace Awake.Core
|
||||
HiddenWindowHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public static void InitializeTray(string text, Icon icon)
|
||||
{
|
||||
CreateHiddenWindow(icon, text);
|
||||
}
|
||||
|
||||
private static void ShowContextMenu(IntPtr hWnd)
|
||||
{
|
||||
if (TrayMenu != IntPtr.Zero)
|
||||
@@ -88,7 +83,7 @@ namespace Awake.Core
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateHiddenWindow(Icon icon, string text)
|
||||
public static void InitializeTray(Icon icon, string text)
|
||||
{
|
||||
IntPtr hWnd = IntPtr.Zero;
|
||||
|
||||
@@ -143,7 +138,13 @@ namespace Awake.Core
|
||||
Bridge.UpdateWindow(hWnd);
|
||||
|
||||
SetShellIcon(hWnd, text, icon);
|
||||
});
|
||||
}).Wait();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
RunOnMainThread(() =>
|
||||
{
|
||||
RunMessageLoop();
|
||||
});
|
||||
});
|
||||
@@ -151,6 +152,8 @@ namespace Awake.Core
|
||||
|
||||
internal static void SetShellIcon(IntPtr hWnd, string text, Icon? icon, TrayIconAction action = TrayIconAction.Add)
|
||||
{
|
||||
Logger.LogInfo($"Setting the shell icon.\nText: {text}\nAction: {action}");
|
||||
|
||||
int message = Native.Constants.NIM_ADD;
|
||||
|
||||
switch (action)
|
||||
@@ -168,6 +171,8 @@ namespace Awake.Core
|
||||
|
||||
if (action == TrayIconAction.Add || action == TrayIconAction.Update)
|
||||
{
|
||||
Logger.LogInfo($"Adding or updating tray icon. HIcon handle is {icon?.Handle}\nHWnd: {hWnd}");
|
||||
|
||||
_notifyIconData = new NotifyIconData
|
||||
{
|
||||
CbSize = Marshal.SizeOf(typeof(NotifyIconData)),
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace Awake
|
||||
internal static readonly string[] AliasesTimeOption = ["--time-limit", "-t"];
|
||||
internal static readonly string[] AliasesPidOption = ["--pid", "-p"];
|
||||
internal static readonly string[] AliasesExpireAtOption = ["--expire-at", "-e"];
|
||||
internal static readonly string[] AliasesParentPidOption = ["--use-parent-pid", "-u"];
|
||||
|
||||
private static readonly Icon _defaultAwakeIcon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets/Awake/awake.ico"));
|
||||
|
||||
@@ -116,6 +117,12 @@ namespace Awake
|
||||
IsRequired = false,
|
||||
};
|
||||
|
||||
var parentPidOption = new Option<bool>(AliasesParentPidOption, () => true, Resources.AWAKE_CMD_PARENT_PID_OPTION)
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
IsRequired = false,
|
||||
};
|
||||
|
||||
RootCommand? rootCommand =
|
||||
[
|
||||
configOption,
|
||||
@@ -123,10 +130,11 @@ namespace Awake
|
||||
timeOption,
|
||||
pidOption,
|
||||
expireAtOption,
|
||||
parentPidOption,
|
||||
];
|
||||
|
||||
rootCommand.Description = Core.Constants.AppName;
|
||||
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption);
|
||||
rootCommand.SetHandler(HandleCommandLineArguments, configOption, displayOption, timeOption, pidOption, expireAtOption, parentPidOption);
|
||||
|
||||
return rootCommand.InvokeAsync(args).Result;
|
||||
}
|
||||
@@ -153,9 +161,9 @@ namespace Awake
|
||||
Manager.CompleteExit(exitCode);
|
||||
}
|
||||
|
||||
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt)
|
||||
private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt, bool useParentPid)
|
||||
{
|
||||
if (pid == 0)
|
||||
if (pid == 0 && !useParentPid)
|
||||
{
|
||||
Logger.LogInfo("No PID specified. Allocating console...");
|
||||
Manager.AllocateConsole();
|
||||
@@ -175,11 +183,12 @@ namespace Awake
|
||||
Logger.LogInfo($"The value for --time-limit is: {timeLimit}");
|
||||
Logger.LogInfo($"The value for --pid is: {pid}");
|
||||
Logger.LogInfo($"The value for --expire-at is: {expireAt}");
|
||||
Logger.LogInfo($"The value for --use-parent-pid is: {useParentPid}");
|
||||
|
||||
// Start the monitor thread that will be used to track the current state.
|
||||
Manager.StartMonitor();
|
||||
|
||||
TrayHelper.InitializeTray(Core.Constants.FullAppName, _defaultAwakeIcon);
|
||||
TrayHelper.InitializeTray(_defaultAwakeIcon, Core.Constants.FullAppName);
|
||||
|
||||
var eventHandle = new EventWaitHandle(false, EventResetMode.ManualReset, PowerToys.Interop.Constants.AwakeExitEvent());
|
||||
new Thread(() =>
|
||||
@@ -209,12 +218,47 @@ namespace Awake
|
||||
}
|
||||
|
||||
ScaffoldConfiguration(settingsPath);
|
||||
|
||||
if (pid != 0)
|
||||
{
|
||||
Logger.LogInfo($"Bound to target process while also using PowerToys settings: {pid}");
|
||||
|
||||
RunnerHelper.WaitForPowerToysRunner(pid, () =>
|
||||
{
|
||||
Logger.LogInfo($"Triggered PID-based exit handler for PID {pid}.");
|
||||
Exit(Resources.AWAKE_EXIT_BINDING_HOOK_MESSAGE, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"There was a problem with the configuration file. Make sure it exists.\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
else if (pid != 0 || useParentPid)
|
||||
{
|
||||
// Second, we snap to process-based execution. Because this is something that
|
||||
// is snapped to a running entity, we only want to enable the ability to set
|
||||
// indefinite keep-awake with the display settings that the user wants to set.
|
||||
int targetPid = pid != 0 ? pid : useParentPid ? Manager.GetParentProcess()?.Id ?? 0 : 0;
|
||||
|
||||
if (targetPid != 0)
|
||||
{
|
||||
Logger.LogInfo($"Bound to target process: {targetPid}");
|
||||
|
||||
Manager.SetIndefiniteKeepAwake(displayOn);
|
||||
|
||||
RunnerHelper.WaitForPowerToysRunner(targetPid, () =>
|
||||
{
|
||||
Logger.LogInfo($"Triggered PID-based exit handler for PID {targetPid}.");
|
||||
Exit(Resources.AWAKE_EXIT_BINDING_HOOK_MESSAGE, 0);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("Not binding to any process.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Date-based binding takes precedence over timed configuration, so we want to
|
||||
@@ -247,15 +291,6 @@ namespace Awake
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pid != 0)
|
||||
{
|
||||
RunnerHelper.WaitForPowerToysRunner(pid, () =>
|
||||
{
|
||||
Logger.LogInfo($"Triggered PID-based exit handler for PID {pid}.");
|
||||
Exit(Resources.AWAKE_EXIT_BINDING_HOOK_MESSAGE, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScaffoldConfiguration(string settingsPath)
|
||||
|
||||
@@ -114,6 +114,15 @@ namespace Awake.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Uses the parent process as the bound target - once the process terminates, Awake stops..
|
||||
/// </summary>
|
||||
internal static string AWAKE_CMD_PARENT_PID_OPTION {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_CMD_PARENT_PID_OPTION", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Exit.
|
||||
/// </summary>
|
||||
|
||||
@@ -205,4 +205,7 @@
|
||||
<value>s</value>
|
||||
<comment>Used to display number of seconds in the system tray tooltip.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_CMD_PARENT_PID_OPTION" xml:space="preserve">
|
||||
<value>Uses the parent process as the bound target - once the process terminates, Awake stops.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -15,6 +15,7 @@
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const wchar_t PowerToysAppFZEditor[] = L"POWERTOYS.FANCYZONESEDITOR.EXE";
|
||||
const wchar_t PowerToysWorkspacesEditor[] = L"POWERTOYS.WORKSPACESEDITOR.EXE";
|
||||
const char SplashClassName[] = "MsoSplash";
|
||||
const wchar_t CoreWindow[] = L"Windows.UI.Core.CoreWindow";
|
||||
const wchar_t SearchUI[] = L"SearchUI.exe";
|
||||
@@ -200,7 +201,7 @@ bool FancyZonesWindowUtils::IsExcludedByDefault(const HWND& hwnd, const std::wst
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::vector<std::wstring> defaultExcludedApps = { NonLocalizable::PowerToysAppFZEditor, NonLocalizable::CoreWindow, NonLocalizable::SearchUI };
|
||||
static std::vector<std::wstring> defaultExcludedApps = { NonLocalizable::PowerToysAppFZEditor, NonLocalizable::PowerToysWorkspacesEditor, NonLocalizable::CoreWindow, NonLocalizable::SearchUI };
|
||||
return (check_excluded_app(hwnd, processPath, defaultExcludedApps));
|
||||
}
|
||||
|
||||
|
||||
@@ -247,6 +247,14 @@ namespace KeyboardEventHandlers
|
||||
bool isMatchOnChordEnd = false;
|
||||
bool isMatchOnChordStart = false;
|
||||
|
||||
static bool isAltRightKeyInvoked = false;
|
||||
|
||||
// Check if the right Alt key (AltGr) is pressed.
|
||||
if (data->lParam->vkCode == VK_RMENU && ii.GetVirtualKeyState(VK_LCONTROL))
|
||||
{
|
||||
isAltRightKeyInvoked = true;
|
||||
}
|
||||
|
||||
// If the shortcut has been pressed down
|
||||
if (!it->second.isShortcutInvoked && it->first.CheckModifiersKeyboardState(ii))
|
||||
{
|
||||
@@ -522,6 +530,13 @@ namespace KeyboardEventHandlers
|
||||
// 5. The user presses any key apart from the action key or a modifier key in the original shortcut - revert the keyboard state to just the original modifiers being held down along with the current key press
|
||||
// 6. The user releases any key apart from original modifier or original action key - This can't happen since the key down would have to happen first, which is handled above
|
||||
|
||||
// Prevents the unintended release of the Ctrl part when AltGr is pressed. AltGr acts as both Ctrl and Alt being pressed.
|
||||
// After triggering a shortcut involving AltGr, the system might attempt to release the Ctrl part. This code ensures Ctrl remains pressed, maintaining the AltGr state correctly.
|
||||
if (isAltRightKeyInvoked && data->lParam->vkCode == VK_LCONTROL && (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the common keys between the two shortcuts
|
||||
int commonKeys = (remapToShortcut && !isRunProgram) ? it->first.GetCommonModifiersCount(std::get<Shortcut>(it->second.targetShortcut)) : 0;
|
||||
|
||||
@@ -543,8 +558,15 @@ namespace KeyboardEventHandlers
|
||||
|
||||
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first, data->lParam->vkCode);
|
||||
|
||||
// Set original shortcut key down state except the action key and the released modifier since the original action key may or may not be held down. If it is held down it will generate it's own key message
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut), data->lParam->vkCode);
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
// Set original shortcut key down state except the action key and the released modifier since the original action key may or may not be held down. If it is held down it will generate it's own key message
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut), data->lParam->vkCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
isAltRightKeyInvoked = false;
|
||||
}
|
||||
|
||||
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+Ctrl+A->Ctrl+V, press Win+Ctrl+A and release A then Ctrl, since Win will be pressed here we need to send a dummy event after it
|
||||
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
@@ -559,8 +581,16 @@ namespace KeyboardEventHandlers
|
||||
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
}
|
||||
|
||||
// Set original shortcut key down state except the action key and the released modifier since the original action key may or may not be held down. If it is held down it will generate it's own key message
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, Shortcut(), data->lParam->vkCode);
|
||||
// Ensures that after releasing both the action key and AltGr, Ctrl does not remain falsely pressed.
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
// Set original shortcut key down state except the action key and the released modifier since the original action key may or may not be held down. If it is held down it will generate it's own key message
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, Shortcut(), data->lParam->vkCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
isAltRightKeyInvoked = false;
|
||||
}
|
||||
|
||||
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+Ctrl+A->V, press Win+Ctrl+A and release A then Ctrl, since Win will be pressed here we need to send a dummy event after it
|
||||
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
@@ -672,16 +702,22 @@ namespace KeyboardEventHandlers
|
||||
// Release new key state
|
||||
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(Helpers::FilterArtificialKeys(std::get<DWORD>(it->second.targetShortcut))), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
|
||||
// Set original shortcut key down state except the action key
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
// Set original shortcut key down state except the action key
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
}
|
||||
|
||||
// Send a dummy key event to prevent modifier press+release from being triggered. Example: Win+A->V, press Shift+Win+A and release A, since Win will be pressed here we need to send a dummy event after it
|
||||
Helpers::SetDummyKeyEvent(keyEventList, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
|
||||
// Reset the remap state
|
||||
it->second.isShortcutInvoked = false;
|
||||
it->second.winKeyInvoked = ModifierKey::Disabled;
|
||||
it->second.isOriginalActionKeyPressed = false;
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
// Reset the remap state
|
||||
it->second.isShortcutInvoked = false;
|
||||
it->second.winKeyInvoked = ModifierKey::Disabled;
|
||||
it->second.isOriginalActionKeyPressed = false;
|
||||
}
|
||||
|
||||
// If app specific shortcut has finished invoking, reset the target application
|
||||
if (activatedApp != KeyboardManagerConstants::NoActivatedApp)
|
||||
@@ -741,7 +777,10 @@ namespace KeyboardEventHandlers
|
||||
if (newRemapping.RemapToKey())
|
||||
{
|
||||
DWORD to = std::get<0>(newRemapping.targetShortcut);
|
||||
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
}
|
||||
if (ii.GetVirtualKeyState(static_cast<WORD>(from.actionKey)))
|
||||
{
|
||||
// If the action key from the last shortcut is still being pressed, release it.
|
||||
@@ -752,13 +791,19 @@ namespace KeyboardEventHandlers
|
||||
else
|
||||
{
|
||||
Shortcut to = std::get<Shortcut>(newRemapping.targetShortcut);
|
||||
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, to);
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
Helpers::SetModifierKeyEvents(from, it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, to);
|
||||
}
|
||||
if (ii.GetVirtualKeyState(static_cast<WORD>(from.actionKey)))
|
||||
{
|
||||
// If the action key from the last shortcut is still being pressed, release it.
|
||||
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(from.actionKey), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
}
|
||||
Helpers::SetModifierKeyEvents(to, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, from);
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
Helpers::SetModifierKeyEvents(to, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, from);
|
||||
}
|
||||
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(to.actionKey), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
newRemapping.isShortcutInvoked = true;
|
||||
}
|
||||
@@ -783,10 +828,13 @@ namespace KeyboardEventHandlers
|
||||
{
|
||||
Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
}
|
||||
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first);
|
||||
|
||||
// Set old shortcut key down state
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
|
||||
// Set old shortcut key down state
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, std::get<Shortcut>(it->second.targetShortcut));
|
||||
}
|
||||
|
||||
// key down for original shortcut action key with shortcut flag so that we don't invoke the same shortcut remap again
|
||||
if (isActionKeyPressed)
|
||||
@@ -800,10 +848,13 @@ namespace KeyboardEventHandlers
|
||||
// Do not send a dummy key as we want the current key press to behave as normal i.e. it can do press+release functionality if required. Required to allow a shortcut to Win key remap invoked directly after shortcut to shortcut is released to open start menu
|
||||
}
|
||||
|
||||
// Reset the remap state
|
||||
it->second.isShortcutInvoked = false;
|
||||
it->second.winKeyInvoked = ModifierKey::Disabled;
|
||||
it->second.isOriginalActionKeyPressed = false;
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
// Reset the remap state
|
||||
it->second.isShortcutInvoked = false;
|
||||
it->second.winKeyInvoked = ModifierKey::Disabled;
|
||||
it->second.isOriginalActionKeyPressed = false;
|
||||
}
|
||||
|
||||
// If app specific shortcut has finished invoking, reset the target application
|
||||
if (activatedApp)
|
||||
@@ -848,8 +899,11 @@ namespace KeyboardEventHandlers
|
||||
{
|
||||
std::vector<INPUT> keyEventList;
|
||||
|
||||
// Set original shortcut key down state
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
// Set original shortcut key down state
|
||||
Helpers::SetModifierKeyEvents(it->first, it->second.winKeyInvoked, keyEventList, true, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
|
||||
}
|
||||
|
||||
// Send the original action key only if it is physically pressed. For remappings to keys other than disabled we already check earlier that it is not pressed in this scenario. For remap to disable
|
||||
if (isRemapToDisable && isOriginalActionKeyPressed)
|
||||
@@ -863,10 +917,13 @@ namespace KeyboardEventHandlers
|
||||
|
||||
// Do not send a dummy key as we want the current key press to behave as normal i.e. it can do press+release functionality if required. Required to allow a shortcut to Win key remap invoked directly after another shortcut to key remap is released to open start menu
|
||||
|
||||
// Reset the remap state
|
||||
it->second.isShortcutInvoked = false;
|
||||
it->second.winKeyInvoked = ModifierKey::Disabled;
|
||||
it->second.isOriginalActionKeyPressed = false;
|
||||
if (!isAltRightKeyInvoked)
|
||||
{
|
||||
// Reset the remap state
|
||||
it->second.isShortcutInvoked = false;
|
||||
it->second.winKeyInvoked = ModifierKey::Disabled;
|
||||
it->second.isOriginalActionKeyPressed = false;
|
||||
}
|
||||
|
||||
// If app specific shortcut has finished invoking, reset the target application
|
||||
if (activatedApp != KeyboardManagerConstants::NoActivatedApp)
|
||||
|
||||
@@ -50,7 +50,6 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter
|
||||
return new List<Result>();
|
||||
}
|
||||
|
||||
// Convert
|
||||
return UnitHandler.Convert(convertModel)
|
||||
.Select(x => GetResult(x))
|
||||
.ToList();
|
||||
|
||||
@@ -593,8 +593,7 @@ namespace PowerLauncher.ViewModel
|
||||
_updateSource?.Dispose();
|
||||
var currentUpdateSource = new CancellationTokenSource();
|
||||
_updateSource = currentUpdateSource;
|
||||
var currentCancellationToken = _updateSource.Token;
|
||||
_updateToken = currentCancellationToken;
|
||||
_updateToken = _updateSource.Token;
|
||||
var queryText = QueryText.Trim();
|
||||
|
||||
var pluginQueryPairs = QueryBuilder.Build(queryText);
|
||||
@@ -631,7 +630,7 @@ namespace PowerLauncher.ViewModel
|
||||
query.SelectedItems = _userSelectedRecord.GetGenericHistory();
|
||||
var results = PluginManager.QueryForPlugin(plugin, query);
|
||||
resultPluginPair[plugin.Metadata] = results;
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -642,7 +641,7 @@ namespace PowerLauncher.ViewModel
|
||||
}
|
||||
else
|
||||
{
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
|
||||
// To execute a query corresponding to each plugin
|
||||
foreach (KeyValuePair<PluginPair, Query> pluginQueryItem in pluginQueryPairs)
|
||||
@@ -652,7 +651,7 @@ namespace PowerLauncher.ViewModel
|
||||
query.SelectedItems = _userSelectedRecord.GetGenericHistory();
|
||||
var results = PluginManager.QueryForPlugin(plugin, query);
|
||||
resultPluginPair[plugin.Metadata] = results;
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,11 +663,11 @@ namespace PowerLauncher.ViewModel
|
||||
Results.Clear();
|
||||
foreach (var p in resultPluginPair)
|
||||
{
|
||||
UpdateResultView(p.Value, queryText, currentCancellationToken);
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
UpdateResultView(p.Value, queryText, _updateToken);
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
numResults = Results.Results.Count;
|
||||
if (!doFinalSort)
|
||||
{
|
||||
@@ -677,7 +676,7 @@ namespace PowerLauncher.ViewModel
|
||||
}
|
||||
}
|
||||
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
if (!doFinalSort)
|
||||
{
|
||||
UpdateResultsListViewAfterQuery(queryText);
|
||||
@@ -689,7 +688,7 @@ namespace PowerLauncher.ViewModel
|
||||
if (!delayedExecution.HasValue || delayedExecution.Value)
|
||||
{
|
||||
// Run the slower query of the DelayedExecution plugins
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
Parallel.ForEach(plugins, (plugin) =>
|
||||
{
|
||||
try
|
||||
@@ -697,7 +696,7 @@ namespace PowerLauncher.ViewModel
|
||||
Query query;
|
||||
pluginQueryPairs.TryGetValue(plugin, out query);
|
||||
var results = PluginManager.QueryForPlugin(plugin, query, true);
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
if ((results?.Count ?? 0) != 0)
|
||||
{
|
||||
lock (_addResultsLock)
|
||||
@@ -705,16 +704,16 @@ namespace PowerLauncher.ViewModel
|
||||
// Using CurrentCultureIgnoreCase since this is user facing
|
||||
if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Remove the original results from the plugin
|
||||
Results.Results.RemoveAll(r => r.Result.PluginID == plugin.Metadata.ID);
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Add the new results from the plugin
|
||||
UpdateResultView(results, queryText, currentCancellationToken);
|
||||
UpdateResultView(results, queryText, _updateToken);
|
||||
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
numResults = Results.Results.Count;
|
||||
if (!doFinalSort)
|
||||
{
|
||||
@@ -722,7 +721,7 @@ namespace PowerLauncher.ViewModel
|
||||
}
|
||||
}
|
||||
|
||||
currentCancellationToken.ThrowIfCancellationRequested();
|
||||
_updateToken.ThrowIfCancellationRequested();
|
||||
if (!doFinalSort)
|
||||
{
|
||||
UpdateResultsListViewAfterQuery(queryText, noInitialResults, true);
|
||||
@@ -751,7 +750,7 @@ namespace PowerLauncher.ViewModel
|
||||
};
|
||||
PowerToysTelemetry.Log.WriteEvent(queryEvent);
|
||||
},
|
||||
currentCancellationToken);
|
||||
_updateToken);
|
||||
|
||||
if (doFinalSort)
|
||||
{
|
||||
@@ -763,7 +762,7 @@ namespace PowerLauncher.ViewModel
|
||||
Results.SelectedItem = Results.Results.FirstOrDefault();
|
||||
UpdateResultsListViewAfterQuery(queryText, false, false);
|
||||
},
|
||||
currentCancellationToken);
|
||||
_updateToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -157,14 +157,16 @@ namespace PowerAccent.Core
|
||||
{
|
||||
return letter switch
|
||||
{
|
||||
LetterKey.VK_0 => new[] { "↉" },
|
||||
LetterKey.VK_1 => new[] { "½", "⅓", "¼", "⅕", "⅙", "⅐", "⅛", "⅑", "⅒" },
|
||||
LetterKey.VK_2 => new[] { "⅔", "⅖" },
|
||||
LetterKey.VK_3 => new[] { "¾", "⅗", "⅜" },
|
||||
LetterKey.VK_4 => new[] { "⅘" },
|
||||
LetterKey.VK_5 => new[] { "⅚", "⅝" },
|
||||
LetterKey.VK_7 => new[] { "⅞" },
|
||||
LetterKey.VK_8 => new[] { "∞" },
|
||||
LetterKey.VK_0 => new[] { "₀", "⁰", "↉" },
|
||||
LetterKey.VK_1 => new[] { "₁", "¹", "½", "⅓", "¼", "⅕", "⅙", "⅐", "⅛", "⅑", "⅒" },
|
||||
LetterKey.VK_2 => new[] { "₂", "²", "⅔", "⅖" },
|
||||
LetterKey.VK_3 => new[] { "₃", "³", "¾", "⅗", "⅜" },
|
||||
LetterKey.VK_4 => new[] { "₄", "⁴", "⅘" },
|
||||
LetterKey.VK_5 => new[] { "₅", "⁵", "⅚", "⅝" },
|
||||
LetterKey.VK_6 => new[] { "₆", "⁶" },
|
||||
LetterKey.VK_7 => new[] { "₇", "⁷", "⅞" },
|
||||
LetterKey.VK_8 => new[] { "₈", "⁸", "∞" },
|
||||
LetterKey.VK_9 => new[] { "₉", "⁹" },
|
||||
LetterKey.VK_A => new[] { "ȧ", "ǽ", "∀" },
|
||||
LetterKey.VK_B => new[] { "ḃ" },
|
||||
LetterKey.VK_C => new[] { "ċ", "°C", "©", "ℂ", "∁" },
|
||||
@@ -478,16 +480,6 @@ namespace PowerAccent.Core
|
||||
{
|
||||
return letter switch
|
||||
{
|
||||
LetterKey.VK_0 => new[] { "₀", "⁰" },
|
||||
LetterKey.VK_1 => new[] { "₁", "¹" },
|
||||
LetterKey.VK_2 => new[] { "₂", "²" },
|
||||
LetterKey.VK_3 => new[] { "₃", "³" },
|
||||
LetterKey.VK_4 => new[] { "₄", "⁴" },
|
||||
LetterKey.VK_5 => new[] { "₅", "⁵" },
|
||||
LetterKey.VK_6 => new[] { "₆", "⁶" },
|
||||
LetterKey.VK_7 => new[] { "₇", "⁷" },
|
||||
LetterKey.VK_8 => new[] { "₈", "⁸" },
|
||||
LetterKey.VK_9 => new[] { "₉", "⁹" },
|
||||
LetterKey.VK_A => new[] { "á", "à", "â", "ã", "ª" },
|
||||
LetterKey.VK_C => new[] { "ç" },
|
||||
LetterKey.VK_E => new[] { "é", "ê", "€" },
|
||||
|
||||
183
src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneable
|
||||
{
|
||||
private int _id;
|
||||
private string _name = string.Empty;
|
||||
private string _prompt = string.Empty;
|
||||
private HotkeySettings _shortcut = new();
|
||||
private bool _isShown;
|
||||
private bool _canMoveUp;
|
||||
private bool _canMoveDown;
|
||||
private bool _isValid;
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public int Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
{
|
||||
if (_id != value)
|
||||
{
|
||||
_id = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (_name != value)
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
UpdateIsValid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt
|
||||
{
|
||||
get => _prompt;
|
||||
set
|
||||
{
|
||||
if (_prompt != value)
|
||||
{
|
||||
_prompt = value;
|
||||
OnPropertyChanged();
|
||||
UpdateIsValid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("shortcut")]
|
||||
public HotkeySettings Shortcut
|
||||
{
|
||||
get => _shortcut;
|
||||
set
|
||||
{
|
||||
if (_shortcut != value)
|
||||
{
|
||||
// We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called
|
||||
// with null; the ShortcutControl depends on this.
|
||||
_shortcut = value ?? new();
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("isShown")]
|
||||
public bool IsShown
|
||||
{
|
||||
get => _isShown;
|
||||
set
|
||||
{
|
||||
if (_isShown != value)
|
||||
{
|
||||
_isShown = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool CanMoveUp
|
||||
{
|
||||
get => _canMoveUp;
|
||||
set
|
||||
{
|
||||
if (_canMoveUp != value)
|
||||
{
|
||||
_canMoveUp = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool CanMoveDown
|
||||
{
|
||||
get => _canMoveDown;
|
||||
set
|
||||
{
|
||||
if (_canMoveDown != value)
|
||||
{
|
||||
_canMoveDown = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsValid
|
||||
{
|
||||
get => _isValid;
|
||||
private set
|
||||
{
|
||||
if (_isValid != value)
|
||||
{
|
||||
_isValid = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public string ToJsonString() => JsonSerializer.Serialize(this);
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
AdvancedPasteCustomAction clone = new();
|
||||
clone.Update(this);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public void Update(AdvancedPasteCustomAction other)
|
||||
{
|
||||
Id = other.Id;
|
||||
Name = other.Name;
|
||||
Prompt = other.Prompt;
|
||||
Shortcut = other.GetShortcutClone();
|
||||
IsShown = other.IsShown;
|
||||
CanMoveUp = other.CanMoveUp;
|
||||
CanMoveDown = other.CanMoveDown;
|
||||
}
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private HotkeySettings GetShortcutClone()
|
||||
{
|
||||
object shortcut = null;
|
||||
if (Shortcut.TryToCmdRepresentable(out string shortcutString))
|
||||
{
|
||||
_ = HotkeySettings.TryParseFromCmd(shortcutString, out shortcut);
|
||||
}
|
||||
|
||||
return (shortcut as HotkeySettings) ?? new HotkeySettings();
|
||||
}
|
||||
|
||||
private void UpdateIsValid()
|
||||
{
|
||||
IsValid = !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Prompt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
public sealed class AdvancedPasteCustomActions
|
||||
{
|
||||
private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public ObservableCollection<AdvancedPasteCustomAction> Value { get; set; } = [];
|
||||
|
||||
public AdvancedPasteCustomActions()
|
||||
{
|
||||
}
|
||||
|
||||
public string ToJsonString() => JsonSerializer.Serialize(this, _serializerOptions);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
PasteAsPlainTextShortcut = DefaultPasteAsPlainTextShortcut;
|
||||
PasteAsMarkdownShortcut = new();
|
||||
PasteAsJsonShortcut = new();
|
||||
CustomActions = new();
|
||||
ShowCustomPreview = true;
|
||||
SendPasteKeyCombination = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
@@ -47,6 +48,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("paste-as-json-hotkey")]
|
||||
public HotkeySettings PasteAsJsonShortcut { get; set; }
|
||||
|
||||
[JsonPropertyName("custom-actions")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteCustomActions CustomActions { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
@@ -16,38 +16,38 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
Id = id;
|
||||
Name = string.Empty;
|
||||
Fit = (int)ResizeFit.Fit;
|
||||
Fit = ResizeFit.Fit;
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
Unit = (int)ResizeUnit.Pixel;
|
||||
Unit = ResizeUnit.Pixel;
|
||||
}
|
||||
|
||||
public ImageSize()
|
||||
{
|
||||
Id = 0;
|
||||
Name = string.Empty;
|
||||
Fit = (int)ResizeFit.Fit;
|
||||
Fit = ResizeFit.Fit;
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
Unit = (int)ResizeUnit.Pixel;
|
||||
Unit = ResizeUnit.Pixel;
|
||||
}
|
||||
|
||||
public ImageSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Fit = (int)fit;
|
||||
Fit = fit;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Unit = (int)unit;
|
||||
Unit = unit;
|
||||
}
|
||||
|
||||
private int _id;
|
||||
private string _name;
|
||||
private int _fit;
|
||||
private ResizeFit _fit;
|
||||
private double _height;
|
||||
private double _width;
|
||||
private int _unit;
|
||||
private ResizeUnit _unit;
|
||||
|
||||
public int Id
|
||||
{
|
||||
@@ -70,7 +70,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Unit == 2 && Fit != 2)
|
||||
if (Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Unit == 2 && Fit != 2)
|
||||
if (Unit == ResizeUnit.Percent && Fit != ResizeFit.Stretch)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
|
||||
[JsonPropertyName("fit")]
|
||||
public int Fit
|
||||
public ResizeFit Fit
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -193,7 +193,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
|
||||
[JsonPropertyName("unit")]
|
||||
public int Unit
|
||||
public ResizeUnit Unit
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
@@ -28,18 +31,25 @@ public sealed class SetAdditionalSettingsCommandLineCommand
|
||||
|
||||
private struct AdditionalPropertyInfo
|
||||
{
|
||||
public string RootPropertyName;
|
||||
public JsonValueKind RootObjectType;
|
||||
// A path to the property starting from the root module Settings object in the following format: "RootPropertyA.NestedPropertyB[...]"
|
||||
public string PropertyPath;
|
||||
|
||||
// Property Type hint so we know how to handle it
|
||||
public JsonValueKind PropertyType;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, AdditionalPropertyInfo> SupportedAdditionalPropertiesInfoForModules = new Dictionary<string, AdditionalPropertyInfo> { { "PowerLauncher", new AdditionalPropertyInfo { RootPropertyName = "Plugins", RootObjectType = JsonValueKind.Array } } };
|
||||
private static readonly Dictionary<string, AdditionalPropertyInfo> SupportedAdditionalPropertiesInfoForModules = new Dictionary<string, AdditionalPropertyInfo> { { "PowerLauncher", new AdditionalPropertyInfo { PropertyPath = "Plugins", PropertyType = JsonValueKind.Array } }, { "ImageResizer", new AdditionalPropertyInfo { PropertyPath = "Properties.ImageresizerSizes.Value", PropertyType = JsonValueKind.Array } } };
|
||||
|
||||
private static void ExecuteRootArray(JsonElement.ArrayEnumerator properties, IEnumerable<object> currentPropertyValuesArray)
|
||||
private static IEnumerable<object> ExecuteRootArray(IEnumerable<JsonElement> properties, IEnumerable<object> currentPropertyValuesArray)
|
||||
{
|
||||
// In case it's an array of object -> combine the existing values with the provided
|
||||
var currentPropertyValueType = currentPropertyValuesArray.FirstOrDefault()?.GetType();
|
||||
// In case it's an array of objects -> combine the existing values with the provided
|
||||
var result = currentPropertyValuesArray;
|
||||
|
||||
var currentPropertyValueType = GetUnderlyingTypeOfCollection(currentPropertyValuesArray);
|
||||
object matchedElement = null;
|
||||
|
||||
object newKeyPropertyValue = null;
|
||||
|
||||
foreach (var arrayElement in properties)
|
||||
{
|
||||
var newElementPropertyValues = new Dictionary<string, object>();
|
||||
@@ -47,15 +57,16 @@ public sealed class SetAdditionalSettingsCommandLineCommand
|
||||
{
|
||||
var elementPropertyName = elementProperty.Name;
|
||||
var elementPropertyType = currentPropertyValueType.GetProperty(elementPropertyName).PropertyType;
|
||||
var elemePropertyValue = ICmdLineRepresentable.ParseFor(elementPropertyType, elementProperty.Value.ToString());
|
||||
var elementNewPropertyValue = ICmdLineRepresentable.ParseFor(elementPropertyType, elementProperty.Value.ToString());
|
||||
if (elementPropertyName == KeyPropertyName)
|
||||
{
|
||||
newKeyPropertyValue = elementNewPropertyValue;
|
||||
foreach (var currentElementValue in currentPropertyValuesArray)
|
||||
{
|
||||
var currentElementType = currentElementValue.GetType();
|
||||
var keyPropertyNameInfo = currentElementType.GetProperty(KeyPropertyName);
|
||||
var keyPropertyValue = keyPropertyNameInfo.GetValue(currentElementValue);
|
||||
if (string.Equals(keyPropertyValue, elemePropertyValue))
|
||||
var currentKeyPropertyValue = keyPropertyNameInfo.GetValue(currentElementValue);
|
||||
if (string.Equals(currentKeyPropertyValue, elementNewPropertyValue))
|
||||
{
|
||||
matchedElement = currentElementValue;
|
||||
break;
|
||||
@@ -64,7 +75,18 @@ public sealed class SetAdditionalSettingsCommandLineCommand
|
||||
}
|
||||
else
|
||||
{
|
||||
newElementPropertyValues.Add(elementPropertyName, elemePropertyValue);
|
||||
newElementPropertyValues.Add(elementPropertyName, elementNewPropertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Appending a new element -> create it first using a default ctor with 0 args and append it to the result
|
||||
if (matchedElement == null)
|
||||
{
|
||||
newElementPropertyValues.Add(KeyPropertyName, newKeyPropertyValue);
|
||||
matchedElement = Activator.CreateInstance(currentPropertyValueType);
|
||||
if (matchedElement != null)
|
||||
{
|
||||
result = result.Append(matchedElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +98,148 @@ public sealed class SetAdditionalSettingsCommandLineCommand
|
||||
propertyInfo.SetValue(matchedElement, overriddenProperty.Value);
|
||||
}
|
||||
}
|
||||
|
||||
matchedElement = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static object GetNestedPropertyValue(object obj, string propertyPath)
|
||||
{
|
||||
if (obj == null || string.IsNullOrWhiteSpace(propertyPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var properties = propertyPath.Split('.');
|
||||
object currentObject = obj;
|
||||
PropertyInfo currentProperty = null;
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (currentObject == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
currentProperty = currentObject.GetType().GetProperty(property);
|
||||
if (currentProperty == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
currentObject = currentProperty.GetValue(currentObject);
|
||||
}
|
||||
|
||||
return currentObject;
|
||||
}
|
||||
|
||||
// To apply changes to a generic collection, we must recreate it and assign it to the property
|
||||
private static object CreateCompatibleCollection(Type collectionType, Type elementType, IEnumerable<object> newValues)
|
||||
{
|
||||
if (typeof(IList<>).MakeGenericType(elementType).IsAssignableFrom(collectionType) ||
|
||||
typeof(ObservableCollection<>).MakeGenericType(elementType).IsAssignableFrom(collectionType))
|
||||
{
|
||||
var concreteType = typeof(List<>).MakeGenericType(elementType);
|
||||
if (typeof(ObservableCollection<>).MakeGenericType(elementType).IsAssignableFrom(collectionType))
|
||||
{
|
||||
concreteType = typeof(ObservableCollection<>).MakeGenericType(elementType);
|
||||
}
|
||||
else if (collectionType.IsInterface || collectionType.IsAbstract)
|
||||
{
|
||||
concreteType = typeof(List<>).MakeGenericType(elementType);
|
||||
}
|
||||
|
||||
var list = (IList)Activator.CreateInstance(concreteType);
|
||||
foreach (var newValue in newValues)
|
||||
{
|
||||
list.Add(Convert.ChangeType(newValue, elementType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
else if (typeof(IEnumerable<>).MakeGenericType(elementType).IsAssignableFrom(collectionType))
|
||||
{
|
||||
var listType = typeof(List<>).MakeGenericType(elementType);
|
||||
var list = (IList)Activator.CreateInstance(listType);
|
||||
foreach (var newValue in newValues)
|
||||
{
|
||||
list.Add(Convert.ChangeType(newValue, elementType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetNestedPropertyValue(object obj, string propertyPath, IEnumerable<object> newValues)
|
||||
{
|
||||
if (obj == null || string.IsNullOrWhiteSpace(propertyPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var properties = propertyPath.Split('.');
|
||||
object currentObject = obj;
|
||||
PropertyInfo currentProperty = null;
|
||||
|
||||
for (int i = 0; i < properties.Length - 1; i++)
|
||||
{
|
||||
if (currentObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currentProperty = currentObject.GetType().GetProperty(properties[i]);
|
||||
if (currentProperty == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currentObject = currentProperty.GetValue(currentObject);
|
||||
}
|
||||
|
||||
if (currentObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currentProperty = currentObject.GetType().GetProperty(properties.Last());
|
||||
if (currentProperty == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var propertyType = currentProperty.PropertyType;
|
||||
var elementType = propertyType.GetGenericArguments()[0];
|
||||
|
||||
var newCollection = CreateCompatibleCollection(propertyType, elementType, newValues);
|
||||
|
||||
if (newCollection != null)
|
||||
{
|
||||
currentProperty.SetValue(currentObject, newCollection);
|
||||
}
|
||||
}
|
||||
|
||||
private static Type GetUnderlyingTypeOfCollection(IEnumerable<object> currentPropertyValuesArray)
|
||||
{
|
||||
Type collectionType = currentPropertyValuesArray.GetType();
|
||||
|
||||
if (!collectionType.IsGenericType)
|
||||
{
|
||||
throw new ArgumentException("Invalid json data supplied");
|
||||
}
|
||||
|
||||
Type[] genericArguments = collectionType.GetGenericArguments();
|
||||
if (genericArguments.Length > 0)
|
||||
{
|
||||
return genericArguments[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Invalid json data supplied");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +255,39 @@ public sealed class SetAdditionalSettingsCommandLineCommand
|
||||
return;
|
||||
}
|
||||
|
||||
var propertyValueInfo = settingsConfigType.GetProperty(additionalPropertiesInfo.RootPropertyName);
|
||||
var currentPropertyValue = propertyValueInfo.GetValue(settingsConfig);
|
||||
var currentPropertyValue = GetNestedPropertyValue(settingsConfig, additionalPropertiesInfo.PropertyPath);
|
||||
|
||||
// For now, only a certain data shapes are supported
|
||||
switch (additionalPropertiesInfo.RootObjectType)
|
||||
switch (additionalPropertiesInfo.PropertyType)
|
||||
{
|
||||
case JsonValueKind.Array:
|
||||
ExecuteRootArray(settings.RootElement.EnumerateArray(), currentPropertyValue as IEnumerable<object>);
|
||||
if (currentPropertyValue == null)
|
||||
{
|
||||
currentPropertyValue = new JsonArray();
|
||||
}
|
||||
|
||||
IEnumerable<JsonElement> propertiesToSet = null;
|
||||
|
||||
// Powershell ConvertTo-Json call omits wrapping a single value in an array, so we must do it here
|
||||
if (settings.RootElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var wrapperArray = new JsonArray();
|
||||
wrapperArray.Add(settings.RootElement);
|
||||
propertiesToSet = (IEnumerable<JsonElement>)wrapperArray.GetEnumerator();
|
||||
}
|
||||
else if (settings.RootElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
propertiesToSet = settings.RootElement.EnumerateArray().AsEnumerable();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Invalid json data supplied");
|
||||
}
|
||||
|
||||
var newPropertyValue = ExecuteRootArray(propertiesToSet, currentPropertyValue as IEnumerable<object>);
|
||||
|
||||
SetNestedPropertyValue(settingsConfig, additionalPropertiesInfo.PropertyPath, newPropertyValue);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
Name,
|
||||
}
|
||||
|
||||
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, false, false, true, 0x4F);
|
||||
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0xC0);
|
||||
|
||||
public WorkspacesProperties()
|
||||
{
|
||||
|
||||
@@ -233,10 +233,10 @@ namespace ViewModelTests
|
||||
// Assert
|
||||
ImageSize newTestSize = viewModel.Sizes.First(x => x.Id == 0);
|
||||
Assert.AreEqual(newTestSize.Name, "New size 1");
|
||||
Assert.AreEqual(newTestSize.Fit, (int)ResizeFit.Fit);
|
||||
Assert.AreEqual(newTestSize.Fit, ResizeFit.Fit);
|
||||
Assert.AreEqual(newTestSize.Width, 854);
|
||||
Assert.AreEqual(newTestSize.Height, 480);
|
||||
Assert.AreEqual(newTestSize.Unit, (int)ResizeUnit.Pixel);
|
||||
Assert.AreEqual(newTestSize.Unit, ResizeUnit.Pixel);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -287,10 +287,10 @@ namespace ViewModelTests
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Test",
|
||||
Fit = (int)ResizeFit.Fit,
|
||||
Fit = ResizeFit.Fit,
|
||||
Width = 30,
|
||||
Height = 30,
|
||||
Unit = (int)ResizeUnit.Pixel,
|
||||
Unit = ResizeUnit.Pixel,
|
||||
};
|
||||
|
||||
double negativeWidth = -2.0;
|
||||
|
||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,32 @@
|
||||
// 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 Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Converters;
|
||||
|
||||
public sealed class ImageResizerFitToIntConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is ResizeFit)
|
||||
{
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is int intValue)
|
||||
{
|
||||
return (ResizeFit)intValue;
|
||||
}
|
||||
|
||||
return ResizeFit.Fill;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Converters;
|
||||
|
||||
public sealed class ImageResizerUnitToIntConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is ResizeUnit)
|
||||
{
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is int intValue)
|
||||
{
|
||||
return (ResizeUnit)intValue;
|
||||
}
|
||||
|
||||
return ResizeUnit.Centimeter;
|
||||
}
|
||||
}
|
||||
@@ -115,13 +115,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
internalSettings = new HotkeySettings();
|
||||
|
||||
this.Unloaded += ShortcutControl_Unloaded;
|
||||
hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents);
|
||||
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
this.Loaded += ShortcutControl_Loaded;
|
||||
|
||||
if (App.GetSettingsWindow() != null)
|
||||
{
|
||||
App.GetSettingsWindow().Activated += ShortcutDialog_SettingsWindow_Activated;
|
||||
}
|
||||
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
// We create the Dialog in C# because doing it in XAML is giving WinUI/XAML Island bugs when using dark theme.
|
||||
shortcutDialog = new ContentDialog
|
||||
@@ -134,11 +130,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
CloseButtonText = resourceLoader.GetString("Activation_Shortcut_Cancel"),
|
||||
DefaultButton = ContentDialogButton.Primary,
|
||||
};
|
||||
shortcutDialog.PrimaryButtonClick += ShortcutDialog_PrimaryButtonClick;
|
||||
shortcutDialog.SecondaryButtonClick += ShortcutDialog_Reset;
|
||||
shortcutDialog.RightTapped += ShortcutDialog_Disable;
|
||||
shortcutDialog.Opened += ShortcutDialog_Opened;
|
||||
shortcutDialog.Closing += ShortcutDialog_Closing;
|
||||
|
||||
AutomationProperties.SetName(EditButton, resourceLoader.GetString("Activation_Shortcut_Title"));
|
||||
|
||||
OnAllowDisableChanged(this, null);
|
||||
@@ -156,14 +150,28 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
}
|
||||
|
||||
// Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded
|
||||
if (hook != null)
|
||||
{
|
||||
hook.Dispose();
|
||||
}
|
||||
hook?.Dispose();
|
||||
|
||||
hook = null;
|
||||
}
|
||||
|
||||
private void ShortcutControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// These all belong here; because of virtualization in e.g. a ListView, the control can go through several Loaded / Unloaded cycles.
|
||||
hook?.Dispose();
|
||||
|
||||
hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents);
|
||||
|
||||
shortcutDialog.PrimaryButtonClick += ShortcutDialog_PrimaryButtonClick;
|
||||
shortcutDialog.Opened += ShortcutDialog_Opened;
|
||||
shortcutDialog.Closing += ShortcutDialog_Closing;
|
||||
|
||||
if (App.GetSettingsWindow() != null)
|
||||
{
|
||||
App.GetSettingsWindow().Activated += ShortcutDialog_SettingsWindow_Activated;
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyEventHandler(int key, bool matchValue, int matchValueCode)
|
||||
{
|
||||
VirtualKey virtualKey = (VirtualKey)key;
|
||||
|
||||
@@ -65,45 +65,47 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1">
|
||||
<ItemsControl
|
||||
Margin="12,26,12,24"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
ItemsSource="{x:Bind ViewModel.FlyoutMenuItems}"
|
||||
TabNavigation="Local">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<tkcontrols:WrapPanel HorizontalAlignment="Stretch" VerticalSpacing="12" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewModels:FlyoutMenuItem">
|
||||
<controls:FlyoutMenuButton
|
||||
AutomationProperties.Name="{x:Bind Label}"
|
||||
Click="ModuleButton_Click"
|
||||
Tag="{x:Bind Tag}"
|
||||
Visibility="{x:Bind Visible, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<controls:FlyoutMenuButton.Content>
|
||||
<TextBlock
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Label}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</controls:FlyoutMenuButton.Content>
|
||||
<controls:FlyoutMenuButton.Icon>
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<BitmapImage UriSource="{x:Bind Icon, Mode=OneWay}" />
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</controls:FlyoutMenuButton.Icon>
|
||||
<ToolTipService.ToolTip>
|
||||
<ToolTip Content="{x:Bind ToolTip}" Visibility="{x:Bind ToolTip, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
</ToolTipService.ToolTip>
|
||||
</controls:FlyoutMenuButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ScrollViewer>
|
||||
<ItemsControl
|
||||
Margin="12,26,12,24"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
ItemsSource="{x:Bind ViewModel.FlyoutMenuItems}"
|
||||
TabNavigation="Local">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<tkcontrols:WrapPanel HorizontalAlignment="Stretch" VerticalSpacing="12" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewModels:FlyoutMenuItem">
|
||||
<controls:FlyoutMenuButton
|
||||
AutomationProperties.Name="{x:Bind Label}"
|
||||
Click="ModuleButton_Click"
|
||||
Tag="{x:Bind Tag}"
|
||||
Visibility="{x:Bind Visible, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<controls:FlyoutMenuButton.Content>
|
||||
<TextBlock
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Label}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</controls:FlyoutMenuButton.Content>
|
||||
<controls:FlyoutMenuButton.Icon>
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<BitmapImage UriSource="{x:Bind Icon, Mode=OneWay}" />
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</controls:FlyoutMenuButton.Icon>
|
||||
<ToolTipService.ToolTip>
|
||||
<ToolTip Content="{x:Bind ToolTip}" Visibility="{x:Bind ToolTip, Converter={StaticResource StringVisibilityConverter}}" />
|
||||
</ToolTipService.ToolTip>
|
||||
</controls:FlyoutMenuButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid Grid.Row="2">
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
|
||||
|
||||
<tk7controls:MarkdownTextBlock x:Uid="Oobe_Workspaces_HowToUse" Background="Transparent" />
|
||||
<controls:ShortcutWithTextLabelControl x:Name="HotkeyControl" x:Uid="Oobe_Workspaces_HowToUse_Shortcut" />
|
||||
|
||||
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
@@ -37,6 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
HotkeyControl.Keys = SettingsRepository<WorkspacesSettings>.GetInstance(new SettingsUtils()).SettingsConfig.Properties.Hotkey.Value.GetKeysList();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:Microsoft.PowerToys.Settings.UI.Library"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
x:Name="RootPage"
|
||||
AutomationProperties.LandmarkType="Main"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
@@ -100,33 +102,95 @@
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="AdvancedPasteUI_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<tkcontrols:SettingsCard x:Uid="AdvancedPasteUI_Actions" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button
|
||||
x:Uid="AdvancedPasteUI_AddCustomActionButton"
|
||||
Click="AddCustomActionButton_Click"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="AdvancedPasteUI_Shortcut" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.AdvancedPasteUIShortcut, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard Visibility="Collapsed">
|
||||
<!-- There's a bug that makes it so that the first shortcut control inside an expander doesn't work. We add this dummy one so the other entries aren't affected. -->
|
||||
<TextBox />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PasteAsPlainText_Shortcut">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.PasteAsPlainTextShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PasteAsMarkdown_Shortcut">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.PasteAsMarkdownShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PasteAsJson_Shortcut">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.PasteAsJsonShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PasteAsPlainText_Shortcut">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.PasteAsPlainTextShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PasteAsMarkdown_Shortcut">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.PasteAsMarkdownShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PasteAsJson_Shortcut">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.PasteAsJsonShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<ItemsControl
|
||||
x:Name="CustomActions"
|
||||
x:Uid="CustomActions"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsTabStop="False"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActions, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AdvancedPasteCustomAction">
|
||||
<tkcontrols:SettingsCard
|
||||
Margin="0,0,0,2"
|
||||
Click="EditCustomActionButton_Click"
|
||||
Description="{x:Bind Prompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsActionIconVisible="False"
|
||||
IsClickEnabled="True">
|
||||
<tkcontrols:SettingsCard.Resources>
|
||||
<x:Double x:Key="SettingsCardActionButtonWidth">0</x:Double>
|
||||
</tkcontrols:SettingsCard.Resources>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=Shortcut, Mode=TwoWay}" />
|
||||
<ToggleSwitch
|
||||
x:Uid="Enable_CustomAction"
|
||||
AutomationProperties.HelpText="{x:Bind Name}"
|
||||
IsOn="{x:Bind IsShown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
<Button
|
||||
x:Uid="More_Options_Button"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveUp"
|
||||
Click="ReorderButtonUp_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind CanMoveUp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveDown"
|
||||
Click="ReorderButtonDown_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind CanMoveDown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="RemoveItem"
|
||||
Click="DeleteCustomActionButton_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="true" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="More_Options_ButtonTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<InfoBar
|
||||
x:Uid="AdvancedPaste_ShortcutWarning"
|
||||
IsClosable="False"
|
||||
@@ -202,5 +266,31 @@
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
<ContentDialog
|
||||
x:Name="CustomActionDialog"
|
||||
x:Uid="CustomActionDialog"
|
||||
Closed="CustomActionDialog_Closed"
|
||||
IsPrimaryButtonEnabled="{Binding IsValid, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsSecondaryButtonEnabled="True"
|
||||
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}">
|
||||
<ContentDialog.DataContext>
|
||||
<models:AdvancedPasteCustomAction />
|
||||
</ContentDialog.DataContext>
|
||||
<StackPanel Spacing="16">
|
||||
<TextBox
|
||||
x:Uid="AdvancedPasteUI_CustomAction_Name"
|
||||
Width="340"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox
|
||||
x:Uid="AdvancedPasteUI_CustomAction_Prompt"
|
||||
Width="340"
|
||||
Height="280"
|
||||
HorizontalAlignment="Left"
|
||||
AcceptsReturn="true"
|
||||
Text="{Binding Prompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||