Compare commits
99 Commits
powerscrip
...
workspaces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b99309c33 | ||
|
|
93669df118 | ||
|
|
56fabda79c | ||
|
|
70555459ab | ||
|
|
d6319516d0 | ||
|
|
53737cbe31 | ||
|
|
87a5fac4bc | ||
|
|
bf6ff579d3 | ||
|
|
d48501be9f | ||
|
|
0362e0d5fc | ||
|
|
0697cd8774 | ||
|
|
9fb18f5bfb | ||
|
|
03c97e0366 | ||
|
|
77c53e6f9a | ||
|
|
0a2e4b5253 | ||
|
|
af03fd610a | ||
|
|
0afe525f31 | ||
|
|
3f67465fc6 | ||
|
|
a43fb12d6f | ||
|
|
bc56443443 | ||
|
|
3298625b67 | ||
|
|
ae9f241ef1 | ||
|
|
67a9fa2d13 | ||
|
|
1cfc923bdb | ||
|
|
2dd802f367 | ||
|
|
a0d17406ba | ||
|
|
4a27c5d5f9 | ||
|
|
8bd5c1be6f | ||
|
|
027124f98a | ||
|
|
df2d162275 | ||
|
|
123ae05e1b | ||
|
|
7b19b4c219 | ||
|
|
2a4919fe41 | ||
|
|
70e08ddd63 | ||
|
|
c17554482c | ||
|
|
b73fd670be | ||
|
|
bcf3c98c8a | ||
|
|
a46a4437e5 | ||
|
|
5b8eaef852 | ||
|
|
d2ba3d9ae4 | ||
|
|
285db8d4a9 | ||
|
|
740e18e081 | ||
|
|
3bf682048e | ||
|
|
28a9bbe8f0 | ||
|
|
536e768cac | ||
|
|
70ff4013b9 | ||
|
|
7a04d4c270 | ||
|
|
8c434cd6f4 | ||
|
|
606e03b085 | ||
|
|
d983dbc285 | ||
|
|
fb6843b0f1 | ||
|
|
6dd1ce5dd1 | ||
|
|
9ea30ec523 | ||
|
|
d7b8fe006d | ||
|
|
c777fcc1e4 | ||
|
|
28e078897a | ||
|
|
35d04b6fd3 | ||
|
|
8a8887eaf8 | ||
|
|
27633f6f7d | ||
|
|
7c194bd108 | ||
|
|
64f1243bdf | ||
|
|
1539e6e061 | ||
|
|
ecc737d0e5 | ||
|
|
b72ae3b8b5 | ||
|
|
e055f303e1 | ||
|
|
8fcdc199a0 | ||
|
|
b7bb10f5f8 | ||
|
|
e1074bc835 | ||
|
|
2390aacbfc | ||
|
|
f85b25696f | ||
|
|
b620c40f75 | ||
|
|
5eb0591c58 | ||
|
|
3a0b5df3af | ||
|
|
23c8bcc9be | ||
|
|
5a236c2e4c | ||
|
|
f061ed9ac6 | ||
|
|
a6ab4ebd3e | ||
|
|
a361c32911 | ||
|
|
54ac6f7c96 | ||
|
|
519bd5398f | ||
|
|
e845b000d2 | ||
|
|
60b78051fe | ||
|
|
c2fcf06391 | ||
|
|
7b7a54a73f | ||
|
|
c15e28bbca | ||
|
|
c3fb02567c | ||
|
|
c1e623cba9 | ||
|
|
f81758d4e7 | ||
|
|
c333ed96c0 | ||
|
|
fb46aaa913 | ||
|
|
de9b92e94a | ||
|
|
2c46c8854c | ||
|
|
3a30809c80 | ||
|
|
a854e2cecc | ||
|
|
525097b54c | ||
|
|
7b4f89bfa6 | ||
|
|
31bf0aaf59 | ||
|
|
127eab3eab | ||
|
|
76303d2f52 |
5
.github/actions/spell-check/expect.txt
vendored
@@ -135,6 +135,7 @@ BITMAPINFO
|
||||
BITMAPINFOHEADER
|
||||
BITSPERPEL
|
||||
BITSPIXEL
|
||||
Blackmagic
|
||||
bla
|
||||
BLENDFUNCTION
|
||||
blittable
|
||||
@@ -539,6 +540,7 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
Fairlight
|
||||
FARPROC
|
||||
fdw
|
||||
fdx
|
||||
@@ -1232,6 +1234,8 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
Nouveaut
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
@@ -2178,6 +2182,7 @@ xclip
|
||||
xcopy
|
||||
xdf
|
||||
xfd
|
||||
xhair
|
||||
xmp
|
||||
Xoshiro
|
||||
xsi
|
||||
|
||||
6
.github/copilot-instructions.md
vendored
@@ -30,6 +30,12 @@ These are auto-applied based on file location:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
|
||||
## Shortcut Guide V2 Manifests
|
||||
|
||||
When creating or editing Shortcut Guide keyboard shortcut manifest files, follow the schema and naming conventions in the spec:
|
||||
|
||||
- [WinGet Manifest Keyboard Shortcuts schema](<../doc/specs/WinGet Manifest Keyboard Shortcuts schema.md>) – manifest file format, field definitions, file naming, and the `+` prefix convention for apps without a WinGet package
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
- [Architecture](../doc/devdocs/core/architecture.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: wpf-to-winui3-migration
|
||||
description: Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.
|
||||
description: 'Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.'
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
|
||||
7
.github/workflows/auto-labeler.yml
vendored
@@ -73,6 +73,13 @@ jobs:
|
||||
|
||||
const itemType = issue.pull_request ? 'Pull request' : 'Issue';
|
||||
|
||||
// Skip pull requests that already have labels applied.
|
||||
if (issue.pull_request && issue.labels && issue.labels.length > 0) {
|
||||
const existingLabels = issue.labels.map(l => l.name).join(', ');
|
||||
console.log(`${itemType} #${issueNumber} already has labels (${existingLabels}); skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = issue.title ?? '';
|
||||
const body = issue.body ?? '';
|
||||
|
||||
|
||||
@@ -232,8 +232,8 @@
|
||||
"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
"PowerToys.WorkspacesLauncher.exe",
|
||||
"PowerToys.WorkspacesWindowArranger.exe",
|
||||
"PowerToys.WorkspacesEditor.exe",
|
||||
"PowerToys.WorkspacesEditor.dll",
|
||||
"WinUI3Apps\\PowerToys.WorkspacesEditor.exe",
|
||||
"WinUI3Apps\\PowerToys.WorkspacesEditor.dll",
|
||||
"PowerToys.WorkspacesLauncherUI.exe",
|
||||
"PowerToys.WorkspacesLauncherUI.dll",
|
||||
"PowerToys.WorkspacesModuleInterface.dll",
|
||||
|
||||
@@ -4,6 +4,29 @@
|
||||
<Import Project="$(MSBuildCachePackageRoot)\build\$(MSBuildCachePackageName).targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
|
||||
<Import Project="$(MSBuildCacheSharedCompilationPackageRoot)\build\Microsoft.MSBuildCache.SharedCompilation.targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
|
||||
|
||||
<!--
|
||||
Onboarding guard: PowerToys has deeply nested source paths that exceed the legacy
|
||||
260-character MAX_PATH limit. Without Windows long path support enabled, the build
|
||||
fails with cryptic "path too long" / "could not find file" errors that are hard for
|
||||
new contributors to diagnose. Detect the missing registry setting up front and emit a
|
||||
clear, actionable error before the confusing failures occur.
|
||||
|
||||
- Covers both Visual Studio (Ctrl+Shift+B) and the command-line build scripts.
|
||||
- Runs only during real builds (skips design-time/IntelliSense passes).
|
||||
- Bypass with /p:SkipLongPathsCheck=true if you know what you're doing.
|
||||
See tools\build\setup-dev-environment.ps1 to enable everything automatically.
|
||||
-->
|
||||
<Target Name="EnsureLongPathsEnabled"
|
||||
BeforeTargets="PrepareForBuild"
|
||||
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipLongPathsCheck)' != 'true' and '$(OS)' == 'Windows_NT'">
|
||||
<PropertyGroup>
|
||||
<_LongPathsEnabled>$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem', 'LongPathsEnabled', null, RegistryView.Registry64))</_LongPathsEnabled>
|
||||
</PropertyGroup>
|
||||
<Error Condition="'$(_LongPathsEnabled)' != '1'"
|
||||
Code="PTLONGPATH"
|
||||
Text="Windows long path support is not enabled. PowerToys source paths exceed the 260-character MAX_PATH limit, so the build will fail with cryptic 'path too long' errors. Fix it by running (from an elevated PowerShell): .\tools\build\setup-dev-environment.ps1 -- or set HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1 (DWORD) and restart Windows. To bypass this check, build with /p:SkipLongPathsCheck=true." />
|
||||
</Target>
|
||||
|
||||
<!-- Override ManifestTool to the x64 host tool under WindowsSdkDir for all projects once the SDK path is known. -->
|
||||
<PropertyGroup Label="ManifestToolOverride">
|
||||
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
@@ -38,7 +38,7 @@
|
||||
<PackageVersion Include="Mages" Version="3.0.0" />
|
||||
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.102" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
@@ -64,7 +64,7 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.4022.49" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
@@ -76,7 +76,7 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.1.0" />
|
||||
@@ -151,4 +151,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
64
NOTICE.md
@@ -12,6 +12,7 @@ This software incorporates material from third parties.
|
||||
- Peek
|
||||
- PowerDisplay
|
||||
- Registry Preview
|
||||
- ZoomIt
|
||||
|
||||
## Utility: Color Picker
|
||||
|
||||
@@ -1549,6 +1550,69 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Utility: ZoomIt
|
||||
|
||||
### libwebp
|
||||
|
||||
ZoomIt uses libwebp to encode screenshots in the WebP image format.
|
||||
|
||||
**Source**: <https://github.com/webmproject/libwebp>
|
||||
|
||||
BSD-3-Clause License
|
||||
|
||||
Copyright (c) 2010, Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of Google nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"These implementations" means the copyrightable works that implement the WebM
|
||||
codecs distributed by Google as part of the WebM Project.
|
||||
|
||||
Google hereby grants to you a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent license to
|
||||
make, have made, use, offer to sell, sell, import, transfer, and otherwise
|
||||
run, modify and propagate the contents of these implementations of WebM, where
|
||||
such license applies only to those patent claims, both currently owned by
|
||||
Google and acquired in the future, licensable by Google that are necessarily
|
||||
infringed by these implementations of WebM. This grant does not include claims
|
||||
that would be infringed only as a consequence of further modification of these
|
||||
implementations. If you or your agent or exclusive licensee institute or order
|
||||
or agree to the institution of patent litigation or any other patent
|
||||
enforcement activity against any entity (including a cross-claim or
|
||||
counterclaim in a lawsuit) alleging that any of these implementations of WebM
|
||||
or any code incorporated within any of these implementations of WebM
|
||||
constitute direct or contributory patent infringement, or inducement of
|
||||
patent infringement, then any patent rights granted to you under this License
|
||||
for these implementations of WebM shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
## NuGet Packages used by PowerToys
|
||||
|
||||
- AdaptiveCards.ObjectModel.WinUI3
|
||||
|
||||
@@ -1022,7 +1022,7 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj">
|
||||
<Project Path="src/modules/Workspaces/WorkspacesEditor.WinUI/WorkspacesEditor.WinUI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# [CleanUp_tool](/tools/CleanUp_tool/) and [CleanUp_tool_powershell_script](/tools/CleanUp_tool_powershell_script/CleanUp_tool.ps1)
|
||||
|
||||
This tool, respective this powershell script, is used to clean up the PowerToys installation. It cleans the `AppData` folder and the registry.
|
||||
|
||||
This tool is currently very outdated and just cleans up the registry keys of some few modules.
|
||||
@@ -10,7 +10,6 @@ Following tools are currently available:
|
||||
|
||||
* [BugReportTool](bug-report-tool.md) - A tool to collect logs and system information for bug reports.
|
||||
* [Build tools](build-tools.md) - A set of scripts that help building PowerToys.
|
||||
* [Clean up tool](clean-up-tool.md) - A tool to clean up the PowerToys installation.
|
||||
* [Monitor info report](monitor-info-report.md) - A small diagnostic tool which helps identifying WinAPI bugs related to the physical monitor detection.
|
||||
* [project template](/tools/project_template/README.md) - A Visual Studio project template for a new PowerToys project.
|
||||
* [StylesReportTool](styles-report-tool.md) - A tool to collect information about an open window.
|
||||
|
||||
@@ -1619,7 +1619,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
L"WinUI3Apps\\PowerToys.WorkspacesEditor.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"Microsoft.CmdPal.UI.exe",
|
||||
L"Microsoft.CmdPal.Ext.PowerToys.exe",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -29,8 +29,30 @@ namespace Microsoft.PowerToys.Common.UI.Controls.Backdrops;
|
||||
/// </remarks>
|
||||
public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Kind"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty KindProperty = DependencyProperty.Register(
|
||||
nameof(Kind),
|
||||
typeof(DesktopAcrylicKind),
|
||||
typeof(AlwaysActiveDesktopAcrylicBackdrop),
|
||||
new PropertyMetadata(DesktopAcrylicKind.Default, OnKindChanged));
|
||||
|
||||
private readonly Dictionary<ICompositionSupportsSystemBackdrop, BackdropTarget> _targets = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desktop acrylic material variant to render. Defaults to
|
||||
/// <see cref="DesktopAcrylicKind.Default"/> (the standard, more opaque
|
||||
/// acrylic); <see cref="DesktopAcrylicKind.Thin"/> renders a lighter, more
|
||||
/// translucent material and <see cref="DesktopAcrylicKind.Base"/> the base
|
||||
/// material. Changing this updates any live backdrop targets immediately.
|
||||
/// </summary>
|
||||
public DesktopAcrylicKind Kind
|
||||
{
|
||||
get => (DesktopAcrylicKind)GetValue(KindProperty);
|
||||
set => SetValue(KindProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
|
||||
{
|
||||
base.OnTargetConnected(connectedTarget, xamlRoot);
|
||||
@@ -41,7 +63,10 @@ public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
Theme = ResolveTheme(xamlRoot),
|
||||
};
|
||||
|
||||
var controller = new DesktopAcrylicController();
|
||||
var controller = new DesktopAcrylicController
|
||||
{
|
||||
Kind = Kind,
|
||||
};
|
||||
controller.SetSystemBackdropConfiguration(configuration);
|
||||
controller.AddSystemBackdropTarget(connectedTarget);
|
||||
|
||||
@@ -70,6 +95,17 @@ public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnKindChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var self = (AlwaysActiveDesktopAcrylicBackdrop)d;
|
||||
var kind = (DesktopAcrylicKind)e.NewValue;
|
||||
|
||||
foreach (var target in self._targets.Values)
|
||||
{
|
||||
target.Controller.Kind = kind;
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemBackdropTheme ResolveTheme(XamlRoot xamlRoot) =>
|
||||
xamlRoot.Content is FrameworkElement rootElement
|
||||
? rootElement.ActualTheme switch
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
xmlns:backdrops="using:Microsoft.PowerToys.Common.UI.Controls.Backdrops"
|
||||
xmlns:local="using:Microsoft.PowerToys.Common.UI.Controls">
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultTransparentCardStyle}" TargetType="local:TransparentCard" />
|
||||
<Style BasedOn="{StaticResource DefaultTransientSurfaceStyle}" TargetType="local:TransientSurface" />
|
||||
|
||||
<Style x:Key="DefaultTransparentCardStyle" TargetType="local:TransparentCard">
|
||||
<Style x:Key="DefaultTransientSurfaceStyle" TargetType="local:TransientSurface">
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource SurfaceStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
@@ -16,7 +16,7 @@
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:TransparentCard">
|
||||
<ControlTemplate TargetType="local:TransientSurface">
|
||||
<Grid
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
@@ -27,7 +27,7 @@
|
||||
</Grid.Shadow>
|
||||
<SystemBackdropElement CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<SystemBackdropElement.SystemBackdrop>
|
||||
<backdrops:AlwaysActiveDesktopAcrylicBackdrop />
|
||||
<backdrops:AlwaysActiveDesktopAcrylicBackdrop Kind="{TemplateBinding AcrylicKind}" />
|
||||
</SystemBackdropElement.SystemBackdrop>
|
||||
</SystemBackdropElement>
|
||||
<ContentPresenter
|
||||
@@ -41,5 +41,4 @@
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,467 @@
|
||||
// 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 CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A floating, self-animating "pseudo window" surface for transient PowerToys
|
||||
/// overlays (toasts, banners, indicators). It looks like a control but behaves
|
||||
/// like a lightweight window: it provides the PowerToys-standard chrome — 1 px
|
||||
/// border in <c>SurfaceStrokeColorDefaultBrush</c>, 8 px corner radius, a
|
||||
/// <c>ThemeShadow</c>, and an always-active desktop acrylic backdrop — and owns
|
||||
/// its own show/hide animations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Designed to be declared as the root content of a
|
||||
/// <see cref="TransparentWindow"/>, which stays animation-agnostic. Call
|
||||
/// <see cref="SubscribeTo"/> once (e.g. from the hosting window's constructor)
|
||||
/// to wire this surface to the window's <see cref="TransparentWindow.Showing"/> /
|
||||
/// <see cref="TransparentWindow.Hiding"/> events. From then on the surface
|
||||
/// animates itself in/out whenever the window is shown or hidden, and uses the
|
||||
/// <see cref="TransparentWindow.Hiding"/> deferral to keep the window visible
|
||||
/// until its out-animation finishes.</para>
|
||||
/// <para>The show transition comes from the window's
|
||||
/// <see cref="TransparentWindow.Show(Transition)"/> call (or from
|
||||
/// <see cref="ShowTransition"/> when shown without one); the hide transition
|
||||
/// always comes from <see cref="HideTransition"/>. Animations target the
|
||||
/// surface itself, so the entire surface (border, acrylic, shadow, inner
|
||||
/// content) animates as one. Apps that want a different look supply their own
|
||||
/// <c>Style TargetType="TransientSurface"</c> in resources — the standard WinUI
|
||||
/// restyle path.</para>
|
||||
/// </remarks>
|
||||
public sealed partial class TransientSurface : ContentControl
|
||||
{
|
||||
private const float ShadowDepth = 32f;
|
||||
private const double SlideInOffset = 24;
|
||||
private const double SlideOutOffset = 12;
|
||||
|
||||
// "Pop" transition: scale between 96% and 100% (a subtle 4% grow). Following
|
||||
// Fluent motion guidance the scale uses a decelerate (EaseOut) curve; the
|
||||
// fade is kept fast so the surface reads as an instant, light pop.
|
||||
//
|
||||
// The fade must run at least as long as the scale: if the scale outlasted the
|
||||
// fade, the surface would reach full opacity while still visibly growing,
|
||||
// which reads as a "resize" rather than a pop. Keeping the fade >= the scale
|
||||
// hides the growth under the opacity ramp, so by the time it is fully opaque
|
||||
// it is already at 100% size.
|
||||
private const float PopScaleFrom = 0.96f;
|
||||
private const double PopFadeShowMs = 180;
|
||||
private const double PopScaleShowMs = 150;
|
||||
private const double PopFadeHideMs = 120;
|
||||
private const double PopScaleHideMs = 120;
|
||||
|
||||
public static readonly DependencyProperty ShowTransitionProperty = DependencyProperty.Register(
|
||||
nameof(ShowTransition),
|
||||
typeof(Transition),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(Transition.None, OnTransitionChanged));
|
||||
|
||||
public static readonly DependencyProperty HideTransitionProperty = DependencyProperty.Register(
|
||||
nameof(HideTransition),
|
||||
typeof(Transition),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(Transition.None, OnTransitionChanged));
|
||||
|
||||
public static readonly DependencyProperty AcrylicKindProperty = DependencyProperty.Register(
|
||||
nameof(AcrylicKind),
|
||||
typeof(DesktopAcrylicKind),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(DesktopAcrylicKind.Thin));
|
||||
|
||||
private readonly DispatcherQueueTimer _hideCompletedTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
|
||||
private readonly ImplicitAnimationSet _noAnimations = new();
|
||||
|
||||
private ImplicitAnimationSet _showAnimations = new();
|
||||
private ImplicitAnimationSet _hideAnimations = new();
|
||||
private bool _hasCustomShowAnimations;
|
||||
private bool _hasCustomHideAnimations;
|
||||
private Action? _abandonPendingHide;
|
||||
|
||||
public TransientSurface()
|
||||
{
|
||||
DefaultStyleKey = typeof(TransientSurface);
|
||||
|
||||
RebuildDefaultAnimations();
|
||||
|
||||
// Pin the scale center to the surface's center so the "Pop" transition
|
||||
// grows/shrinks from the middle, not the top-left corner. An expression
|
||||
// animation bound to the visual's own size keeps the center correct from
|
||||
// the very first frame (a SizeChanged handler would race the show
|
||||
// animation and let the first pop scale from 0,0).
|
||||
PinScaleCenter();
|
||||
|
||||
// Start hidden so the first Show() animates in from the configured pose.
|
||||
Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after <see cref="Hide"/> once the longest animation in
|
||||
/// <see cref="HideAnimations"/> (delay + duration) has completed.
|
||||
/// </summary>
|
||||
public event EventHandler? HideCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transition played when the surface is shown without an
|
||||
/// explicit one (see <see cref="Show()"/>). Defaults to
|
||||
/// <see cref="Transition.None"/>, which plays no animation at all (the
|
||||
/// surface appears instantly); a directional value adds a fade plus a slide
|
||||
/// in from that edge, and <see cref="Transition.Pop"/> a fade plus a subtle
|
||||
/// scale-up. Changing this regenerates the default <see cref="ShowAnimations"/>
|
||||
/// unless it has been set explicitly.
|
||||
/// </summary>
|
||||
public Transition ShowTransition
|
||||
{
|
||||
get => (Transition)GetValue(ShowTransitionProperty);
|
||||
set => SetValue(ShowTransitionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transition played when the surface is hidden (see
|
||||
/// <see cref="Hide"/>). Defaults to <see cref="Transition.None"/>, which
|
||||
/// plays no animation at all (the surface disappears instantly); a
|
||||
/// directional value adds a fade plus a slide out toward that edge, and
|
||||
/// <see cref="Transition.Pop"/> a fade plus a subtle scale-down. Changing
|
||||
/// this regenerates the default <see cref="HideAnimations"/> unless it has
|
||||
/// been set explicitly.
|
||||
/// </summary>
|
||||
public Transition HideTransition
|
||||
{
|
||||
get => (Transition)GetValue(HideTransitionProperty);
|
||||
set => SetValue(HideTransitionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desktop acrylic material variant painted behind the
|
||||
/// surface. Defaults to <see cref="DesktopAcrylicKind.Thin"/> (a lighter,
|
||||
/// more translucent material); set <see cref="DesktopAcrylicKind.Default"/>
|
||||
/// for the standard, more opaque acrylic or <see cref="DesktopAcrylicKind.Base"/>
|
||||
/// for the base material. Has no effect when a custom template without the
|
||||
/// default acrylic backdrop is applied.
|
||||
/// </summary>
|
||||
public DesktopAcrylicKind AcrylicKind
|
||||
{
|
||||
get => (DesktopAcrylicKind)GetValue(AcrylicKindProperty);
|
||||
set => SetValue(AcrylicKindProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played when <see cref="Show()"/> flips the
|
||||
/// surface to <see cref="Visibility.Visible"/>. Defaults to the animation
|
||||
/// derived from <see cref="ShowTransition"/>. Assigning a value marks the set
|
||||
/// as custom so <see cref="ShowTransition"/> no longer overwrites it.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet ShowAnimations
|
||||
{
|
||||
get => _showAnimations;
|
||||
set
|
||||
{
|
||||
_showAnimations = value ?? new ImplicitAnimationSet();
|
||||
_hasCustomShowAnimations = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played when <see cref="Hide"/> flips the
|
||||
/// surface to <see cref="Visibility.Collapsed"/>. Defaults to the animation
|
||||
/// derived from <see cref="HideTransition"/>. Assigning a value marks the set
|
||||
/// as custom so <see cref="HideTransition"/> no longer overwrites it.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet HideAnimations
|
||||
{
|
||||
get => _hideAnimations;
|
||||
set
|
||||
{
|
||||
_hideAnimations = value ?? new ImplicitAnimationSet();
|
||||
_hasCustomHideAnimations = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wires this surface to a hosting <see cref="TransparentWindow"/> so it
|
||||
/// animates itself in and out in response to the window's
|
||||
/// <see cref="TransparentWindow.Showing"/> / <see cref="TransparentWindow.Hiding"/>
|
||||
/// events. Call this once after the surface has been set as (or placed within)
|
||||
/// the window's content.
|
||||
/// </summary>
|
||||
/// <param name="host">The window whose show/hide transitions drive this surface.</param>
|
||||
public void SubscribeTo(TransparentWindow host)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
|
||||
host.Showing += OnHostShowing;
|
||||
host.Hiding += OnHostHiding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the surface to its hidden pose and flips it to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays,
|
||||
/// using <paramref name="transition"/> as the show transition.
|
||||
/// </summary>
|
||||
/// <param name="transition">The transition to play when showing.</param>
|
||||
public void Show(Transition transition)
|
||||
{
|
||||
ShowTransition = transition;
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the surface to its hidden pose and flips it to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays.
|
||||
/// Repeated calls re-trigger the show animation cleanly and cancel any
|
||||
/// pending <see cref="HideCompleted"/> notification.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
_hideCompletedTimer.Stop();
|
||||
|
||||
// If a hide from a previous cycle is still in flight, abandon it: drop its
|
||||
// pending HideCompleted handler so the outstanding deferral is never
|
||||
// completed. We are showing again, so the host must keep the window
|
||||
// visible instead of later hiding it for this interrupted cycle.
|
||||
_abandonPendingHide?.Invoke();
|
||||
_abandonPendingHide = null;
|
||||
|
||||
// Attach the show animation and detach any hide animation: when Show() is
|
||||
// called while the surface is still visible, the Collapsed -> Visible
|
||||
// restart below would otherwise play the hide animation (a fade/scale out)
|
||||
// immediately before the show, producing a visible flash. The real hide
|
||||
// animation is re-attached just-in-time in Hide().
|
||||
Implicit.SetShowAnimations(this, _showAnimations);
|
||||
Implicit.SetHideAnimations(this, _noAnimations);
|
||||
|
||||
// Reset to the hidden pose so the show animation always animates from the
|
||||
// configured starting frame.
|
||||
Visibility = Visibility.Collapsed;
|
||||
Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips the surface to <see cref="Visibility.Collapsed"/> so
|
||||
/// <see cref="HideAnimations"/> plays, then raises <see cref="HideCompleted"/>
|
||||
/// once the longest animation in <see cref="HideAnimations"/> (delay +
|
||||
/// duration) has completed.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
// Attach the hide animation just before collapsing (Show() detaches it to
|
||||
// avoid a flash when re-showing an already-visible surface).
|
||||
Implicit.SetHideAnimations(this, _hideAnimations);
|
||||
|
||||
Visibility = Visibility.Collapsed;
|
||||
|
||||
_hideCompletedTimer.Debounce(
|
||||
() => HideCompleted?.Invoke(this, EventArgs.Empty),
|
||||
interval: GetAnimationSetTotalDuration(_hideAnimations),
|
||||
immediate: false);
|
||||
}
|
||||
|
||||
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((TransientSurface)d).RebuildDefaultAnimations();
|
||||
}
|
||||
|
||||
private static TimeSpan GetAnimationSetTotalDuration(ImplicitAnimationSet set)
|
||||
{
|
||||
TimeSpan longest = TimeSpan.Zero;
|
||||
foreach (var animation in set)
|
||||
{
|
||||
if (animation is Animation anim)
|
||||
{
|
||||
var total = (anim.Delay ?? TimeSpan.Zero) + (anim.Duration ?? TimeSpan.Zero);
|
||||
if (total > longest)
|
||||
{
|
||||
longest = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
private static (string? ShowFrom, string? HideTo) GetSlideOffsets(Transition transition) => transition switch
|
||||
{
|
||||
Transition.Bottom => ($"0,{SlideInOffset},{ShadowDepth}", $"0,{SlideOutOffset},{ShadowDepth}"),
|
||||
Transition.Top => ($"0,{-SlideInOffset},{ShadowDepth}", $"0,{-SlideOutOffset},{ShadowDepth}"),
|
||||
Transition.Left => ($"{-SlideInOffset},0,{ShadowDepth}", $"{-SlideOutOffset},0,{ShadowDepth}"),
|
||||
Transition.Right => ($"{SlideInOffset},0,{ShadowDepth}", $"{SlideOutOffset},0,{ShadowDepth}"),
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
private void OnHostShowing(TransparentWindow sender, ShowingEventArgs e)
|
||||
{
|
||||
if (e.Transition is Transition transition)
|
||||
{
|
||||
Show(transition);
|
||||
}
|
||||
else
|
||||
{
|
||||
Show();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHostHiding(TransparentWindow sender, HidingEventArgs e)
|
||||
{
|
||||
// Take a deferral so the host keeps its window visible until our
|
||||
// out-animation has finished, then complete it from HideCompleted.
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
void OnHideCompleted(object? s, EventArgs args)
|
||||
{
|
||||
HideCompleted -= OnHideCompleted;
|
||||
_abandonPendingHide = null;
|
||||
deferral.Complete();
|
||||
}
|
||||
|
||||
// Let a subsequent Show() cancel this hide cleanly: unsubscribe the
|
||||
// handler so the deferral is never completed (the window stays visible)
|
||||
// rather than firing AppWindow.Hide for an interrupted cycle.
|
||||
_abandonPendingHide = () => HideCompleted -= OnHideCompleted;
|
||||
|
||||
HideCompleted += OnHideCompleted;
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void RebuildDefaultAnimations()
|
||||
{
|
||||
if (!_hasCustomShowAnimations)
|
||||
{
|
||||
_showAnimations = BuildShowAnimations(ShowTransition);
|
||||
}
|
||||
|
||||
if (!_hasCustomHideAnimations)
|
||||
{
|
||||
_hideAnimations = BuildHideAnimations(HideTransition);
|
||||
}
|
||||
}
|
||||
|
||||
private void PinScaleCenter()
|
||||
{
|
||||
var visual = ElementCompositionPreview.GetElementVisual(this);
|
||||
var center = visual.Compositor.CreateExpressionAnimation("Vector3(this.Target.Size.X * 0.5, this.Target.Size.Y * 0.5, 0)");
|
||||
visual.StartAnimation("CenterPoint", center);
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildShowAnimations(Transition transition)
|
||||
{
|
||||
var animations = new ImplicitAnimationSet();
|
||||
|
||||
if (transition == Transition.None)
|
||||
{
|
||||
// No animation at all.
|
||||
return animations;
|
||||
}
|
||||
|
||||
if (transition == Transition.Pop)
|
||||
{
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(PopFadeShowMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new ScaleAnimation
|
||||
{
|
||||
From = $"{PopScaleFrom},{PopScaleFrom},1",
|
||||
To = "1,1,1",
|
||||
Duration = TimeSpan.FromMilliseconds(PopScaleShowMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
var (slideFrom, _) = GetSlideOffsets(transition);
|
||||
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(200),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new TranslationAnimation
|
||||
{
|
||||
From = slideFrom,
|
||||
To = $"0,0,{ShadowDepth}",
|
||||
Duration = TimeSpan.FromMilliseconds(250),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildHideAnimations(Transition transition)
|
||||
{
|
||||
var animations = new ImplicitAnimationSet();
|
||||
|
||||
if (transition == Transition.None)
|
||||
{
|
||||
// No animation at all.
|
||||
return animations;
|
||||
}
|
||||
|
||||
if (transition == Transition.Pop)
|
||||
{
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(PopFadeHideMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new ScaleAnimation
|
||||
{
|
||||
From = "1,1,1",
|
||||
To = $"{PopScaleFrom},{PopScaleFrom},1",
|
||||
Duration = TimeSpan.FromMilliseconds(PopScaleHideMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
var (_, slideTo) = GetSlideOffsets(transition);
|
||||
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new TranslationAnimation
|
||||
{
|
||||
From = $"0,0,{ShadowDepth}",
|
||||
To = slideTo,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A show or hide transition a surface (e.g. <see cref="TransientSurface"/>)
|
||||
/// plays when it is shown or hidden. The directional values describe an edge —
|
||||
/// interpreted as <em>in from</em> that edge on show and <em>out toward</em> it
|
||||
/// on hide — while <see cref="None"/> and <see cref="Pop"/> are non-directional.
|
||||
/// </summary>
|
||||
public enum Transition
|
||||
{
|
||||
/// <summary>No animation; the surface appears and disappears instantly.</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Slide from the left edge (in from on show, out toward on hide).</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Slide from the top edge (in from on show, out toward on hide).</summary>
|
||||
Top,
|
||||
|
||||
/// <summary>Slide from the right edge (in from on show, out toward on hide).</summary>
|
||||
Right,
|
||||
|
||||
/// <summary>Slide from the bottom edge (in from on show, out toward on hide).</summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// A subtle "pop": a quick fade combined with a small scale between 96% and
|
||||
/// 100% from the surface's center. Stays in place — no slide.
|
||||
/// </summary>
|
||||
Pop,
|
||||
}
|
||||
@@ -1,27 +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 Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A floating "card" surface for transient PowerToys overlays (toasts,
|
||||
/// banners, indicators). Provides the PowerToys-standard chrome — 1 px
|
||||
/// border in <c>SurfaceStrokeColorDefaultBrush</c>, 8 px corner radius,
|
||||
/// a <c>ThemeShadow</c>, and an always-active desktop acrylic backdrop —
|
||||
/// via a default <see cref="Microsoft.UI.Xaml.Controls.ControlTemplate"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives inside a <see cref="Window.TransparentWindow"/>. Apps that want a
|
||||
/// different look supply their own <c>Style TargetType="TransparentCard"</c>
|
||||
/// in resources — the standard WinUI restyle path.
|
||||
/// </remarks>
|
||||
public sealed partial class TransparentCard : ContentControl
|
||||
{
|
||||
public TransparentCard()
|
||||
{
|
||||
DefaultStyleKey = typeof(TransparentCard);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,6 @@
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/TransparentCard/TransparentCard.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/TransientSurface/TransientSurface.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -9,7 +9,7 @@ using Microsoft.UI.Windowing;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Flyout;
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper for positioning and sizing flyout-style WinUI 3 windows
|
||||
@@ -187,16 +187,13 @@ public static partial class FlyoutWindowHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two-step move that avoids WM_DPICHANGED double-scaling. First teleports a 1×1
|
||||
/// window into the target display (which may trigger an auto-rescale, but on a 1×1
|
||||
/// rect the effect is invisible). Then sets the real position+size while the window
|
||||
/// is already on the target monitor — no DPI boundary crossing, so WinUI's auto
|
||||
/// handler doesn't fire and overwrite our computed rect.
|
||||
///
|
||||
/// Skips the teleport when the window is already on the target display, since there
|
||||
/// is no boundary to cross.
|
||||
/// Move and resize <paramref name="window"/> to <paramref name="finalRect"/> (absolute
|
||||
/// screen physical-pixel coordinates) on <paramref name="targetDisplay"/>. Performs a
|
||||
/// two-step move that avoids WM_DPICHANGED double-scaling: first a 1×1 teleport into the
|
||||
/// target display (invisible at that size), then the real position+size while the window
|
||||
/// is already on that monitor. Skips the teleport when already on the target display.
|
||||
/// </summary>
|
||||
private static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
|
||||
public static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
|
||||
{
|
||||
var currentDisplay = DisplayArea.GetFromWindowId(window.AppWindow.Id, DisplayAreaFallback.Nearest);
|
||||
bool needsTeleport = currentDisplay is null || currentDisplay.DisplayId.Value != targetDisplay.DisplayId.Value;
|
||||
@@ -1,290 +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.Runtime.InteropServices;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable transparent host window for transient overlays
|
||||
/// (toasts, banners, indicators) that should not steal foreground.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The constructor applies all of the boilerplate that PowerToys overlays
|
||||
/// currently hand-roll:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Strip the native frame and caption (<c>WS_THICKFRAME</c> etc.).</item>
|
||||
/// <item>Disable the Win11 1-pixel DWM border and corner rounding.</item>
|
||||
/// <item>Mark the window as a tool window so it stays out of the taskbar and Alt-Tab.</item>
|
||||
/// <item>Extend content into the title bar and collapse the title bar.</item>
|
||||
/// </list>
|
||||
/// <para>The visible chrome (acrylic + border + corner radius + shadow) lives
|
||||
/// in a <see cref="TransparentCard"/> that the constructor assigns to
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/>. Consumers supply their own
|
||||
/// UI via <see cref="InnerContent"/> — which is the XAML default-content slot
|
||||
/// thanks to <see cref="ContentPropertyAttribute"/> — so a derived window can
|
||||
/// be written as <c><common:TransparentWindow><TextBlock/></common:TransparentWindow></c>.</para>
|
||||
/// <para>Transparency is achieved with a <see cref="TransparentTintBackdrop"/>
|
||||
/// system backdrop so the area outside the <see cref="TransparentCard"/> is
|
||||
/// fully see-through. That buffer area is NOT click-through, so consumers
|
||||
/// should keep it as small as possible (just enough to give the card's
|
||||
/// shadow + slide animation room to breathe — roughly 24 px on each side).</para>
|
||||
/// <para><see cref="Show"/> and <see cref="Hide"/> coordinate <c>SW_SHOWNA</c>
|
||||
/// (no-activate), the <see cref="Microsoft.UI.Xaml.UIElement.Visibility"/>
|
||||
/// toggle on the card, and a debounced
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> sized from the longest
|
||||
/// animation in <see cref="HideAnimations"/>. Animations target the card so
|
||||
/// the entire surface (border, acrylic, shadow, inner content) slides as one.</para>
|
||||
/// </remarks>
|
||||
[ContentProperty(Name = nameof(InnerContent))]
|
||||
public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
private const uint DwmwaColorNone = 0xFFFFFFFE;
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwaBorderColor = 34;
|
||||
private const int DwmwcpDoNotRound = 1;
|
||||
|
||||
private const int GwlExStyle = -20;
|
||||
private const int WsExToolWindow = 0x00000080;
|
||||
|
||||
private const int SwShowNa = 8;
|
||||
|
||||
private readonly DispatcherQueueTimer _hideCloseTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly nint _hwnd;
|
||||
private readonly TransparentCard _card;
|
||||
|
||||
private ImplicitAnimationSet _showAnimations;
|
||||
private ImplicitAnimationSet _hideAnimations;
|
||||
|
||||
public TransparentWindow()
|
||||
{
|
||||
AppWindow.Hide();
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
|
||||
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
|
||||
unsafe
|
||||
{
|
||||
uint borderColor = DwmwaColorNone;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaBorderColor, &borderColor, sizeof(uint));
|
||||
|
||||
int cornerPref = DwmwcpDoNotRound;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaWindowCornerPreference, &cornerPref, sizeof(int));
|
||||
}
|
||||
|
||||
ApplyExStyleBit(WsExToolWindow, true);
|
||||
|
||||
_showAnimations = BuildDefaultShowAnimations();
|
||||
_hideAnimations = BuildDefaultHideAnimations();
|
||||
|
||||
_card = new TransparentCard();
|
||||
Content = _card;
|
||||
|
||||
SystemBackdrop = new TransparentTintBackdrop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="TransparentCard"/> that provides the window's
|
||||
/// visible chrome (acrylic + border + shadow). Consumers can configure
|
||||
/// its layout (e.g. <c>HorizontalAlignment</c>, <c>VerticalAlignment</c>,
|
||||
/// <c>MaxWidth</c>, <c>Margin</c>) to position the card inside the
|
||||
/// window, or apply a custom <c>Style</c> to change its look.
|
||||
/// </summary>
|
||||
public TransparentCard Card => _card;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the visual hosted inside the window's
|
||||
/// <see cref="TransparentCard"/>. This is the XAML default-content slot:
|
||||
/// child elements declared between the opening and closing
|
||||
/// <c>TransparentWindow</c> tags in a derived .xaml are routed here.
|
||||
/// </summary>
|
||||
public object? InnerContent
|
||||
{
|
||||
get => _card.Content;
|
||||
set => _card.Content = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played against
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> when <see cref="Show"/>
|
||||
/// flips it to <see cref="Visibility.Visible"/>. Defaults to a 200 ms
|
||||
/// fade-in plus a 250 ms slide-up of 24 px.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet ShowAnimations
|
||||
{
|
||||
get => _showAnimations;
|
||||
set => _showAnimations = value ?? new ImplicitAnimationSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played against
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> when <see cref="Hide"/>
|
||||
/// flips it to <see cref="Visibility.Collapsed"/>. Defaults to a 180 ms
|
||||
/// fade-out plus a 180 ms slide-down of 12 px.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet HideAnimations
|
||||
{
|
||||
get => _hideAnimations;
|
||||
set => _hideAnimations = value ?? new ImplicitAnimationSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and flips
|
||||
/// <see cref="Microsoft.UI.Xaml.Window.Content"/> to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays.
|
||||
/// Repeated calls reset the content to its hidden pose first so the show
|
||||
/// animation re-triggers cleanly. Any pending hide is cancelled.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
_hideCloseTimer.Stop();
|
||||
|
||||
if (Content is UIElement content)
|
||||
{
|
||||
// Re-apply each call so swapping animation collections at
|
||||
// runtime takes effect on the next show/hide cycle.
|
||||
Implicit.SetShowAnimations(content, _showAnimations);
|
||||
Implicit.SetHideAnimations(content, _hideAnimations);
|
||||
|
||||
// Reset to the hidden pose so the show animation always
|
||||
// animates from the configured starting frame.
|
||||
content.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
|
||||
if (Content is UIElement c2)
|
||||
{
|
||||
c2.Visibility = Visibility.Visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips <see cref="Microsoft.UI.Xaml.Window.Content"/> to
|
||||
/// <see cref="Visibility.Collapsed"/> so <see cref="HideAnimations"/>
|
||||
/// plays, then hides the underlying
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow"/> once the longest
|
||||
/// animation in <see cref="HideAnimations"/> (delay + duration) has
|
||||
/// completed.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
if (Content is UIElement content)
|
||||
{
|
||||
content.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
_hideCloseTimer.Debounce(
|
||||
AppWindow.Hide,
|
||||
interval: GetAnimationSetTotalDuration(_hideAnimations),
|
||||
immediate: false);
|
||||
});
|
||||
}
|
||||
|
||||
private static TimeSpan GetAnimationSetTotalDuration(ImplicitAnimationSet set)
|
||||
{
|
||||
TimeSpan longest = TimeSpan.Zero;
|
||||
foreach (var animation in set)
|
||||
{
|
||||
if (animation is Animation anim)
|
||||
{
|
||||
var total = (anim.Delay ?? TimeSpan.Zero) + (anim.Duration ?? TimeSpan.Zero);
|
||||
if (total > longest)
|
||||
{
|
||||
longest = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
private void ApplyExStyleBit(int bit, bool set)
|
||||
{
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nint exStyle = GetWindowLongPtr(_hwnd, GwlExStyle);
|
||||
nint updated = set ? exStyle | bit : exStyle & ~(nint)bit;
|
||||
if (updated != exStyle)
|
||||
{
|
||||
_ = SetWindowLongPtr(_hwnd, GwlExStyle, updated);
|
||||
}
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildDefaultShowAnimations() => new()
|
||||
{
|
||||
new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(200),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
new TranslationAnimation
|
||||
{
|
||||
From = "0,24,32",
|
||||
To = "0,0,32",
|
||||
Duration = TimeSpan.FromMilliseconds(250),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
};
|
||||
|
||||
private static ImplicitAnimationSet BuildDefaultHideAnimations() => new()
|
||||
{
|
||||
new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
new TranslationAnimation
|
||||
{
|
||||
From = "0,0,32",
|
||||
To = "0,12,32",
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
},
|
||||
};
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static unsafe partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, void* pvAttribute, int cbAttribute);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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 Deferral = global::Windows.Foundation.Deferral;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Data for <see cref="TransparentWindow.Hiding"/>. Supports deferrals so an
|
||||
/// animated surface can keep the window visible until its out-animation has
|
||||
/// finished. If no handler takes a deferral, the window hides immediately.
|
||||
/// </summary>
|
||||
public sealed class HidingEventArgs : EventArgs
|
||||
{
|
||||
private int _outstanding;
|
||||
private bool _raised;
|
||||
private Action? _continuation;
|
||||
|
||||
/// <summary>
|
||||
/// Requests that the window stay visible until the returned deferral is
|
||||
/// completed. Call <see cref="Deferral.Complete"/> once the out-animation
|
||||
/// has finished.
|
||||
/// </summary>
|
||||
/// <returns>A deferral that must be completed to allow the window to hide.</returns>
|
||||
public Deferral GetDeferral()
|
||||
{
|
||||
Interlocked.Increment(ref _outstanding);
|
||||
return new Deferral(OnDeferralCompleted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the window after raising the event to register what should run
|
||||
/// once every outstanding deferral has completed (or immediately if none
|
||||
/// were taken).
|
||||
/// </summary>
|
||||
internal void RunWhenComplete(Action continuation)
|
||||
{
|
||||
_continuation = continuation;
|
||||
_raised = true;
|
||||
TryComplete();
|
||||
}
|
||||
|
||||
private void OnDeferralCompleted()
|
||||
{
|
||||
Interlocked.Decrement(ref _outstanding);
|
||||
TryComplete();
|
||||
}
|
||||
|
||||
private void TryComplete()
|
||||
{
|
||||
if (_raised && Volatile.Read(ref _outstanding) == 0)
|
||||
{
|
||||
var continuation = _continuation;
|
||||
_continuation = null;
|
||||
continuation?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Data for <see cref="TransparentWindow.Showing"/>. Carries the transition the
|
||||
/// content should play, or <see langword="null"/> to let the content use its own
|
||||
/// configured show transition.
|
||||
/// </summary>
|
||||
public sealed class ShowingEventArgs : EventArgs
|
||||
{
|
||||
public ShowingEventArgs(Transition? transition)
|
||||
{
|
||||
Transition = transition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transition the content should play, or <see langword="null"/> to
|
||||
/// use the content's own configured show transition.
|
||||
/// </summary>
|
||||
public Transition? Transition { get; }
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.Foundation;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable transparent host window for transient overlays
|
||||
/// (toasts, banners, indicators) that should not steal foreground.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The constructor applies all of the boilerplate that PowerToys overlays
|
||||
/// currently hand-roll:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Strip the native frame and caption (<c>WS_THICKFRAME</c> etc.).</item>
|
||||
/// <item>Disable the Win11 1-pixel DWM border and corner rounding.</item>
|
||||
/// <item>Mark the window as a tool window so it stays out of the taskbar and Alt-Tab.</item>
|
||||
/// <item>Extend content into the title bar and collapse the title bar.</item>
|
||||
/// <item>Apply a <see cref="TransparentTintBackdrop"/> so the HWND is fully
|
||||
/// see-through and the visible chrome can be drawn by the content.</item>
|
||||
/// </list>
|
||||
/// <para>This window is intentionally animation-agnostic: it does not own any
|
||||
/// chrome or motion. Consumers supply their own content (typically a
|
||||
/// <see cref="TransientSurface"/>) which draws the acrylic, border, corners and
|
||||
/// shadow, and animates itself. <see cref="Show()"/> and <see cref="Hide"/>
|
||||
/// coordinate <c>SW_SHOWNA</c> (no-activate) with the
|
||||
/// <see cref="Showing"/> / <see cref="Hiding"/> events: a content surface
|
||||
/// subscribes to those (e.g. via <see cref="TransientSurface.SubscribeTo"/>)
|
||||
/// and plays its in/out animation. The <see cref="Hiding"/> event supports
|
||||
/// deferrals, so the underlying
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> is delayed until the
|
||||
/// content has finished animating out. With no listener the window simply shows
|
||||
/// or hides immediately.</para>
|
||||
/// <para><b>Multiple surfaces.</b> More than one <see cref="TransientSurface"/>
|
||||
/// may host on the same window by each calling
|
||||
/// <see cref="TransientSurface.SubscribeTo"/>. The <see cref="Showing"/> and
|
||||
/// <see cref="Hiding"/> events are simply raised for every subscriber, and
|
||||
/// because <see cref="HidingEventArgs"/> aggregates deferrals the underlying
|
||||
/// window is hidden only after <em>all</em> surfaces have finished animating
|
||||
/// out. To let each surface play its own distinct transition, call the
|
||||
/// parameterless <see cref="Show()"/> (so every surface uses its configured
|
||||
/// <c>ShowTransition</c>/<c>HideTransition</c>); the <see cref="Show(Transition)"/>
|
||||
/// overload instead broadcasts a single transition to all surfaces. Sizing the
|
||||
/// window and positioning each surface within it remain the consumer's
|
||||
/// responsibility (this window owns no layout).</para>
|
||||
/// </remarks>
|
||||
public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
private const uint DwmwaColorNone = 0xFFFFFFFE;
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwaBorderColor = 34;
|
||||
private const int DwmwcpDoNotRound = 1;
|
||||
|
||||
private const int GwlExStyle = -20;
|
||||
private const int WsExToolWindow = 0x00000080;
|
||||
|
||||
private const int SwShowNa = 8;
|
||||
|
||||
private readonly nint _hwnd;
|
||||
|
||||
private bool _inputHooked;
|
||||
private bool _seenActivated;
|
||||
|
||||
public TransparentWindow()
|
||||
{
|
||||
AppWindow.Hide();
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
|
||||
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
|
||||
unsafe
|
||||
{
|
||||
uint borderColor = DwmwaColorNone;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaBorderColor, &borderColor, sizeof(uint));
|
||||
|
||||
int cornerPref = DwmwcpDoNotRound;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaWindowCornerPreference, &cornerPref, sizeof(int));
|
||||
}
|
||||
|
||||
ApplyExStyleBit(WsExToolWindow, true);
|
||||
|
||||
SystemBackdrop = new TransparentTintBackdrop();
|
||||
|
||||
Activated += OnActivatedForDismiss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether pressing <c>Esc</c> while the
|
||||
/// window content has keyboard focus dismisses the window (<see cref="Hide"/>).
|
||||
/// Defaults to <see langword="false"/>. The window is shown without
|
||||
/// activation, so the consumer must activate it for its content to receive
|
||||
/// keyboard input.
|
||||
/// </summary>
|
||||
public bool DismissOnEscape { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window dismisses itself
|
||||
/// (<see cref="Hide"/>) when it loses focus (is deactivated), i.e. light
|
||||
/// dismiss. Defaults to <see langword="false"/>. Only takes effect after the
|
||||
/// window has been activated at least once since the last <see cref="Show()"/>,
|
||||
/// so the transient deactivation that can occur during the show sequence does
|
||||
/// not dismiss it prematurely. The window is shown without activation, so the
|
||||
/// consumer must activate it for this to apply.
|
||||
/// </summary>
|
||||
public bool DismissOnFocusLost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised (without activation) when <see cref="Show()"/> makes the window
|
||||
/// visible. A content surface subscribes to this to play its in-animation,
|
||||
/// using <see cref="ShowingEventArgs.Transition"/>.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<TransparentWindow, ShowingEventArgs>? Showing;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when <see cref="Hide"/> begins dismissing the window. A content
|
||||
/// surface subscribes to this to play its out-animation, taking a deferral
|
||||
/// (<see cref="HidingEventArgs.GetDeferral"/>) so the underlying window stays
|
||||
/// visible until the animation completes.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<TransparentWindow, HidingEventArgs>? Hiding;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> without a transition, so subscribed content animates
|
||||
/// in using its own configured show transition.
|
||||
/// </summary>
|
||||
public void Show() => RaiseShow(null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> so subscribed content animates in using
|
||||
/// <paramref name="transition"/>, overriding its configured show transition.
|
||||
/// </summary>
|
||||
/// <param name="transition">The transition the content should play.</param>
|
||||
public void Show(Transition transition) => RaiseShow(transition);
|
||||
|
||||
private void RaiseShow(Transition? transition)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
_seenActivated = false;
|
||||
EnsureInputHooks();
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
Showing?.Invoke(this, new ShowingEventArgs(transition));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises <see cref="Hiding"/> so subscribed content animates out, then hides
|
||||
/// the underlying <see cref="Microsoft.UI.Windowing.AppWindow"/> once every
|
||||
/// deferral taken by a handler has completed (immediately if none were taken).
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
var args = new HidingEventArgs();
|
||||
Hiding?.Invoke(this, args);
|
||||
args.RunWhenComplete(AppWindow.Hide);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnActivatedForDismiss(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
if (DismissOnFocusLost && _seenActivated)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_seenActivated = true;
|
||||
}
|
||||
|
||||
private void EnsureInputHooks()
|
||||
{
|
||||
if (_inputHooked || Content is not UIElement element)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
element.KeyDown += OnContentKeyDown;
|
||||
_inputHooked = true;
|
||||
}
|
||||
|
||||
private void OnContentKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (DismissOnEscape && e.Key == global::Windows.System.VirtualKey.Escape)
|
||||
{
|
||||
e.Handled = true;
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyExStyleBit(int bit, bool set)
|
||||
{
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nint exStyle = GetWindowLongPtr(_hwnd, GwlExStyle);
|
||||
nint updated = set ? exStyle | bit : exStyle & ~(nint)bit;
|
||||
if (updated != exStyle)
|
||||
{
|
||||
_ = SetWindowLongPtr(_hwnd, GwlExStyle, updated);
|
||||
}
|
||||
}
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static unsafe partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, void* pvAttribute, int cbAttribute);
|
||||
}
|
||||
@@ -103,7 +103,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
[PowerToysModule.FancyZone] = new ModuleInfo("PowerToys.FancyZonesEditor.exe", "FancyZones Layout"),
|
||||
[PowerToysModule.Hosts] = new ModuleInfo("PowerToys.Hosts.exe", "Hosts File Editor", "WinUI3Apps"),
|
||||
[PowerToysModule.Runner] = new ModuleInfo("PowerToys.exe", "PowerToys"),
|
||||
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"),
|
||||
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor", "WinUI3Apps"),
|
||||
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
|
||||
[PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"),
|
||||
[PowerToysModule.ScreenRuler] = new ModuleInfo("PowerToys.MeasureToolUI.exe", "PowerToys.ScreenRuler", "WinUI3Apps"),
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 2.3 KiB |
@@ -373,6 +373,13 @@ static int g_overlayRenderedH = 0;
|
||||
// Always On Top (WindowCornerUtils::CornersRadius).
|
||||
static int CornerRadiusForWindow(HWND hwnd)
|
||||
{
|
||||
// Remote sessions draw square windows even on Win11, yet still report DWMWCP_DEFAULT. Match the
|
||||
// window: a remote session gets square (radius 0) so the overlay border doesn't round off the corner.
|
||||
if (GetSystemMetrics(SM_REMOTESESSION))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pref = 0; // DWMWCP_DEFAULT
|
||||
if (DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)) != S_OK)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "trace.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#ifdef COMPOSITION
|
||||
namespace winrt
|
||||
@@ -48,6 +50,18 @@ private:
|
||||
void ClearDrawingPoint();
|
||||
void ClearDrawing();
|
||||
void BringToFront();
|
||||
// Ripple mode: spawn the press/hold ring + glow at the click point and
|
||||
// continue the animation into a fade-out on release. The held ring may
|
||||
// optionally follow the cursor while held (gated by m_rippleShowDragTrail).
|
||||
void SpawnRippleHoldDot(MouseButton button);
|
||||
void FadeRippleHoldDot(MouseButton button);
|
||||
// Ripple mode: emit a single self-contained ripple (grow + fade) for a quick
|
||||
// click, independent of any held indicator.
|
||||
void EmitSingleRipple(MouseButton button);
|
||||
// Spotlight mode: pressed-state animation that shrinks the mask while
|
||||
// a mouse button is held and restores it on release.
|
||||
void SpotlightAnimatePress();
|
||||
void SpotlightAnimateRelease();
|
||||
HHOOK m_mouseHook = NULL;
|
||||
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
|
||||
// Helpers for spotlight overlay
|
||||
@@ -71,6 +85,16 @@ private:
|
||||
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
|
||||
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
|
||||
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
|
||||
// Ellipse geometries kept alongside the pointer shapes so press-down /
|
||||
// release animations can target the radius directly.
|
||||
winrt::CompositionEllipseGeometry m_leftGeometry{ nullptr };
|
||||
winrt::CompositionEllipseGeometry m_rightGeometry{ nullptr };
|
||||
// Ripple-mode held glow (the soft halo behind the ring) — paired with
|
||||
// m_left/rightPointer (which holds the ring shape) while a button is held.
|
||||
winrt::CompositionSpriteShape m_leftRippleGlow{ nullptr };
|
||||
winrt::CompositionSpriteShape m_rightRippleGlow{ nullptr };
|
||||
winrt::CompositionEllipseGeometry m_leftGlowGeometry{ nullptr };
|
||||
winrt::CompositionEllipseGeometry m_rightGlowGeometry{ nullptr };
|
||||
// Spotlight overlay (mask with soft feathered edge)
|
||||
winrt::SpriteVisual m_overlay{ nullptr };
|
||||
winrt::CompositionMaskBrush m_spotlightMask{ nullptr };
|
||||
@@ -84,9 +108,22 @@ private:
|
||||
bool m_rightPointerEnabled = true;
|
||||
bool m_alwaysPointerEnabled = true;
|
||||
bool m_spotlightMode = false;
|
||||
bool m_spotlightPressed = false;
|
||||
bool m_rippleMode = true;
|
||||
bool m_rippleShowDragTrail = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL;
|
||||
bool m_rippleShowReleasePulse = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE;
|
||||
float m_rippleSize = static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE);
|
||||
float m_rippleIntensity = static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY);
|
||||
int m_rippleDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
|
||||
|
||||
bool m_leftButtonPressed = false;
|
||||
bool m_rightButtonPressed = false;
|
||||
// Pending hold-detection timers. A ripple "held indicator" is only spawned
|
||||
// once the button has been held past a short threshold; a quick click that
|
||||
// releases before then emits a single self-contained ripple instead. This
|
||||
// prevents a single click from rendering two ripples (press + release).
|
||||
UINT_PTR m_leftHoldTimer = 0;
|
||||
UINT_PTR m_rightHoldTimer = 0;
|
||||
UINT_PTR m_timer_id = 0;
|
||||
|
||||
bool m_visible = false;
|
||||
@@ -102,6 +139,11 @@ private:
|
||||
winrt::Windows::UI::Color m_alwaysColor = MOUSE_HIGHLIGHTER_DEFAULT_ALWAYS_COLOR;
|
||||
};
|
||||
static const uint32_t BRING_TO_FRONT_TIMER_ID = 123;
|
||||
static const uint32_t HOLD_RIPPLE_TIMER_LEFT = 124;
|
||||
static const uint32_t HOLD_RIPPLE_TIMER_RIGHT = 125;
|
||||
// How long a ripple button must be held before the persistent "held indicator"
|
||||
// is shown. Releasing before this is treated as a quick click (single ripple).
|
||||
static const uint32_t HOLD_RIPPLE_THRESHOLD_MS = 180;
|
||||
Highlighter* Highlighter::instance = nullptr;
|
||||
|
||||
bool Highlighter::CreateHighlighter()
|
||||
@@ -194,11 +236,34 @@ void Highlighter::AddDrawingPoint(MouseButton button)
|
||||
{
|
||||
circleShape.FillBrush(m_compositor.CreateColorBrush(m_leftClickColor));
|
||||
m_leftPointer = circleShape;
|
||||
m_leftGeometry = circleGeometry;
|
||||
|
||||
// Niels-style press-down shrink: holding the button squeezes the
|
||||
// circle to 70% over 180ms after a 150ms delay so quick clicks skip
|
||||
// it. StartDrawingPointFading stops this animation on release.
|
||||
const float pressedRadius = m_radius * 0.70f;
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
|
||||
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(180));
|
||||
anim.DelayTime(std::chrono::milliseconds(150));
|
||||
circleGeometry.StartAnimation(L"Radius", anim);
|
||||
}
|
||||
else if (button == MouseButton::Right)
|
||||
{
|
||||
circleShape.FillBrush(m_compositor.CreateColorBrush(m_rightClickColor));
|
||||
m_rightPointer = circleShape;
|
||||
m_rightGeometry = circleGeometry;
|
||||
|
||||
const float pressedRadius = m_radius * 0.70f;
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
|
||||
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(180));
|
||||
anim.DelayTime(std::chrono::milliseconds(150));
|
||||
circleGeometry.StartAnimation(L"Radius", anim);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,17 +303,36 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
|
||||
if (button == MouseButton::Left)
|
||||
{
|
||||
m_leftPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
if (m_leftRippleGlow)
|
||||
{
|
||||
m_leftRippleGlow.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
}
|
||||
else if (button == MouseButton::Right)
|
||||
{
|
||||
m_rightPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
if (m_rightRippleGlow)
|
||||
{
|
||||
m_rightRippleGlow.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// always / spotlight idle
|
||||
if (m_spotlightMode)
|
||||
{
|
||||
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||
if (m_spotlightPressed)
|
||||
{
|
||||
// Only update position while pressed — radius is being animated
|
||||
if (m_spotlightMaskGradient)
|
||||
{
|
||||
m_spotlightMaskGradient.EllipseCenter({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||
}
|
||||
}
|
||||
else if (m_alwaysPointer)
|
||||
{
|
||||
@@ -259,14 +343,24 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
|
||||
void Highlighter::StartDrawingPointFading(MouseButton button)
|
||||
{
|
||||
winrt::Windows::UI::Composition::CompositionSpriteShape circleShape{ nullptr };
|
||||
winrt::Windows::UI::Composition::CompositionEllipseGeometry geom{ nullptr };
|
||||
if (button == MouseButton::Left)
|
||||
{
|
||||
circleShape = m_leftPointer;
|
||||
geom = m_leftGeometry;
|
||||
}
|
||||
else
|
||||
{
|
||||
// right
|
||||
circleShape = m_rightPointer;
|
||||
geom = m_rightGeometry;
|
||||
}
|
||||
|
||||
// Stop any in-flight press-down shrink so the geometry doesn't keep
|
||||
// animating while the fill is being faded out.
|
||||
if (geom && m_compositor)
|
||||
{
|
||||
geom.StopAnimation(L"Radius");
|
||||
}
|
||||
|
||||
auto brushColor = circleShape.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color();
|
||||
@@ -329,6 +423,30 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
switch (wParam)
|
||||
{
|
||||
case WM_LBUTTONDOWN:
|
||||
if (instance->m_spotlightMode)
|
||||
{
|
||||
instance->SpotlightAnimatePress();
|
||||
break;
|
||||
}
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_leftPointerEnabled)
|
||||
{
|
||||
// Defer the held indicator: only spawn it if the button is
|
||||
// still down after the hold threshold. A quick click handled
|
||||
// on button-up emits a single ripple instead.
|
||||
instance->m_leftButtonPressed = true;
|
||||
if (instance->m_leftHoldTimer == 0)
|
||||
{
|
||||
instance->m_leftHoldTimer = SetTimer(instance->m_hwnd, HOLD_RIPPLE_TIMER_LEFT, HOLD_RIPPLE_THRESHOLD_MS, nullptr);
|
||||
}
|
||||
if (instance->m_timer_id == 0)
|
||||
{
|
||||
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (instance->m_leftPointerEnabled)
|
||||
{
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
|
||||
@@ -354,6 +472,28 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_RBUTTONDOWN:
|
||||
if (instance->m_spotlightMode)
|
||||
{
|
||||
instance->SpotlightAnimatePress();
|
||||
break;
|
||||
}
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_rightPointerEnabled)
|
||||
{
|
||||
// Defer the held indicator (see WM_LBUTTONDOWN).
|
||||
instance->m_rightButtonPressed = true;
|
||||
if (instance->m_rightHoldTimer == 0)
|
||||
{
|
||||
instance->m_rightHoldTimer = SetTimer(instance->m_hwnd, HOLD_RIPPLE_TIMER_RIGHT, HOLD_RIPPLE_THRESHOLD_MS, nullptr);
|
||||
}
|
||||
if (instance->m_timer_id == 0)
|
||||
{
|
||||
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (instance->m_rightPointerEnabled)
|
||||
{
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
|
||||
@@ -376,6 +516,24 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_MOUSEMOVE:
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
// Held ripple ring follows the cursor while a button is down,
|
||||
// gated by the "follow cursor while held" setting. When the
|
||||
// setting is off, the ring stays anchored at the click point.
|
||||
if (instance->m_rippleShowDragTrail)
|
||||
{
|
||||
if (instance->m_leftButtonPressed && instance->m_leftPointer)
|
||||
{
|
||||
instance->UpdateDrawingPointPosition(MouseButton::Left);
|
||||
}
|
||||
if (instance->m_rightButtonPressed && instance->m_rightPointer)
|
||||
{
|
||||
instance->UpdateDrawingPointPosition(MouseButton::Right);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (instance->m_leftButtonPressed)
|
||||
{
|
||||
instance->UpdateDrawingPointPosition(MouseButton::Left);
|
||||
@@ -390,11 +548,33 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_LBUTTONUP:
|
||||
if (instance->m_spotlightPressed)
|
||||
{
|
||||
instance->SpotlightAnimateRelease();
|
||||
}
|
||||
if (instance->m_leftButtonPressed)
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Left);
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_leftHoldTimer != 0)
|
||||
{
|
||||
// Released before the hold threshold => quick click.
|
||||
KillTimer(instance->m_hwnd, instance->m_leftHoldTimer);
|
||||
instance->m_leftHoldTimer = 0;
|
||||
instance->EmitSingleRipple(MouseButton::Left);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Held indicator was already shown; expand + fade it.
|
||||
instance->FadeRippleHoldDot(MouseButton::Left);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Left);
|
||||
}
|
||||
instance->m_leftButtonPressed = false;
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
|
||||
if (!instance->m_rippleMode && instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
|
||||
{
|
||||
// Add AlwaysPointer only when it's enabled and RightPointer is not active
|
||||
instance->AddDrawingPoint(MouseButton::None);
|
||||
@@ -402,11 +582,32 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
|
||||
}
|
||||
break;
|
||||
case WM_RBUTTONUP:
|
||||
if (instance->m_spotlightPressed)
|
||||
{
|
||||
instance->SpotlightAnimateRelease();
|
||||
}
|
||||
if (instance->m_rightButtonPressed)
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Right);
|
||||
if (instance->m_rippleMode)
|
||||
{
|
||||
if (instance->m_rightHoldTimer != 0)
|
||||
{
|
||||
// Released before the hold threshold => quick click.
|
||||
KillTimer(instance->m_hwnd, instance->m_rightHoldTimer);
|
||||
instance->m_rightHoldTimer = 0;
|
||||
instance->EmitSingleRipple(MouseButton::Right);
|
||||
}
|
||||
else
|
||||
{
|
||||
instance->FadeRippleHoldDot(MouseButton::Right);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
instance->StartDrawingPointFading(MouseButton::Right);
|
||||
}
|
||||
instance->m_rightButtonPressed = false;
|
||||
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
|
||||
if (!instance->m_rippleMode && instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
|
||||
{
|
||||
// Add AlwaysPointer only when it's enabled and LeftPointer is not active
|
||||
instance->AddDrawingPoint(MouseButton::None);
|
||||
@@ -448,9 +649,16 @@ void Highlighter::StopDrawing()
|
||||
m_visible = false;
|
||||
m_leftButtonPressed = false;
|
||||
m_rightButtonPressed = false;
|
||||
m_spotlightPressed = false;
|
||||
m_leftPointer = nullptr;
|
||||
m_rightPointer = nullptr;
|
||||
m_alwaysPointer = nullptr;
|
||||
m_leftGeometry = nullptr;
|
||||
m_rightGeometry = nullptr;
|
||||
m_leftRippleGlow = nullptr;
|
||||
m_rightRippleGlow = nullptr;
|
||||
m_leftGlowGeometry = nullptr;
|
||||
m_rightGlowGeometry = nullptr;
|
||||
if (m_overlay)
|
||||
{
|
||||
m_overlay.IsVisible(false);
|
||||
@@ -478,6 +686,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
|
||||
m_rightPointerEnabled = settings.rightButtonColor.A != 0;
|
||||
m_alwaysPointerEnabled = settings.alwaysColor.A != 0;
|
||||
m_spotlightMode = settings.spotlightMode && settings.alwaysColor.A != 0;
|
||||
m_rippleMode = settings.rippleMode && !m_spotlightMode;
|
||||
m_rippleSize = (settings.rippleSize > 0) ? static_cast<float>(settings.rippleSize) : static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE);
|
||||
m_rippleIntensity = (settings.rippleIntensity > 0.0) ? static_cast<float>(settings.rippleIntensity) : static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY);
|
||||
m_rippleDurationMs = (settings.rippleDurationMs > 0) ? settings.rippleDurationMs : MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
|
||||
m_rippleShowDragTrail = settings.rippleShowDragTrail;
|
||||
m_rippleShowReleasePulse = settings.rippleShowReleasePulse;
|
||||
|
||||
// Reset transient pressed-state flag so a settings change while a button
|
||||
// happens to be down doesn't leave the spotlight stuck at a shrunken size.
|
||||
m_spotlightPressed = false;
|
||||
|
||||
if (m_spotlightMode)
|
||||
{
|
||||
@@ -548,6 +766,7 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
|
||||
// If we would use a timer with a 50 ms period, there would be a flickering on the UI, as in most of the cases
|
||||
// the pinned window hides our window in a few milliseconds.
|
||||
case BRING_TO_FRONT_TIMER_ID:
|
||||
{
|
||||
static int fireCount = 0;
|
||||
if (fireCount++ >= 4)
|
||||
{
|
||||
@@ -558,6 +777,24 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
|
||||
instance->BringToFront();
|
||||
break;
|
||||
}
|
||||
case HOLD_RIPPLE_TIMER_LEFT:
|
||||
// Button held past the threshold: show the persistent held indicator.
|
||||
KillTimer(instance->m_hwnd, instance->m_leftHoldTimer);
|
||||
instance->m_leftHoldTimer = 0;
|
||||
if (instance->m_leftButtonPressed)
|
||||
{
|
||||
instance->SpawnRippleHoldDot(MouseButton::Left);
|
||||
}
|
||||
break;
|
||||
case HOLD_RIPPLE_TIMER_RIGHT:
|
||||
KillTimer(instance->m_hwnd, instance->m_rightHoldTimer);
|
||||
instance->m_rightHoldTimer = 0;
|
||||
if (instance->m_rightButtonPressed)
|
||||
{
|
||||
instance->SpawnRippleHoldDot(MouseButton::Right);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -643,6 +880,548 @@ void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool sho
|
||||
}
|
||||
}
|
||||
|
||||
// Spotlight press-down: shrink the mask radius briefly while a button is held.
|
||||
void Highlighter::SpotlightAnimatePress()
|
||||
{
|
||||
if (!m_spotlightMode || !m_spotlightMaskGradient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_spotlightPressed = true;
|
||||
const float pressedRadius = m_radius * 0.85f;
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
|
||||
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(120));
|
||||
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", anim);
|
||||
}
|
||||
|
||||
// Spotlight release: animate the mask back to the configured radius.
|
||||
void Highlighter::SpotlightAnimateRelease()
|
||||
{
|
||||
m_spotlightPressed = false;
|
||||
|
||||
if (!m_spotlightMode || !m_spotlightMaskGradient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
auto current = m_spotlightMaskGradient.EllipseRadius();
|
||||
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
anim.InsertKeyFrame(0.0f, current);
|
||||
anim.InsertKeyFrame(1.0f, { m_radius, m_radius }, ease);
|
||||
anim.Duration(std::chrono::milliseconds(200));
|
||||
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", anim);
|
||||
}
|
||||
|
||||
// Spawn the press/hold ring + glow at the click point. The shapes persist
|
||||
// until FadeRippleHoldDot is called (button-up). While held they can be
|
||||
// re-positioned to follow the cursor (UpdateDrawingPointPosition).
|
||||
void Highlighter::SpawnRippleHoldDot(MouseButton button)
|
||||
{
|
||||
if (!m_compositor || !m_shape)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
|
||||
if (color.A == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
POINT pt{};
|
||||
if (!GetCursorPos(&pt))
|
||||
{
|
||||
return;
|
||||
}
|
||||
ScreenToClient(m_hwnd, &pt);
|
||||
const float fx = static_cast<float>(pt.x);
|
||||
const float fy = static_cast<float>(pt.y);
|
||||
|
||||
// Resolve sizing/intensity from the ripple-specific settings so they're
|
||||
// independent of the legacy "always-on dot" controls.
|
||||
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
|
||||
float intensity = m_rippleIntensity;
|
||||
if (intensity < 0.15f) intensity = 0.15f;
|
||||
if (intensity > 1.35f) intensity = 1.35f;
|
||||
|
||||
const float ringHeld = baseSize * 0.55f;
|
||||
const float glowHeld = baseSize * 0.65f;
|
||||
const float lineWidth = (std::max)(2.25f, baseSize * (0.035f + intensity * 0.045f));
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
// Held indicator: appears once the button has been held past the hold
|
||||
// threshold and sits at the held radius until release. It must NOT expand
|
||||
// outward on appearance — it only FADES IN at the held size. The single
|
||||
// outward "ripple" expansion happens exclusively on release
|
||||
// (FadeRippleHoldDot). If this grew outward, a slow single click (release
|
||||
// shortly after the threshold) would show grow-to-held + release as two
|
||||
// expansions — the double-ripple bug.
|
||||
auto dur = std::chrono::milliseconds(120);
|
||||
|
||||
auto clampByte = [](float v) -> uint8_t {
|
||||
if (v < 0.0f) v = 0.0f;
|
||||
if (v > 255.0f) v = 255.0f;
|
||||
return static_cast<uint8_t>(v);
|
||||
};
|
||||
|
||||
// Glow color is the click color, lower alpha (×0.30), scaled by intensity.
|
||||
const float glowAlpha = static_cast<float>(color.A) * 0.30f * intensity;
|
||||
auto glowColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(glowAlpha), color.R, color.G, color.B);
|
||||
auto glowTransparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
// Ring color uses full base alpha (alphaMul like the press recipe).
|
||||
const float alphaMul = 0.18f + intensity * 0.78f;
|
||||
auto ringColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * alphaMul), color.R, color.G, color.B);
|
||||
auto ringTransparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
// Clean up any stray "still held" shapes for this button — guards against
|
||||
// stray button-down without matching button-up (e.g. focus loss).
|
||||
winrt::CompositionSpriteShape& heldRing = (button == MouseButton::Left) ? m_leftPointer : m_rightPointer;
|
||||
winrt::CompositionSpriteShape& heldGlow = (button == MouseButton::Left) ? m_leftRippleGlow : m_rightRippleGlow;
|
||||
winrt::CompositionEllipseGeometry& heldGeom = (button == MouseButton::Left) ? m_leftGeometry : m_rightGeometry;
|
||||
winrt::CompositionEllipseGeometry& heldGlowGeom = (button == MouseButton::Left) ? m_leftGlowGeometry : m_rightGlowGeometry;
|
||||
|
||||
if (m_shape && m_shape.Shapes())
|
||||
{
|
||||
auto shapes = m_shape.Shapes();
|
||||
uint32_t idx = 0;
|
||||
if (heldRing && shapes.IndexOf(heldRing, idx))
|
||||
{
|
||||
shapes.RemoveAt(idx);
|
||||
}
|
||||
if (heldGlow && shapes.IndexOf(heldGlow, idx))
|
||||
{
|
||||
shapes.RemoveAt(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Glow (filled) — added first so the ring renders on top. Sits at the held
|
||||
// radius and fades its alpha in (no outward size growth).
|
||||
auto glowGeom = m_compositor.CreateEllipseGeometry();
|
||||
glowGeom.Radius({ glowHeld, glowHeld });
|
||||
auto glowBrush = m_compositor.CreateColorBrush(glowTransparent);
|
||||
auto glowShape = m_compositor.CreateSpriteShape(glowGeom);
|
||||
glowShape.Offset({ fx, fy });
|
||||
glowShape.FillBrush(glowBrush);
|
||||
m_shape.Shapes().Append(glowShape);
|
||||
|
||||
auto glowFadeIn = m_compositor.CreateColorKeyFrameAnimation();
|
||||
glowFadeIn.InsertKeyFrame(0.0f, glowTransparent);
|
||||
glowFadeIn.InsertKeyFrame(1.0f, glowColor, ease);
|
||||
glowFadeIn.Duration(dur);
|
||||
glowBrush.StartAnimation(L"Color", glowFadeIn);
|
||||
|
||||
// Ring (stroked) — same: fixed at held radius, alpha fade-in only.
|
||||
auto ringGeom = m_compositor.CreateEllipseGeometry();
|
||||
ringGeom.Radius({ ringHeld, ringHeld });
|
||||
auto ringBrush = m_compositor.CreateColorBrush(ringTransparent);
|
||||
auto ringShape = m_compositor.CreateSpriteShape(ringGeom);
|
||||
ringShape.Offset({ fx, fy });
|
||||
ringShape.StrokeBrush(ringBrush);
|
||||
ringShape.StrokeThickness(lineWidth);
|
||||
ringShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(ringShape);
|
||||
|
||||
auto ringFadeIn = m_compositor.CreateColorKeyFrameAnimation();
|
||||
ringFadeIn.InsertKeyFrame(0.0f, ringTransparent);
|
||||
ringFadeIn.InsertKeyFrame(1.0f, ringColor, ease);
|
||||
ringFadeIn.Duration(dur);
|
||||
ringBrush.StartAnimation(L"Color", ringFadeIn);
|
||||
|
||||
heldRing = ringShape;
|
||||
heldGlow = glowShape;
|
||||
heldGeom = ringGeom;
|
||||
heldGlowGeom = glowGeom;
|
||||
}
|
||||
|
||||
// Continue the held-ring/glow animation outward and fade both to transparent.
|
||||
// For right-click, optionally spawn the expanding crosshair lines.
|
||||
void Highlighter::FadeRippleHoldDot(MouseButton button)
|
||||
{
|
||||
if (!m_compositor || !m_shape)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::CompositionSpriteShape& heldRing = (button == MouseButton::Left) ? m_leftPointer : m_rightPointer;
|
||||
winrt::CompositionSpriteShape& heldGlow = (button == MouseButton::Left) ? m_leftRippleGlow : m_rightRippleGlow;
|
||||
winrt::CompositionEllipseGeometry& heldGeom = (button == MouseButton::Left) ? m_leftGeometry : m_rightGeometry;
|
||||
winrt::CompositionEllipseGeometry& heldGlowGeom = (button == MouseButton::Left) ? m_leftGlowGeometry : m_rightGlowGeometry;
|
||||
|
||||
if (!heldRing && !heldGlow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
|
||||
|
||||
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
|
||||
float intensity = m_rippleIntensity;
|
||||
if (intensity < 0.15f) intensity = 0.15f;
|
||||
if (intensity > 1.35f) intensity = 1.35f;
|
||||
|
||||
int durationMs = m_rippleDurationMs;
|
||||
if (durationMs < 60) durationMs = 60;
|
||||
if (durationMs > 2000) durationMs = 2000;
|
||||
auto dur = std::chrono::milliseconds(durationMs);
|
||||
|
||||
const float ringHeld = baseSize * 0.55f;
|
||||
const float ringEnd = baseSize * 1.05f;
|
||||
const float glowHeld = baseSize * 0.65f;
|
||||
const float glowEnd = baseSize * 1.40f;
|
||||
|
||||
auto clampByte = [](float v) -> uint8_t {
|
||||
if (v < 0.0f) v = 0.0f;
|
||||
if (v > 255.0f) v = 255.0f;
|
||||
return static_cast<uint8_t>(v);
|
||||
};
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
auto transparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
// Track everything spawned by this fade (and the held shapes themselves)
|
||||
// so the completion callback can remove them in one pass.
|
||||
auto spawned = std::make_shared<std::vector<winrt::CompositionSpriteShape>>();
|
||||
|
||||
auto batch = m_compositor.CreateScopedBatch(winrt::CompositionBatchTypes::Animation);
|
||||
|
||||
if (heldGlow && heldGlowGeom)
|
||||
{
|
||||
// The held indicator has settled at the held radius; expand it outward
|
||||
// from there and fade it to transparent.
|
||||
heldGlowGeom.StopAnimation(L"Radius");
|
||||
auto glowAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
glowAnim.InsertKeyFrame(0.0f, { glowHeld, glowHeld });
|
||||
glowAnim.InsertKeyFrame(1.0f, { glowEnd, glowEnd }, ease);
|
||||
glowAnim.Duration(dur);
|
||||
heldGlowGeom.StartAnimation(L"Radius", glowAnim);
|
||||
|
||||
auto brush = heldGlow.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>();
|
||||
auto startColor = brush.Color();
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, startColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
brush.StartAnimation(L"Color", colorAnim);
|
||||
|
||||
spawned->push_back(heldGlow);
|
||||
}
|
||||
|
||||
if (heldRing && heldGeom)
|
||||
{
|
||||
heldGeom.StopAnimation(L"Radius");
|
||||
auto ringAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
ringAnim.InsertKeyFrame(0.0f, { ringHeld, ringHeld });
|
||||
ringAnim.InsertKeyFrame(1.0f, { ringEnd, ringEnd }, ease);
|
||||
ringAnim.Duration(dur);
|
||||
heldGeom.StartAnimation(L"Radius", ringAnim);
|
||||
|
||||
auto brush = heldRing.StrokeBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>();
|
||||
auto startColor = brush.Color();
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, startColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
brush.StartAnimation(L"Color", colorAnim);
|
||||
|
||||
spawned->push_back(heldRing);
|
||||
}
|
||||
|
||||
// Right-click only: spawn expanding crosshair lines centered on the ring.
|
||||
// Gated by the "show crosshairs on right-click release" toggle.
|
||||
if (button == MouseButton::Right && m_rippleShowReleasePulse && heldRing)
|
||||
{
|
||||
const float xhairAlphaMul = 0.18f + intensity * 0.78f;
|
||||
auto xhairColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * xhairAlphaMul), color.R, color.G, color.B);
|
||||
const float xhairThickness = (std::max)(1.25f, baseSize * (0.025f + intensity * 0.03f));
|
||||
|
||||
auto center = heldRing.Offset();
|
||||
const float startSpan = ringHeld * 0.85f;
|
||||
const float endSpan = ringEnd * 0.85f;
|
||||
|
||||
auto makeLine = [&](float ax1, float ay1, float ax2, float ay2,
|
||||
float bx1, float by1, float bx2, float by2) {
|
||||
auto lineGeom = m_compositor.CreateLineGeometry();
|
||||
lineGeom.Start({ ax1, ay1 });
|
||||
lineGeom.End({ ax2, ay2 });
|
||||
|
||||
auto lineBrush = m_compositor.CreateColorBrush(xhairColor);
|
||||
auto lineShape = m_compositor.CreateSpriteShape(lineGeom);
|
||||
lineShape.StrokeBrush(lineBrush);
|
||||
lineShape.StrokeThickness(xhairThickness);
|
||||
lineShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(lineShape);
|
||||
spawned->push_back(lineShape);
|
||||
|
||||
auto startAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
startAnim.InsertKeyFrame(0.0f, { ax1, ay1 });
|
||||
startAnim.InsertKeyFrame(1.0f, { bx1, by1 }, ease);
|
||||
startAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"Start", startAnim);
|
||||
|
||||
auto endAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
endAnim.InsertKeyFrame(0.0f, { ax2, ay2 });
|
||||
endAnim.InsertKeyFrame(1.0f, { bx2, by2 }, ease);
|
||||
endAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"End", endAnim);
|
||||
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, xhairColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
lineBrush.StartAnimation(L"Color", colorAnim);
|
||||
};
|
||||
|
||||
// Horizontal line (left half, right half).
|
||||
makeLine(center.x - startSpan, center.y, center.x - startSpan * 0.30f, center.y,
|
||||
center.x - endSpan, center.y, center.x - endSpan * 0.30f, center.y);
|
||||
makeLine(center.x + startSpan * 0.30f, center.y, center.x + startSpan, center.y,
|
||||
center.x + endSpan * 0.30f, center.y, center.x + endSpan, center.y);
|
||||
// Vertical line (top half, bottom half).
|
||||
makeLine(center.x, center.y - startSpan, center.x, center.y - startSpan * 0.30f,
|
||||
center.x, center.y - endSpan, center.x, center.y - endSpan * 0.30f);
|
||||
makeLine(center.x, center.y + startSpan * 0.30f, center.x, center.y + startSpan,
|
||||
center.x, center.y + endSpan * 0.30f, center.x, center.y + endSpan);
|
||||
}
|
||||
|
||||
// Detach our member handles BEFORE the batch completes so subsequent
|
||||
// press events on this button create fresh shapes rather than racing.
|
||||
heldRing = nullptr;
|
||||
heldGlow = nullptr;
|
||||
heldGeom = nullptr;
|
||||
heldGlowGeom = nullptr;
|
||||
|
||||
batch.End();
|
||||
|
||||
if (spawned->empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto dispatcher = m_dispatcherQueueController.DispatcherQueue();
|
||||
batch.Completed([dispatcher, spawned](auto&&, auto&&) {
|
||||
dispatcher.TryEnqueue([spawned]() {
|
||||
try
|
||||
{
|
||||
if (Highlighter::instance == nullptr || Highlighter::instance->m_shape == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto shapes = Highlighter::instance->m_shape.Shapes();
|
||||
for (auto const& s : *spawned)
|
||||
{
|
||||
uint32_t index = 0;
|
||||
if (shapes.IndexOf(s, index))
|
||||
{
|
||||
shapes.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Highlighter may have torn down between batch completion and dispatch — ignore.
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Self-contained single ripple for a quick click (press + release before the
|
||||
// hold threshold). Spawns a fresh ring + glow that grow from the click point
|
||||
// outward and fade to transparent in one continuous animation — no held
|
||||
// indicator, so a single click produces exactly one ripple. For right-click,
|
||||
// optionally spawns the expanding crosshair lines too.
|
||||
void Highlighter::EmitSingleRipple(MouseButton button)
|
||||
{
|
||||
if (!m_compositor || !m_shape)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
|
||||
if (color.A == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
POINT pt{};
|
||||
if (!GetCursorPos(&pt))
|
||||
{
|
||||
return;
|
||||
}
|
||||
ScreenToClient(m_hwnd, &pt);
|
||||
const float fx = static_cast<float>(pt.x);
|
||||
const float fy = static_cast<float>(pt.y);
|
||||
|
||||
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
|
||||
float intensity = m_rippleIntensity;
|
||||
if (intensity < 0.15f) intensity = 0.15f;
|
||||
if (intensity > 1.35f) intensity = 1.35f;
|
||||
|
||||
int durationMs = m_rippleDurationMs;
|
||||
if (durationMs < 60) durationMs = 60;
|
||||
if (durationMs > 2000) durationMs = 2000;
|
||||
auto dur = std::chrono::milliseconds(durationMs);
|
||||
|
||||
const float ringStart = baseSize * 0.20f;
|
||||
const float ringEnd = baseSize * 1.05f;
|
||||
const float glowStart = baseSize * 0.30f;
|
||||
const float glowEnd = baseSize * 1.40f;
|
||||
const float lineWidth = (std::max)(2.25f, baseSize * (0.035f + intensity * 0.045f));
|
||||
|
||||
auto clampByte = [](float v) -> uint8_t {
|
||||
if (v < 0.0f) v = 0.0f;
|
||||
if (v > 255.0f) v = 255.0f;
|
||||
return static_cast<uint8_t>(v);
|
||||
};
|
||||
|
||||
const float glowAlpha = static_cast<float>(color.A) * 0.30f * intensity;
|
||||
auto glowColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(glowAlpha), color.R, color.G, color.B);
|
||||
const float alphaMul = 0.18f + intensity * 0.78f;
|
||||
auto ringColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * alphaMul), color.R, color.G, color.B);
|
||||
|
||||
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
|
||||
auto transparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
|
||||
|
||||
auto spawned = std::make_shared<std::vector<winrt::CompositionSpriteShape>>();
|
||||
|
||||
auto batch = m_compositor.CreateScopedBatch(winrt::CompositionBatchTypes::Animation);
|
||||
|
||||
// Glow (filled) — added first so the ring renders on top.
|
||||
auto glowGeom = m_compositor.CreateEllipseGeometry();
|
||||
glowGeom.Radius({ glowStart, glowStart });
|
||||
auto glowBrush = m_compositor.CreateColorBrush(glowColor);
|
||||
auto glowShape = m_compositor.CreateSpriteShape(glowGeom);
|
||||
glowShape.Offset({ fx, fy });
|
||||
glowShape.FillBrush(glowBrush);
|
||||
m_shape.Shapes().Append(glowShape);
|
||||
spawned->push_back(glowShape);
|
||||
|
||||
auto glowAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
glowAnim.InsertKeyFrame(0.0f, { glowStart, glowStart });
|
||||
glowAnim.InsertKeyFrame(1.0f, { glowEnd, glowEnd }, ease);
|
||||
glowAnim.Duration(dur);
|
||||
glowGeom.StartAnimation(L"Radius", glowAnim);
|
||||
|
||||
auto glowColorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
glowColorAnim.InsertKeyFrame(0.0f, glowColor);
|
||||
glowColorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
glowColorAnim.Duration(dur);
|
||||
glowBrush.StartAnimation(L"Color", glowColorAnim);
|
||||
|
||||
// Ring (stroked).
|
||||
auto ringGeom = m_compositor.CreateEllipseGeometry();
|
||||
ringGeom.Radius({ ringStart, ringStart });
|
||||
auto ringBrush = m_compositor.CreateColorBrush(ringColor);
|
||||
auto ringShape = m_compositor.CreateSpriteShape(ringGeom);
|
||||
ringShape.Offset({ fx, fy });
|
||||
ringShape.StrokeBrush(ringBrush);
|
||||
ringShape.StrokeThickness(lineWidth);
|
||||
ringShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(ringShape);
|
||||
spawned->push_back(ringShape);
|
||||
|
||||
auto ringAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
ringAnim.InsertKeyFrame(0.0f, { ringStart, ringStart });
|
||||
ringAnim.InsertKeyFrame(1.0f, { ringEnd, ringEnd }, ease);
|
||||
ringAnim.Duration(dur);
|
||||
ringGeom.StartAnimation(L"Radius", ringAnim);
|
||||
|
||||
auto ringColorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
ringColorAnim.InsertKeyFrame(0.0f, ringColor);
|
||||
ringColorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
ringColorAnim.Duration(dur);
|
||||
ringBrush.StartAnimation(L"Color", ringColorAnim);
|
||||
|
||||
// Right-click only: spawn expanding crosshair lines centered on the click
|
||||
// point. Gated by the "show crosshairs on right-click release" toggle.
|
||||
if (button == MouseButton::Right && m_rippleShowReleasePulse)
|
||||
{
|
||||
auto xhairColor = ringColor;
|
||||
const float xhairThickness = (std::max)(1.25f, baseSize * (0.025f + intensity * 0.03f));
|
||||
|
||||
const float startSpan = (baseSize * 0.55f) * 0.85f;
|
||||
const float endSpan = ringEnd * 0.85f;
|
||||
|
||||
auto makeLine = [&](float ax1, float ay1, float ax2, float ay2,
|
||||
float bx1, float by1, float bx2, float by2) {
|
||||
auto lineGeom = m_compositor.CreateLineGeometry();
|
||||
lineGeom.Start({ ax1, ay1 });
|
||||
lineGeom.End({ ax2, ay2 });
|
||||
|
||||
auto lineBrush = m_compositor.CreateColorBrush(xhairColor);
|
||||
auto lineShape = m_compositor.CreateSpriteShape(lineGeom);
|
||||
lineShape.StrokeBrush(lineBrush);
|
||||
lineShape.StrokeThickness(xhairThickness);
|
||||
lineShape.IsStrokeNonScaling(true);
|
||||
m_shape.Shapes().Append(lineShape);
|
||||
spawned->push_back(lineShape);
|
||||
|
||||
auto startAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
startAnim.InsertKeyFrame(0.0f, { ax1, ay1 });
|
||||
startAnim.InsertKeyFrame(1.0f, { bx1, by1 }, ease);
|
||||
startAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"Start", startAnim);
|
||||
|
||||
auto endAnim = m_compositor.CreateVector2KeyFrameAnimation();
|
||||
endAnim.InsertKeyFrame(0.0f, { ax2, ay2 });
|
||||
endAnim.InsertKeyFrame(1.0f, { bx2, by2 }, ease);
|
||||
endAnim.Duration(dur);
|
||||
lineGeom.StartAnimation(L"End", endAnim);
|
||||
|
||||
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
|
||||
colorAnim.InsertKeyFrame(0.0f, xhairColor);
|
||||
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
|
||||
colorAnim.Duration(dur);
|
||||
lineBrush.StartAnimation(L"Color", colorAnim);
|
||||
};
|
||||
|
||||
// Horizontal line (left half, right half).
|
||||
makeLine(fx - startSpan, fy, fx - startSpan * 0.30f, fy,
|
||||
fx - endSpan, fy, fx - endSpan * 0.30f, fy);
|
||||
makeLine(fx + startSpan * 0.30f, fy, fx + startSpan, fy,
|
||||
fx + endSpan * 0.30f, fy, fx + endSpan, fy);
|
||||
// Vertical line (top half, bottom half).
|
||||
makeLine(fx, fy - startSpan, fx, fy - startSpan * 0.30f,
|
||||
fx, fy - endSpan, fx, fy - endSpan * 0.30f);
|
||||
makeLine(fx, fy + startSpan * 0.30f, fx, fy + startSpan,
|
||||
fx, fy + endSpan * 0.30f, fx, fy + endSpan);
|
||||
}
|
||||
|
||||
batch.End();
|
||||
|
||||
auto dispatcher = m_dispatcherQueueController.DispatcherQueue();
|
||||
batch.Completed([dispatcher, spawned](auto&&, auto&&) {
|
||||
dispatcher.TryEnqueue([spawned]() {
|
||||
try
|
||||
{
|
||||
if (Highlighter::instance == nullptr || Highlighter::instance->m_shape == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto shapes = Highlighter::instance->m_shape.Shapes();
|
||||
for (auto const& s : *spawned)
|
||||
{
|
||||
uint32_t index = 0;
|
||||
if (shapes.IndexOf(s, index))
|
||||
{
|
||||
shapes.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Highlighter may have torn down between batch completion and dispatch — ignore.
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma region MouseHighlighter_API
|
||||
|
||||
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_LEFT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(166, 255, 255, 0);
|
||||
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_RIGHT_BUTTON_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(166, 0, 0, 255);
|
||||
const winrt::Windows::UI::Color MOUSE_HIGHLIGHTER_DEFAULT_ALWAYS_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(0, 255, 0, 0);
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RADIUS = 20;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS = 500;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS = 250;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RADIUS = 30;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS = 400;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS = 400;
|
||||
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_AUTO_ACTIVATE = false;
|
||||
// Ripple-specific defaults (independent of the always-on circle settings above).
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE = 60;
|
||||
constexpr double MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY = 0.7;
|
||||
constexpr int MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS = 480;
|
||||
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL = true;
|
||||
constexpr bool MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE = true;
|
||||
|
||||
struct MouseHighlighterSettings
|
||||
{
|
||||
@@ -19,6 +25,12 @@ struct MouseHighlighterSettings
|
||||
int fadeDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS;
|
||||
bool autoActivate = MOUSE_HIGHLIGHTER_DEFAULT_AUTO_ACTIVATE;
|
||||
bool spotlightMode = false;
|
||||
bool rippleMode = true;
|
||||
int rippleSize = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE;
|
||||
double rippleIntensity = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY;
|
||||
int rippleDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
|
||||
bool rippleShowDragTrail = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL;
|
||||
bool rippleShowReleasePulse = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE;
|
||||
};
|
||||
|
||||
int MouseHighlighterMain(HINSTANCE hinst, MouseHighlighterSettings settings);
|
||||
|
||||
@@ -21,6 +21,12 @@ namespace
|
||||
const wchar_t JSON_KEY_HIGHLIGHT_FADE_DURATION_MS[] = L"highlight_fade_duration_ms";
|
||||
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
|
||||
const wchar_t JSON_KEY_SPOTLIGHT_MODE[] = L"spotlight_mode";
|
||||
const wchar_t JSON_KEY_RIPPLE_MODE[] = L"ripple_mode";
|
||||
const wchar_t JSON_KEY_RIPPLE_SIZE[] = L"ripple_size";
|
||||
const wchar_t JSON_KEY_RIPPLE_INTENSITY[] = L"ripple_intensity";
|
||||
const wchar_t JSON_KEY_RIPPLE_DURATION_MS[] = L"ripple_duration_ms";
|
||||
const wchar_t JSON_KEY_RIPPLE_SHOW_DRAG_TRAIL[] = L"ripple_show_drag_trail";
|
||||
const wchar_t JSON_KEY_RIPPLE_SHOW_RELEASE_PULSE[] = L"ripple_show_release_pulse";
|
||||
}
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
@@ -392,6 +398,90 @@ public:
|
||||
{
|
||||
Logger::warn("Failed to initialize spotlight mode settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple mode
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_MODE);
|
||||
highlightSettings.rippleMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple mode settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple size
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_SIZE);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value > 0)
|
||||
{
|
||||
highlightSettings.rippleSize = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Invalid ripple size value");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple size from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple intensity
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_INTENSITY);
|
||||
double value = jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE);
|
||||
if (value > 0.0)
|
||||
{
|
||||
highlightSettings.rippleIntensity = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Invalid ripple intensity value");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple intensity from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple duration
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_DURATION_MS);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value > 0)
|
||||
{
|
||||
highlightSettings.rippleDurationMs = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Invalid ripple duration value");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple duration from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple show drag trail
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_SHOW_DRAG_TRAIL);
|
||||
highlightSettings.rippleShowDragTrail = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple show drag trail from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse ripple show release pulse
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_SHOW_RELEASE_PULSE);
|
||||
highlightSettings.rippleShowReleasePulse = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize ripple show release pulse from settings. Will use default value");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natstepfilter" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -119,7 +119,7 @@
|
||||
</resheader>
|
||||
<data name="context_menu_item_new" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="context_menu_item_open_templates" xml:space="preserve">
|
||||
<value>Open templates</value>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</resheader>
|
||||
<data name="context_menu_item_new" xml:space="preserve">
|
||||
<value>New+</value>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+</comment>
|
||||
<comment>The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+, French it would become Nouveau+ (not Nouveauté+)</comment>
|
||||
</data>
|
||||
<data name="context_menu_item_open_templates" xml:space="preserve">
|
||||
<value>Open templates</value>
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
PackageName: BlackmagicDesign.DaVinciResolve
|
||||
Name: DaVinci Resolve
|
||||
WindowFilter: "Resolve.exe"
|
||||
BackgroundProcess: false
|
||||
Shortcuts:
|
||||
- SectionName: Popular shortcuts
|
||||
Properties:
|
||||
- Name: Edit
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F5
|
||||
- Name: Color
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F6
|
||||
- Name: Fairlight
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F7
|
||||
- Name: Deliver
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F8
|
||||
- Name: Play / Pause
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Space
|
||||
- Name: Play Reverse
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- J
|
||||
- Name: Stop
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- K
|
||||
- Name: Play Forward
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- L
|
||||
- Name: Import Media
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- I
|
||||
- Name: Export / Deliver
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- E
|
||||
- Name: Save Project
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- S
|
||||
- Name: Cut Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- B
|
||||
- Name: Blade Edit
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Backslash
|
||||
- Name: Ripple Delete
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Delete
|
||||
- Name: Undo
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Z
|
||||
- Name: Redo
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Z
|
||||
- Name: Mark In
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- I
|
||||
- Name: Mark Out
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- O
|
||||
- Name: Marker
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- M
|
||||
- Name: Select All
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- A
|
||||
- Name: Go to Beginning
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Home
|
||||
- Name: Go to End
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- End
|
||||
- Name: Snapping
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- N
|
||||
- Name: Selection Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- A
|
||||
- Name: Trim Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- T
|
||||
- Name: Change Clip Speed
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- R
|
||||
- SectionName: Timeline navigation
|
||||
Properties:
|
||||
- Name: Go to Next Frame
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Right
|
||||
- Name: Go to Previous Frame
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Left
|
||||
- Name: Jump Forward 5 Frames
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Right
|
||||
- Name: Jump Back 5 Frames
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Left
|
||||
- Name: Go to Next Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Up
|
||||
- Name: Go to Previous Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Down
|
||||
- Name: Go to Next Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Down
|
||||
- Name: Go to Previous Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Up
|
||||
- Name: Zoom In Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Equals
|
||||
- Name: Zoom Out Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Minus
|
||||
- Name: Full Screen Playback
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Space
|
||||
- Name: Go to Previous Edit Point
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- PageUp
|
||||
- Name: Go to Next Edit Point
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- PageDown
|
||||
- SectionName: Edit
|
||||
Properties:
|
||||
- Name: Delete
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Delete
|
||||
- Name: Copy
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- C
|
||||
- Name: Paste
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- V
|
||||
- Name: Cut
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- X
|
||||
- Name: Duplicate Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- D
|
||||
- Name: Render in Place
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- X
|
||||
- Name: Add Edit
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Backslash
|
||||
- Name: Append to End of Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- End
|
||||
- Name: Replace Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- R
|
||||
- Name: Move Clip Up One Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Up
|
||||
- Name: Move Clip Down One Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Down
|
||||
- Name: Split Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- B
|
||||
- Name: Link Clips
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- L
|
||||
- Name: Create Compound Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- G
|
||||
- SectionName: Color
|
||||
Properties:
|
||||
- Name: Add Serial Node
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- S
|
||||
- Name: Add Parallel Node
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- P
|
||||
- Name: Add Layer Node
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: true
|
||||
Keys:
|
||||
- L
|
||||
- Name: Select Node 1
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- Name: Select Node 2
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "2"
|
||||
- Name: Select Node 3
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "3"
|
||||
- Name: Select Node 4
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "4"
|
||||
- Name: Select Node 5
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "5"
|
||||
- Name: Enable/Disable Current Grade
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- D
|
||||
- Name: Preview Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- W
|
||||
- Name: Grade All Frames in Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- D
|
||||
- Name: Keyframe Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- K
|
||||
- Name: Select Color Wheels
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- Name: Select Curves
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "2"
|
||||
- Name: Select Qualifier
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "3"
|
||||
- Name: Select Power Window
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "4"
|
||||
- Name: Select Tracking
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "5"
|
||||
- Name: Reset Color Grade
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- U
|
||||
- SectionName: Fairlight
|
||||
Properties:
|
||||
- Name: Mute Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- M
|
||||
- Name: Solo Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- S
|
||||
- Name: Automation Mode
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F
|
||||
- Name: Record Arm Selected Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- R
|
||||
- Name: Headphones Solo
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- H
|
||||
- Name: Add Marker
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Insert
|
||||
- Name: Add Audio Track
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- A
|
||||
- Name: Bounce Mix
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- X
|
||||
- SectionName: Fusion
|
||||
Properties:
|
||||
- Name: Switch Between Spline and Keyframes
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- K
|
||||
- Name: Add Keyframe
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Shift
|
||||
- Name: View Current Tool
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- Name: View Node Flow
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "2"
|
||||
- Name: View Keyframes
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "3"
|
||||
- Name: View Spline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "4"
|
||||
- Name: Merge Selected Tools
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- N
|
||||
- Name: Bypass Selected Tool
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- "1"
|
||||
- SectionName: Media
|
||||
Properties:
|
||||
- Name: Reveal in Explorer
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- E
|
||||
- Name: Smart Bin
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- S
|
||||
- Name: Rename Clip
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F2
|
||||
- Name: Import XML / AAF
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- I
|
||||
- Name: Create New Bin
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- N
|
||||
- Name: Add Clip to Timeline
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Enter
|
||||
- Name: Viewer Zoom In
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Equals
|
||||
- Name: Viewer Zoom Out
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Minus
|
||||
- SectionName: Deliver
|
||||
Properties:
|
||||
- Name: Add to Render Queue
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- Enter
|
||||
- Name: Start Render
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: true
|
||||
Alt: false
|
||||
Keys:
|
||||
- Enter
|
||||
- Name: Select Preset
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: false
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- F2
|
||||
- Name: Render Settings
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- E
|
||||
- Name: Browse Output Location
|
||||
Shortcut:
|
||||
- Win: false
|
||||
Ctrl: true
|
||||
Shift: false
|
||||
Alt: false
|
||||
Keys:
|
||||
- B
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
@@ -12,25 +11,17 @@ using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Windows.Management.Deployment;
|
||||
|
||||
namespace WorkspacesCsharpLibrary.Models
|
||||
{
|
||||
public partial class BaseApplication : INotifyPropertyChanged, IDisposable
|
||||
public partial class BaseApplication : ObservableObject, IDisposable
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public string PwaAppId { get; set; }
|
||||
|
||||
public string AppPath { get; set; }
|
||||
|
||||
private bool _isNotFound;
|
||||
|
||||
public string PackagedId { get; set; }
|
||||
|
||||
public string PackagedName { get; set; }
|
||||
@@ -39,23 +30,9 @@ namespace WorkspacesCsharpLibrary.Models
|
||||
|
||||
public string Aumid { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsNotFound
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isNotFound;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_isNotFound != value)
|
||||
{
|
||||
_isNotFound = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNotFound)));
|
||||
}
|
||||
}
|
||||
}
|
||||
[ObservableProperty]
|
||||
[property: JsonIgnore]
|
||||
private bool _isNotFound;
|
||||
|
||||
private Icon _icon;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
// 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.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesEditor.Models;
|
||||
|
||||
namespace WorkspacesEditor.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the Application model: state toggles, computed properties,
|
||||
/// position management, and copy semantics.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ApplicationModelTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void SwitchDeletion_InitiallyIncluded_TogglesOff()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "Notepad");
|
||||
var app = project.Applications[0];
|
||||
app.IsIncluded = true;
|
||||
|
||||
app.IsIncluded = !app.IsIncluded;
|
||||
|
||||
Assert.IsFalse(app.IsIncluded);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void SwitchDeletion_InitiallyExcluded_TogglesOn()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "Notepad");
|
||||
var app = project.Applications[0];
|
||||
app.IsIncluded = false;
|
||||
|
||||
app.IsIncluded = !app.IsIncluded;
|
||||
|
||||
Assert.IsTrue(app.IsIncluded);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void SwitchDeletion_DoubleToggle_ReturnsToOriginal()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "Notepad");
|
||||
var app = project.Applications[0];
|
||||
app.IsIncluded = true;
|
||||
|
||||
app.IsIncluded = !app.IsIncluded;
|
||||
app.IsIncluded = !app.IsIncluded;
|
||||
|
||||
Assert.IsTrue(app.IsIncluded);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void AppMainParams_NotElevatedNoArgs_ReturnsEmpty()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "Notepad");
|
||||
var app = project.Applications[0];
|
||||
app.IsElevated = false;
|
||||
app.CommandLineArguments = string.Empty;
|
||||
|
||||
Assert.AreEqual(string.Empty, app.AppMainParams);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void AppMainParams_ElevatedNoArgs_ContainsText()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "Regedit");
|
||||
var app = project.Applications[0];
|
||||
app.IsElevated = true;
|
||||
app.CommandLineArguments = string.Empty;
|
||||
|
||||
Assert.IsTrue(app.AppMainParams.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void AppMainParams_NotElevatedWithArgs_ContainsArgs()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "Code");
|
||||
var app = project.Applications[0];
|
||||
app.IsElevated = false;
|
||||
app.CommandLineArguments = "--new-window";
|
||||
|
||||
Assert.IsTrue(app.AppMainParams.Contains("--new-window", System.StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void AppMainParams_ElevatedWithArgs_ContainsBoth()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "Code");
|
||||
var app = project.Applications[0];
|
||||
app.IsElevated = true;
|
||||
app.CommandLineArguments = "--reuse-window";
|
||||
|
||||
var result = app.AppMainParams;
|
||||
Assert.IsTrue(result.Contains("--reuse-window", System.StringComparison.Ordinal));
|
||||
Assert.IsTrue(result.Contains('|'), "Should have separator between admin and args");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void PositionComboboxIndex_Custom_ReturnsZero()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.Minimized = false;
|
||||
app.Maximized = false;
|
||||
|
||||
Assert.AreEqual(0, app.PositionComboboxIndex);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void PositionComboboxIndex_Maximized_ReturnsOne()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.Minimized = false;
|
||||
app.Maximized = true;
|
||||
|
||||
Assert.AreEqual(1, app.PositionComboboxIndex);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void PositionComboboxIndex_Minimized_ReturnsTwo()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.Minimized = true;
|
||||
app.Maximized = false;
|
||||
|
||||
Assert.AreEqual(2, app.PositionComboboxIndex);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void EditPositionEnabled_CustomPosition_ReturnsTrue()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.Minimized = false;
|
||||
app.Maximized = false;
|
||||
|
||||
Assert.IsTrue(app.EditPositionEnabled);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void EditPositionEnabled_Maximized_ReturnsFalse()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.Maximized = true;
|
||||
|
||||
Assert.IsFalse(app.EditPositionEnabled);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void EditPositionEnabled_Minimized_ReturnsFalse()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.Minimized = true;
|
||||
|
||||
Assert.IsFalse(app.EditPositionEnabled);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void RepeatIndexString_IndexZeroOrOne_ReturnsEmpty()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
|
||||
app.RepeatIndex = 0;
|
||||
Assert.AreEqual(string.Empty, app.RepeatIndexString);
|
||||
|
||||
app.RepeatIndex = 1;
|
||||
Assert.AreEqual(string.Empty, app.RepeatIndexString);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void RepeatIndexString_IndexGreaterThanOne_ReturnsNumber()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
|
||||
app.RepeatIndex = 2;
|
||||
Assert.AreEqual("2", app.RepeatIndexString);
|
||||
|
||||
app.RepeatIndex = 5;
|
||||
Assert.AreEqual("5", app.RepeatIndexString);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void WindowPosition_Equality_SameValues_ReturnsTrue()
|
||||
{
|
||||
var pos1 = new Application.WindowPosition { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
var pos2 = new Application.WindowPosition { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
|
||||
Assert.IsTrue(pos1 == pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void WindowPosition_Inequality_DifferentValues_ReturnsTrue()
|
||||
{
|
||||
var pos1 = new Application.WindowPosition { X = 0, Y = 0, Width = 1920, Height = 1080 };
|
||||
var pos2 = new Application.WindowPosition { X = 960, Y = 0, Width = 960, Height = 1080 };
|
||||
|
||||
Assert.IsTrue(pos1 != pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void CopyConstructor_CopiesAllFields()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "VS Code");
|
||||
var original = project.Applications[0];
|
||||
original.CommandLineArguments = "--new-window";
|
||||
original.IsElevated = true;
|
||||
original.Maximized = true;
|
||||
original.MonitorNumber = 2;
|
||||
original.RepeatIndex = 3;
|
||||
|
||||
var copy = new Application(original);
|
||||
|
||||
Assert.AreEqual(original.AppName, copy.AppName);
|
||||
Assert.AreEqual(original.CommandLineArguments, copy.CommandLineArguments);
|
||||
Assert.AreEqual(original.IsElevated, copy.IsElevated);
|
||||
Assert.AreEqual(original.Maximized, copy.Maximized);
|
||||
Assert.AreEqual(original.MonitorNumber, copy.MonitorNumber);
|
||||
Assert.AreEqual(original.RepeatIndex, copy.RepeatIndex);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void IsAppMainParamVisible_EmptyParams_ReturnsFalse()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.IsElevated = false;
|
||||
app.CommandLineArguments = string.Empty;
|
||||
|
||||
_ = app.AppMainParams;
|
||||
Assert.IsFalse(app.IsAppMainParamVisible);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Application")]
|
||||
public void IsAppMainParamVisible_HasParams_ReturnsTrue()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
var app = project.Applications[0];
|
||||
app.IsElevated = true;
|
||||
|
||||
_ = app.AppMainParams;
|
||||
Assert.IsTrue(app.IsAppMainParamVisible);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for MainViewModel search and filter logic.
|
||||
/// The search filters workspaces by name and app name (case-insensitive, partial match).
|
||||
/// This behavior must be preserved after the WinUI migration.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class EditorViewModelSearchAndFilterTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_Empty_ReturnsAllWorkspaces()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("DevSetup", 0, 0, "VS Code", "Terminal"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge", "Notepad"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = string.Empty;
|
||||
Assert.AreEqual(2, vm.WorkspacesView.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_Null_ReturnsAllWorkspaces()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("DevSetup", 0, 0, "VS Code"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = null;
|
||||
vm.RefreshWorkspacesView();
|
||||
Assert.AreEqual(2, vm.WorkspacesView.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_MatchesWorkspaceName_ReturnsMatching()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("DevSetup", 0, 0, "VS Code"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge"),
|
||||
TestHelpers.CreateProject("DesignWork", 0, 0, "Figma"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = "Dev";
|
||||
var results = vm.WorkspacesView.ToList();
|
||||
|
||||
Assert.AreEqual(1, results.Count);
|
||||
Assert.AreEqual("DevSetup", results[0].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_MatchesAppName_ReturnsWorkspaceContainingApp()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("DevSetup", 0, 0, "VS Code", "Terminal"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge", "Notepad"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = "Terminal";
|
||||
var results = vm.WorkspacesView.ToList();
|
||||
|
||||
Assert.AreEqual(1, results.Count);
|
||||
Assert.AreEqual("DevSetup", results[0].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_CaseInsensitive_MatchesRegardlessOfCase()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("DevSetup", 0, 0, "VS Code"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = "devsetup";
|
||||
Assert.AreEqual(1, vm.WorkspacesView.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_NoMatch_ReturnsEmpty()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("DevSetup", 0, 0, "VS Code"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = "NonExistent";
|
||||
Assert.AreEqual(0, vm.WorkspacesView.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_PartialMatch_MatchesSubstring()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("MyDevelopmentSetup", 0, 0, "VS Code"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = "Develop";
|
||||
Assert.AreEqual(1, vm.WorkspacesView.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_MatchesMultiple_ReturnsAll()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("DevSetup1", 0, 0, "VS Code"),
|
||||
TestHelpers.CreateProject("DevSetup2", 0, 0, "Terminal"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = "Dev";
|
||||
Assert.AreEqual(2, vm.WorkspacesView.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_Changed_RaisesPropertyChangedForWorkspacesView()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("Test", 0, 0, "App"),
|
||||
};
|
||||
|
||||
var changedProps = new System.Collections.Generic.List<string>();
|
||||
vm.PropertyChanged += (s, e) => changedProps.Add(e.PropertyName);
|
||||
|
||||
vm.SearchTerm = "Test";
|
||||
Assert.IsTrue(changedProps.Contains("WorkspacesView"), $"Expected WorkspacesView in [{string.Join(", ", changedProps)}]");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_EmptyCollection_ReturnsEmptyAndSetsFlag()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>();
|
||||
|
||||
vm.SearchTerm = "anything";
|
||||
Assert.AreEqual(0, vm.WorkspacesView.Count);
|
||||
Assert.IsTrue(vm.IsWorkspacesViewEmpty);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Search")]
|
||||
public void SearchTerm_MatchesAppNameCaseInsensitive_ReturnsWorkspace()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("MySetup", 0, 0, "Visual Studio Code"),
|
||||
};
|
||||
|
||||
vm.SearchTerm = "visual studio";
|
||||
Assert.AreEqual(1, vm.WorkspacesView.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for MainViewModel sort logic.
|
||||
/// Sorting affects the order of WorkspacesView: by name, creation time, or last-launched.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class EditorViewModelSortTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Sort")]
|
||||
public void Sort_ByName_ReturnsAlphabeticalOrder()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("Zebra", 0, 0, "App"),
|
||||
TestHelpers.CreateProject("Alpha", 0, 0, "App"),
|
||||
TestHelpers.CreateProject("Middle", 0, 0, "App"),
|
||||
};
|
||||
|
||||
vm.OrderByIndex = 2; // Name
|
||||
vm.RefreshWorkspacesView();
|
||||
var results = vm.WorkspacesView.ToList();
|
||||
|
||||
Assert.AreEqual("Alpha", results[0].Name);
|
||||
Assert.AreEqual("Middle", results[1].Name);
|
||||
Assert.AreEqual("Zebra", results[2].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Sort")]
|
||||
public void Sort_ByCreated_ReturnsNewestFirst()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("Oldest", 1000, 0, "App"),
|
||||
TestHelpers.CreateProject("Newest", 3000, 0, "App"),
|
||||
TestHelpers.CreateProject("Middle", 2000, 0, "App"),
|
||||
};
|
||||
|
||||
vm.OrderByIndex = 1; // Created (descending)
|
||||
vm.RefreshWorkspacesView();
|
||||
var results = vm.WorkspacesView.ToList();
|
||||
|
||||
Assert.AreEqual("Newest", results[0].Name);
|
||||
Assert.AreEqual("Middle", results[1].Name);
|
||||
Assert.AreEqual("Oldest", results[2].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Sort")]
|
||||
public void Sort_ByLastViewed_ReturnsMostRecentFirst()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("LeastRecent", 0, 1000, "App"),
|
||||
TestHelpers.CreateProject("MostRecent", 0, 3000, "App"),
|
||||
TestHelpers.CreateProject("Middle", 0, 2000, "App"),
|
||||
};
|
||||
|
||||
vm.OrderByIndex = 0; // LastViewed (descending)
|
||||
vm.RefreshWorkspacesView();
|
||||
var results = vm.WorkspacesView.ToList();
|
||||
|
||||
Assert.AreEqual("MostRecent", results[0].Name);
|
||||
Assert.AreEqual("Middle", results[1].Name);
|
||||
Assert.AreEqual("LeastRecent", results[2].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Sort")]
|
||||
public void Sort_OrderByIndex_RaisesPropertyChanged()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>();
|
||||
|
||||
var changedProps = new System.Collections.Generic.List<string>();
|
||||
vm.PropertyChanged += (s, e) => changedProps.Add(e.PropertyName);
|
||||
|
||||
vm.OrderByIndex = 1;
|
||||
Assert.IsTrue(changedProps.Contains("WorkspacesView"), $"Expected WorkspacesView in [{string.Join(", ", changedProps)}]");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel.Sort")]
|
||||
public void Sort_CombinedWithFilter_FilteredResultsAreSorted()
|
||||
{
|
||||
var vm = TestHelpers.CreateViewModel();
|
||||
vm.Workspaces = new ObservableCollection<Project>
|
||||
{
|
||||
TestHelpers.CreateProject("Z Dev", 0, 0, "VS Code"),
|
||||
TestHelpers.CreateProject("A Dev", 0, 0, "Terminal"),
|
||||
TestHelpers.CreateProject("Browsing", 0, 0, "Edge"),
|
||||
};
|
||||
|
||||
vm.OrderByIndex = 2; // Name
|
||||
vm.SearchTerm = "Dev";
|
||||
var results = vm.WorkspacesView.ToList();
|
||||
|
||||
Assert.AreEqual(2, results.Count);
|
||||
Assert.AreEqual("A Dev", results[0].Name);
|
||||
Assert.AreEqual("Z Dev", results[1].Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace WorkspacesEditor.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for MainWindow configuration constants and constraints.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MainWindowConstraintTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("Window.Constraints")]
|
||||
public void MinWindowWidth_IsAtLeast750()
|
||||
{
|
||||
Assert.IsTrue(MainWindow.MinWindowWidth >= 750, "Min width must be at least 750 to fit all UI elements.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Window.Constraints")]
|
||||
public void MinWindowHeight_IsAtLeast680()
|
||||
{
|
||||
Assert.IsTrue(MainWindow.MinWindowHeight >= 680, "Min height must be at least 680 to fit all UI elements.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Window.Constraints")]
|
||||
public void MinWindowDimensions_AreReasonable()
|
||||
{
|
||||
// Ensure min size isn't accidentally set too large (e.g., exceeding common displays)
|
||||
Assert.IsTrue(MainWindow.MinWindowWidth <= 1024, "Min width should not exceed 1024.");
|
||||
Assert.IsTrue(MainWindow.MinWindowHeight <= 768, "Min height should not exceed 768.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesEditor.Models;
|
||||
|
||||
namespace WorkspacesEditor.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for Project model validation, computed properties, and state management.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ProjectModelValidationTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void CanBeSaved_NameAndAppsPresent_ReturnsTrue()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("My Workspace", 0, 0, "Notepad");
|
||||
Assert.IsTrue(project.CanBeSaved);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void CanBeSaved_EmptyName_ReturnsFalse()
|
||||
{
|
||||
var project = TestHelpers.CreateProject(string.Empty, 0, 0, "Notepad");
|
||||
Assert.IsFalse(project.CanBeSaved);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void CanBeSaved_NoApps_ReturnsFalse()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test Workspace");
|
||||
Assert.IsFalse(project.CanBeSaved);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void Name_SetValue_RaisesPropertyChanged()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Initial", 0, 0, "App");
|
||||
|
||||
var changedProps = new List<string>();
|
||||
project.PropertyChanged += (s, e) => changedProps.Add(e.PropertyName);
|
||||
|
||||
project.Name = "Changed";
|
||||
|
||||
Assert.IsTrue(changedProps.Contains("Name"));
|
||||
Assert.IsTrue(changedProps.Contains("CanBeSaved"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void AppsCountString_SingleApp_ContainsOne()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App1");
|
||||
Assert.IsTrue(project.AppsCountString.StartsWith('1'));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void AppsCountString_MultipleApps_ContainsCount()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App1", "App2", "App3");
|
||||
Assert.IsTrue(project.AppsCountString.StartsWith('3'));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void LastLaunched_NeverLaunched_ReturnsNonEmptyString()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
Assert.IsTrue(project.LastLaunched.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void IsRevertEnabled_SetTrue_RaisesPropertyChanged()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
|
||||
string changedProp = null;
|
||||
project.PropertyChanged += (s, e) => changedProp = e.PropertyName;
|
||||
|
||||
project.IsRevertEnabled = true;
|
||||
Assert.AreEqual("IsRevertEnabled", changedProp);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void IsPopupVisible_SetTrue_RaisesPropertyChanged()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
|
||||
string changedProp = null;
|
||||
project.PropertyChanged += (s, e) => changedProp = e.PropertyName;
|
||||
|
||||
project.IsPopupVisible = true;
|
||||
Assert.AreEqual("IsPopupVisible", changedProp);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void Name_Changed_UpdatesCanBeSaved()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Valid", 0, 0, "App");
|
||||
Assert.IsTrue(project.CanBeSaved);
|
||||
|
||||
project.Name = string.Empty;
|
||||
Assert.IsFalse(project.CanBeSaved);
|
||||
|
||||
project.Name = "Valid Again";
|
||||
Assert.IsTrue(project.CanBeSaved);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void MoveExistingWindows_DefaultFalse_CanBeSet()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
Assert.IsFalse(project.MoveExistingWindows);
|
||||
|
||||
project.MoveExistingWindows = true;
|
||||
Assert.IsTrue(project.MoveExistingWindows);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model.Project")]
|
||||
public void IsShortcutNeeded_DefaultFalse_CanBeSet()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App");
|
||||
Assert.IsFalse(project.IsShortcutNeeded);
|
||||
|
||||
project.IsShortcutNeeded = true;
|
||||
Assert.IsTrue(project.IsShortcutNeeded);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared helpers for creating test fixtures.
|
||||
/// Constructs Project and Application objects via the same constructors
|
||||
/// used in production (ProjectWrapper deserialization path).
|
||||
/// </summary>
|
||||
internal static class TestHelpers
|
||||
{
|
||||
internal static MainViewModel CreateViewModel()
|
||||
{
|
||||
return new MainViewModel(new Utils.WorkspacesEditorIO());
|
||||
}
|
||||
|
||||
internal static Project CreateProject(string name, long creationTime = 0, long lastLaunchedTime = 0, params string[] appNames)
|
||||
{
|
||||
var appWrappers = appNames.Select(n => new ApplicationWrapper
|
||||
{
|
||||
Application = n,
|
||||
ApplicationPath = $@"C:\{n}.exe",
|
||||
Title = string.Empty,
|
||||
PackageFullName = string.Empty,
|
||||
AppUserModelId = string.Empty,
|
||||
PwaAppId = string.Empty,
|
||||
CommandLineArguments = string.Empty,
|
||||
IsElevated = false,
|
||||
CanLaunchElevated = false,
|
||||
Minimized = false,
|
||||
Maximized = false,
|
||||
Position = default,
|
||||
Monitor = 0,
|
||||
}).ToList();
|
||||
|
||||
var projectWrapper = new ProjectWrapper
|
||||
{
|
||||
Id = $"{{{Guid.NewGuid()}}}",
|
||||
Name = name,
|
||||
CreationTime = creationTime,
|
||||
LastLaunchedTime = lastLaunchedTime,
|
||||
IsShortcutNeeded = false,
|
||||
MoveExistingWindows = false,
|
||||
Applications = appWrappers,
|
||||
MonitorConfiguration = new List<MonitorConfigurationWrapper>(),
|
||||
};
|
||||
|
||||
return new Project(projectWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesEditor.Models;
|
||||
|
||||
namespace WorkspacesEditor.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Smoke test to verify the test infrastructure compiles and Project/Application
|
||||
/// objects can be created for testing.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class TestInfrastructureTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("Infrastructure")]
|
||||
public void CreateProject_WithApps_ReturnsValidProject()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("TestWorkspace", 0, 0, "Notepad", "VS Code");
|
||||
|
||||
Assert.IsNotNull(project);
|
||||
Assert.AreEqual("TestWorkspace", project.Name);
|
||||
Assert.AreEqual(2, project.Applications.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Infrastructure")]
|
||||
public void CreateProject_ApplicationNames_AreCorrect()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("Test", 0, 0, "App1", "App2", "App3");
|
||||
|
||||
Assert.AreEqual("App1", project.Applications[0].AppName);
|
||||
Assert.AreEqual("App2", project.Applications[1].AppName);
|
||||
Assert.AreEqual("App3", project.Applications[2].AppName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Infrastructure")]
|
||||
public void CreateProject_NoApps_ReturnsEmptyApplicationsList()
|
||||
{
|
||||
var project = TestHelpers.CreateProject("EmptyWorkspace");
|
||||
|
||||
Assert.IsNotNull(project.Applications);
|
||||
Assert.AreEqual(0, project.Applications.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\WorkspacesEditor.Tests\</OutputPath>
|
||||
<RootNamespace>WorkspacesEditor.UnitTests</RootNamespace>
|
||||
<AssemblyName>PowerToys.WorkspacesEditor.Tests</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesEditor.WinUI\WorkspacesEditor.WinUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,19 +1,19 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace WorkspacesEditor.Converters
|
||||
{
|
||||
public class BooleanToInvertedVisibilityConverter : IValueConverter
|
||||
public partial class BooleanToInvertedVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if ((bool)value)
|
||||
if (value is bool boolValue && boolValue)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace WorkspacesEditor.Converters
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace WorkspacesEditor.Converters
|
||||
{
|
||||
public partial class BooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue && boolValue)
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using WorkspacesEditor.Helpers;
|
||||
|
||||
namespace WorkspacesEditor.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a workspace name to a contextual button label like "Launch MyWorkspace".
|
||||
/// </summary>
|
||||
public sealed partial class LaunchButtonNameConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
string name = value as string ?? string.Empty;
|
||||
string launchStr = ResourceLoaderInstance.ResourceLoader?.GetString("Launch") ?? "Launch";
|
||||
return $"{launchStr} {name}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using WorkspacesEditor.Helpers;
|
||||
|
||||
namespace WorkspacesEditor.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a workspace name to a contextual label like "More options for MyWorkspace".
|
||||
/// </summary>
|
||||
public sealed partial class MoreOptionsButtonNameConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
string name = value as string ?? string.Empty;
|
||||
string moreOptionsStr = ResourceLoaderInstance.ResourceLoader?.GetString("MoreOptions") ?? "More options";
|
||||
return $"{moreOptionsStr} {name}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
namespace WorkspacesEditor.Helpers
|
||||
{
|
||||
internal static class IconHelper
|
||||
{
|
||||
public static BitmapImage TryGetExecutableIcon(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using Icon icon = Icon.ExtractAssociatedIcon(path);
|
||||
if (icon is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using Bitmap bitmap = icon.ToBitmap();
|
||||
using MemoryStream stream = new();
|
||||
bitmap.Save(stream, ImageFormat.Png);
|
||||
stream.Position = 0;
|
||||
|
||||
BitmapImage bitmapImage = new();
|
||||
bitmapImage.SetSource(stream.AsRandomAccessStream());
|
||||
return bitmapImage;
|
||||
}
|
||||
catch (Exception ex) when (ex is FileNotFoundException
|
||||
or UnauthorizedAccessException
|
||||
or Win32Exception
|
||||
or ArgumentException
|
||||
or IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
internal static class ResourceLoaderInstance
|
||||
{
|
||||
private static ResourceLoader _resourceLoader;
|
||||
|
||||
internal static ResourceLoader ResourceLoader
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_resourceLoader == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_resourceLoader = new ResourceLoader("PowerToys.WorkspacesEditor.pri");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load ResourceLoader: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return _resourceLoader;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.UI.Xaml;
|
||||
|
||||
namespace WorkspacesEditor.Helpers
|
||||
{
|
||||
internal static class ThemeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the current app theme is dark.
|
||||
/// Uses WinUI Application.RequestedTheme which respects system settings.
|
||||
/// </summary>
|
||||
internal static bool IsDarkTheme()
|
||||
{
|
||||
if (Application.Current?.RequestedTheme == ApplicationTheme.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace WorkspacesEditor.Helpers
|
||||
{
|
||||
public class WindowStateData
|
||||
{
|
||||
[JsonPropertyName("top")]
|
||||
public double Top { get; set; }
|
||||
|
||||
[JsonPropertyName("left")]
|
||||
public double Left { get; set; }
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
public double Width { get; set; }
|
||||
|
||||
[JsonPropertyName("height")]
|
||||
public double Height { get; set; }
|
||||
|
||||
[JsonPropertyName("maximized")]
|
||||
public bool Maximized { get; set; }
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Width > 0 && Height > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Text.Json;
|
||||
|
||||
using ManagedCommon;
|
||||
|
||||
namespace WorkspacesEditor.Helpers
|
||||
{
|
||||
internal static class WindowStateHelper
|
||||
{
|
||||
private static readonly string StateFilePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft",
|
||||
"PowerToys",
|
||||
"Workspaces",
|
||||
"editor-window-state.json");
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true };
|
||||
|
||||
public static WindowStateData Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(StateFilePath))
|
||||
{
|
||||
string json = File.ReadAllText(StateFilePath);
|
||||
return JsonSerializer.Deserialize<WindowStateData>(json);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load editor window state", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void Save(WindowStateData state)
|
||||
{
|
||||
try
|
||||
{
|
||||
string directory = Path.GetDirectoryName(StateFilePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = JsonSerializer.Serialize(state, SerializerOptions);
|
||||
File.WriteAllText(StateFilePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to save editor window state", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent to request graceful application shutdown via the WinUI lifecycle.
|
||||
/// </summary>
|
||||
public sealed class CloseApplicationMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent by ViewModel to request navigation back to the main page.
|
||||
/// </summary>
|
||||
public sealed class GoBackMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent by ViewModel to request the main window be minimized.
|
||||
/// </summary>
|
||||
public sealed class MinimizeWindowMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 WorkspacesEditor.Models;
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent by ViewModel to request navigation to the editor page for a project.
|
||||
/// </summary>
|
||||
public sealed class NavigateToEditorMessage
|
||||
{
|
||||
public Project Project { get; }
|
||||
|
||||
public NavigateToEditorMessage(Project project)
|
||||
{
|
||||
Project = project;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent by ViewModel to request the main window be restored from minimized state.
|
||||
/// </summary>
|
||||
public sealed class RestoreWindowMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent by ViewModel to request the View layer show the snapshot window.
|
||||
/// </summary>
|
||||
public sealed class ShowSnapshotWindowMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent by SnapshotWindow when user cancels (closes without capturing).
|
||||
/// </summary>
|
||||
public sealed class SnapshotCancelledMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Sent by SnapshotWindow when user clicks Capture.
|
||||
/// </summary>
|
||||
public sealed class SnapshotCapturedMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
public sealed partial class AppListDataTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate HeaderTemplate { get; set; }
|
||||
|
||||
public DataTemplate AppTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
return item is MonitorHeaderRow ? HeaderTemplate : AppTemplate;
|
||||
}
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
return SelectTemplateCore(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,15 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
using WorkspacesCsharpLibrary.Models;
|
||||
using WorkspacesEditor.Helpers;
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
@@ -17,7 +22,7 @@ namespace WorkspacesEditor.Models
|
||||
Minimized = 2,
|
||||
}
|
||||
|
||||
public class Application : BaseApplication, IDisposable
|
||||
public partial class Application : BaseApplication, IDisposable
|
||||
{
|
||||
private bool _isInitialized;
|
||||
|
||||
@@ -90,7 +95,7 @@ namespace WorkspacesEditor.Models
|
||||
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
return HashCode.Combine(X, Y, Width, Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,18 +111,11 @@ namespace WorkspacesEditor.Models
|
||||
|
||||
public string CommandLineArguments { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(AppMainParams))]
|
||||
[NotifyPropertyChangedFor(nameof(IsAppMainParamVisible))]
|
||||
private bool _isElevated;
|
||||
|
||||
public bool IsElevated
|
||||
{
|
||||
get => _isElevated;
|
||||
set
|
||||
{
|
||||
_isElevated = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppMainParams)));
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanLaunchElevated { get; set; }
|
||||
|
||||
internal void SwitchDeletion()
|
||||
@@ -130,7 +128,7 @@ namespace WorkspacesEditor.Models
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Parent.Initialize(App.GetCurrentTheme());
|
||||
Parent?.InitializePreview();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,35 +145,37 @@ namespace WorkspacesEditor.Models
|
||||
{
|
||||
Maximized = value == (int)WindowPositionKind.Maximized;
|
||||
Minimized = value == (int)WindowPositionKind.Minimized;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(EditPositionEnabled)));
|
||||
OnPropertyChanged(nameof(EditPositionEnabled));
|
||||
RedrawPreviewImage();
|
||||
}
|
||||
}
|
||||
|
||||
private string _appMainParams;
|
||||
|
||||
public string AppMainParams
|
||||
{
|
||||
get
|
||||
{
|
||||
_appMainParams = _isElevated ? Properties.Resources.Admin : string.Empty;
|
||||
string adminStr = ResourceLoaderInstance.ResourceLoader?.GetString("Admin") ?? "Admin";
|
||||
string argsStr = ResourceLoaderInstance.ResourceLoader?.GetString("Args") ?? "Args";
|
||||
|
||||
string result = IsElevated ? adminStr : string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(CommandLineArguments))
|
||||
{
|
||||
_appMainParams += (_appMainParams == string.Empty ? string.Empty : " | ") + Properties.Resources.Args + ": " + CommandLineArguments;
|
||||
result += (result == string.Empty ? string.Empty : " | ") + argsStr + ": " + CommandLineArguments;
|
||||
}
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsAppMainParamVisible)));
|
||||
return _appMainParams;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAppMainParamVisible => !string.IsNullOrWhiteSpace(_appMainParams);
|
||||
public bool IsAppMainParamVisible => !string.IsNullOrWhiteSpace(AppMainParams);
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsHighlighted { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int RepeatIndex { get; set; }
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(RepeatIndexString))]
|
||||
[property: JsonIgnore]
|
||||
private int _repeatIndex;
|
||||
|
||||
[JsonIgnore]
|
||||
public string RepeatIndexString => RepeatIndex <= 1 ? string.Empty : RepeatIndex.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -231,51 +231,64 @@ namespace WorkspacesEditor.Models
|
||||
public void InitializationFinished()
|
||||
{
|
||||
_isInitialized = true;
|
||||
LoadIcon();
|
||||
}
|
||||
|
||||
private void LoadIcon()
|
||||
{
|
||||
_iconImage = IconHelper.TryGetExecutableIcon(AppPath);
|
||||
if (_iconImage == null && !string.IsNullOrEmpty(AppPath))
|
||||
{
|
||||
IsNotFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isExpanded;
|
||||
|
||||
public bool IsExpanded
|
||||
public string DeleteButtonContent
|
||||
{
|
||||
get => _isExpanded;
|
||||
set
|
||||
get
|
||||
{
|
||||
if (_isExpanded != value)
|
||||
{
|
||||
_isExpanded = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsExpanded)));
|
||||
}
|
||||
string deleteStr = ResourceLoaderInstance.ResourceLoader?.GetString("Delete") ?? "Remove";
|
||||
string addBackStr = ResourceLoaderInstance.ResourceLoader?.GetString("AddBack") ?? "Add back";
|
||||
return IsIncluded ? deleteStr : addBackStr;
|
||||
}
|
||||
}
|
||||
|
||||
public string DeleteButtonContent => _isIncluded ? Properties.Resources.Delete : Properties.Resources.AddBack;
|
||||
public string DeleteButtonAccessibleName => $"{DeleteButtonContent} {AppName}";
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(DeleteButtonContent))]
|
||||
[NotifyPropertyChangedFor(nameof(DeleteButtonAccessibleName))]
|
||||
private bool _isIncluded = true;
|
||||
|
||||
public bool IsIncluded
|
||||
partial void OnIsIncludedChanged(bool value)
|
||||
{
|
||||
get => _isIncluded;
|
||||
set
|
||||
if (!value)
|
||||
{
|
||||
if (_isIncluded != value)
|
||||
{
|
||||
_isIncluded = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsIncluded)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(DeleteButtonContent)));
|
||||
if (!_isIncluded)
|
||||
{
|
||||
IsExpanded = false;
|
||||
}
|
||||
}
|
||||
IsExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapImage _iconImage;
|
||||
|
||||
[JsonIgnore]
|
||||
public BitmapImage IconImage => _iconImage;
|
||||
|
||||
internal void CommandLineTextChanged(string newCommandLineValue)
|
||||
{
|
||||
CommandLineArguments = newCommandLineValue;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppMainParams)));
|
||||
OnPropertyChanged(nameof(AppMainParams));
|
||||
OnPropertyChanged(nameof(IsAppMainParamVisible));
|
||||
}
|
||||
|
||||
public string Version { get; set; }
|
||||
|
||||
public new void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Windows;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.ComponentModel;
|
||||
using System.Windows;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
public class MonitorSetup : Monitor, INotifyPropertyChanged
|
||||
public partial class MonitorSetup : Monitor
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public string MonitorInfo => MonitorName;
|
||||
|
||||
public string MonitorInfoWithResolution => $"{MonitorName} {MonitorDpiAwareBounds.Width}x{MonitorDpiAwareBounds.Height}";
|
||||
337
src/modules/Workspaces/WorkspacesEditor.WinUI/Models/Project.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
using Windows.Foundation;
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesEditor.Helpers;
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
public partial class Project : ObservableObject
|
||||
{
|
||||
[JsonIgnore]
|
||||
public string EditorWindowTitle { get; set; }
|
||||
|
||||
public string Id { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanBeSaved))]
|
||||
private string _name;
|
||||
|
||||
public long CreationTime { get; }
|
||||
|
||||
public long LastLaunchedTime { get; }
|
||||
|
||||
public bool IsShortcutNeeded { get; set; }
|
||||
|
||||
public bool MoveExistingWindows { get; set; }
|
||||
|
||||
public string LastLaunched
|
||||
{
|
||||
get
|
||||
{
|
||||
string lastLaunched = GetString("LastLaunched") + ": ";
|
||||
if (LastLaunchedTime == 0)
|
||||
{
|
||||
return lastLaunched + GetString("Never");
|
||||
}
|
||||
|
||||
const int Second = 1;
|
||||
const int Minute = 60 * Second;
|
||||
const int Hour = 60 * Minute;
|
||||
const int Day = 24 * Hour;
|
||||
const int Month = 30 * Day;
|
||||
|
||||
DateTime lastLaunchDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(LastLaunchedTime);
|
||||
|
||||
TimeSpan ts = DateTime.UtcNow - lastLaunchDateTime;
|
||||
double delta = Math.Abs(ts.TotalSeconds);
|
||||
|
||||
if (delta < 1 * Minute)
|
||||
{
|
||||
return lastLaunched + GetString("Recently");
|
||||
}
|
||||
|
||||
if (delta < 2 * Minute)
|
||||
{
|
||||
return lastLaunched + GetString("OneMinuteAgo");
|
||||
}
|
||||
|
||||
if (delta < 45 * Minute)
|
||||
{
|
||||
return lastLaunched + ts.Minutes + " " + GetString("MinutesAgo");
|
||||
}
|
||||
|
||||
if (delta < 90 * Minute)
|
||||
{
|
||||
return lastLaunched + GetString("OneHourAgo");
|
||||
}
|
||||
|
||||
if (delta < 24 * Hour)
|
||||
{
|
||||
return lastLaunched + ts.Hours + " " + GetString("HoursAgo");
|
||||
}
|
||||
|
||||
if (delta < 48 * Hour)
|
||||
{
|
||||
return lastLaunched + GetString("Yesterday");
|
||||
}
|
||||
|
||||
if (delta < 30 * Day)
|
||||
{
|
||||
return lastLaunched + ts.Days + " " + GetString("DaysAgo");
|
||||
}
|
||||
|
||||
if (delta < 12 * Month)
|
||||
{
|
||||
int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
|
||||
return lastLaunched + (months <= 1 ? GetString("OneMonthAgo") : months + " " + GetString("MonthsAgo"));
|
||||
}
|
||||
else
|
||||
{
|
||||
int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
|
||||
return lastLaunched + (years <= 1 ? GetString("OneYearAgo") : years + " " + GetString("YearsAgo"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanBeSaved => !string.IsNullOrEmpty(Name) && Applications.Count > 0;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isRevertEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
[property: JsonIgnore]
|
||||
private bool _isPopupVisible;
|
||||
|
||||
public List<Application> Applications { get; set; }
|
||||
|
||||
public List<object> ApplicationsListed
|
||||
{
|
||||
get
|
||||
{
|
||||
List<object> applicationsListed = [];
|
||||
ILookup<MonitorSetup, Application> apps = Applications.Where(x => !x.Minimized).ToLookup(x => x.MonitorSetup);
|
||||
foreach (IGrouping<MonitorSetup, Application> appItem in apps.OrderBy(x => x.Key.MonitorDpiUnawareBounds.X).ThenBy(x => x.Key.MonitorDpiUnawareBounds.Y))
|
||||
{
|
||||
MonitorHeaderRow headerRow = new() { MonitorName = GetString("Screen") + " " + appItem.Key.MonitorNumber, SelectString = GetString("SelectAllAppsOnMonitor") + " " + appItem.Key.MonitorInfo };
|
||||
applicationsListed.Add(headerRow);
|
||||
foreach (Application app in appItem)
|
||||
{
|
||||
applicationsListed.Add(app);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<Application> minimizedApps = Applications.Where(x => x.Minimized);
|
||||
if (minimizedApps.Any())
|
||||
{
|
||||
MonitorHeaderRow headerRow = new() { MonitorName = GetString("Minimized_Apps"), SelectString = GetString("SelectAllMinimizedApps") };
|
||||
applicationsListed.Add(headerRow);
|
||||
foreach (Application app in minimizedApps)
|
||||
{
|
||||
applicationsListed.Add(app);
|
||||
}
|
||||
}
|
||||
|
||||
return applicationsListed;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string AppsCountString
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = Applications.Count;
|
||||
return count.ToString(CultureInfo.InvariantCulture) + " " + (count == 1 ? GetString("App") : GetString("Apps"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call after modifying the Applications list to notify dependent computed properties.
|
||||
/// </summary>
|
||||
public void NotifyApplicationsChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(AppsCountString));
|
||||
OnPropertyChanged(nameof(CanBeSaved));
|
||||
OnPropertyChanged(nameof(ApplicationsListed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call to refresh the relative time display for LastLaunched.
|
||||
/// </summary>
|
||||
public void NotifyLastLaunchedChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(LastLaunched));
|
||||
}
|
||||
|
||||
public List<MonitorSetup> Monitors { get; }
|
||||
|
||||
public bool IsPositionChangedManually { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[property: JsonIgnore]
|
||||
private BitmapImage _previewIcons;
|
||||
|
||||
[ObservableProperty]
|
||||
[property: JsonIgnore]
|
||||
private BitmapImage _previewImage;
|
||||
|
||||
[ObservableProperty]
|
||||
[property: JsonIgnore]
|
||||
private double _previewImageWidth;
|
||||
|
||||
public Project()
|
||||
{
|
||||
Applications = [];
|
||||
Monitors = [];
|
||||
}
|
||||
|
||||
public Project(Project selectedProject)
|
||||
{
|
||||
Id = selectedProject.Id;
|
||||
Name = selectedProject.Name;
|
||||
PreviewIcons = selectedProject.PreviewIcons;
|
||||
PreviewImage = selectedProject.PreviewImage;
|
||||
IsShortcutNeeded = selectedProject.IsShortcutNeeded;
|
||||
MoveExistingWindows = selectedProject.MoveExistingWindows;
|
||||
|
||||
Monitors = [];
|
||||
foreach (MonitorSetup item in selectedProject.Monitors.OrderBy(x => x.MonitorDpiAwareBounds.X).ThenBy(x => x.MonitorDpiAwareBounds.Y))
|
||||
{
|
||||
Monitors.Add(item);
|
||||
}
|
||||
|
||||
Applications = [];
|
||||
foreach (Application item in selectedProject.Applications)
|
||||
{
|
||||
Application newApp = new(item);
|
||||
newApp.Parent = this;
|
||||
newApp.InitializationFinished();
|
||||
Applications.Add(newApp);
|
||||
}
|
||||
}
|
||||
|
||||
public Project(ProjectWrapper project)
|
||||
{
|
||||
Id = project.Id;
|
||||
Name = project.Name;
|
||||
CreationTime = project.CreationTime;
|
||||
LastLaunchedTime = project.LastLaunchedTime;
|
||||
IsShortcutNeeded = project.IsShortcutNeeded;
|
||||
MoveExistingWindows = project.MoveExistingWindows;
|
||||
Monitors = [];
|
||||
Applications = [];
|
||||
|
||||
foreach (ApplicationWrapper app in project.Applications)
|
||||
{
|
||||
Application newApp = new()
|
||||
{
|
||||
Id = string.IsNullOrEmpty(app.Id) ? $"{{{Guid.NewGuid()}}}" : app.Id,
|
||||
AppName = app.Application,
|
||||
AppPath = app.ApplicationPath,
|
||||
AppTitle = app.Title,
|
||||
PwaAppId = string.IsNullOrEmpty(app.PwaAppId) ? string.Empty : app.PwaAppId,
|
||||
Version = string.IsNullOrEmpty(app.Version) ? string.Empty : app.Version,
|
||||
PackageFullName = app.PackageFullName,
|
||||
AppUserModelId = app.AppUserModelId,
|
||||
Parent = this,
|
||||
CommandLineArguments = app.CommandLineArguments,
|
||||
IsElevated = app.IsElevated,
|
||||
CanLaunchElevated = app.CanLaunchElevated,
|
||||
Maximized = app.Maximized,
|
||||
Minimized = app.Minimized,
|
||||
IsNotFound = false,
|
||||
Position = new Application.WindowPosition()
|
||||
{
|
||||
Height = app.Position.Height,
|
||||
Width = app.Position.Width,
|
||||
X = app.Position.X,
|
||||
Y = app.Position.Y,
|
||||
},
|
||||
MonitorNumber = app.Monitor,
|
||||
};
|
||||
newApp.InitializationFinished();
|
||||
Applications.Add(newApp);
|
||||
}
|
||||
|
||||
foreach (MonitorConfigurationWrapper monitor in project.MonitorConfiguration)
|
||||
{
|
||||
Rect dpiAware = new(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
|
||||
Rect dpiUnaware = new(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height);
|
||||
Monitors.Add(new MonitorSetup(monitor.Id, monitor.InstanceId, monitor.MonitorNumber, monitor.Dpi, dpiAware, dpiUnaware));
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializePreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Applications == null || Applications.Count == 0 || Monitors == null || Monitors.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute bounding rect across all monitors
|
||||
double left = Monitors.Min(m => m.MonitorDpiAwareBounds.X);
|
||||
double top = Monitors.Min(m => m.MonitorDpiAwareBounds.Y);
|
||||
double right = Monitors.Max(m => m.MonitorDpiAwareBounds.X + m.MonitorDpiAwareBounds.Width);
|
||||
double bottom = Monitors.Max(m => m.MonitorDpiAwareBounds.Y + m.MonitorDpiAwareBounds.Height);
|
||||
|
||||
var bounds = new System.Drawing.Rectangle((int)left, (int)top, (int)(right - left), (int)(bottom - top));
|
||||
|
||||
bool isDarkTheme = Helpers.ThemeHelper.IsDarkTheme();
|
||||
|
||||
PreviewImage = Utils.DrawHelper.DrawPreview(this, bounds, isDarkTheme);
|
||||
PreviewImageWidth = bounds.Width * 0.1;
|
||||
PreviewIcons = Utils.DrawHelper.DrawPreviewIcons(this);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
ManagedCommon.Logger.LogError("Preview render failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public MonitorSetup GetMonitorForApp(Application app)
|
||||
{
|
||||
if (Monitors == null || Monitors.Count == 0)
|
||||
{
|
||||
return new MonitorSetup("Unknown", string.Empty, app.MonitorNumber, 96, default, default);
|
||||
}
|
||||
|
||||
return Monitors.FirstOrDefault(m => m.MonitorNumber == app.MonitorNumber)
|
||||
?? Monitors[0];
|
||||
}
|
||||
|
||||
public void CloseExpanders()
|
||||
{
|
||||
foreach (Application app in Applications)
|
||||
{
|
||||
app.IsExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAfterLaunchAndEdit(Project projectBefore)
|
||||
{
|
||||
Id = projectBefore.Id;
|
||||
IsRevertEnabled = true;
|
||||
}
|
||||
|
||||
private static string GetString(string key)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader?.GetString(key) ?? key;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/modules/Workspaces/WorkspacesEditor.WinUI/Program.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger("\\Workspaces\\WorkspacesEditor");
|
||||
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
return;
|
||||
}
|
||||
|
||||
const string mutexName = "Local\\PowerToys_Workspaces_Editor_InstanceMutex";
|
||||
bool createdNew;
|
||||
using var mutex = new Mutex(true, mutexName, out createdNew);
|
||||
|
||||
if (!createdNew)
|
||||
{
|
||||
Logger.LogWarning("Another instance of Workspaces Editor is already running. Exiting this instance.");
|
||||
return;
|
||||
}
|
||||
|
||||
Microsoft.UI.Xaml.Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AddBack" xml:space="preserve">
|
||||
<value>Add back</value>
|
||||
@@ -140,7 +35,6 @@
|
||||
</data>
|
||||
<data name="Args" xml:space="preserve">
|
||||
<value>Args</value>
|
||||
<comment>Arguments</comment>
|
||||
</data>
|
||||
<data name="Cancel" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
@@ -178,6 +72,9 @@
|
||||
<data name="Edit" xml:space="preserve">
|
||||
<value>Edit</value>
|
||||
</data>
|
||||
<data name="EditNameTextBox.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Workspace name</value>
|
||||
</data>
|
||||
<data name="EditWorkspace" xml:space="preserve">
|
||||
<value>Edit Workspace</value>
|
||||
</data>
|
||||
@@ -210,7 +107,6 @@
|
||||
</data>
|
||||
<data name="Left" xml:space="preserve">
|
||||
<value>Left</value>
|
||||
<comment>the left x coordinate</comment>
|
||||
</data>
|
||||
<data name="MainTitle" xml:space="preserve">
|
||||
<value>Workspaces Editor</value>
|
||||
@@ -230,6 +126,9 @@
|
||||
<data name="MonthsAgo" xml:space="preserve">
|
||||
<value>months ago</value>
|
||||
</data>
|
||||
<data name="MoreOptions" xml:space="preserve">
|
||||
<value>More options for</value>
|
||||
</data>
|
||||
<data name="MoveIfExist" xml:space="preserve">
|
||||
<value>Move existing windows</value>
|
||||
</data>
|
||||
@@ -290,6 +189,9 @@
|
||||
<data name="SearchExplanation" xml:space="preserve">
|
||||
<value>Search for Workspaces or apps</value>
|
||||
</data>
|
||||
<data name="SearchTextBox.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Search workspaces</value>
|
||||
</data>
|
||||
<data name="SecondsAgo" xml:space="preserve">
|
||||
<value>seconds ago</value>
|
||||
</data>
|
||||
@@ -316,7 +218,6 @@
|
||||
</data>
|
||||
<data name="Top" xml:space="preserve">
|
||||
<value>Top</value>
|
||||
<comment>the top y coordinate</comment>
|
||||
</data>
|
||||
<data name="Width" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
@@ -333,4 +234,100 @@
|
||||
<data name="Yesterday" xml:space="preserve">
|
||||
<value>yesterday</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="DismissButton.Content" xml:space="preserve">
|
||||
<value>Dismiss</value>
|
||||
</data>
|
||||
<data name="LaunchButton.Content" xml:space="preserve">
|
||||
<value>Launch</value>
|
||||
</data>
|
||||
<data name="LastLaunchedItem.Content" xml:space="preserve">
|
||||
<value>Last launched</value>
|
||||
</data>
|
||||
<data name="CreatedItem.Content" xml:space="preserve">
|
||||
<value>Created</value>
|
||||
</data>
|
||||
<data name="NameItem.Content" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="CustomItem.Content" xml:space="preserve">
|
||||
<value>Custom</value>
|
||||
</data>
|
||||
<data name="MaximizedItem.Content" xml:space="preserve">
|
||||
<value>Maximized</value>
|
||||
</data>
|
||||
<data name="MinimizedItem.Content" xml:space="preserve">
|
||||
<value>Minimized</value>
|
||||
</data>
|
||||
<data name="LaunchAsAdminLabel.Text" xml:space="preserve">
|
||||
<value>Launch as Admin</value>
|
||||
</data>
|
||||
<data name="CliArgumentsLabel.Text" xml:space="preserve">
|
||||
<value>CLI arguments</value>
|
||||
</data>
|
||||
<data name="WindowPositionLabel.Text" xml:space="preserve">
|
||||
<value>Window position</value>
|
||||
</data>
|
||||
<data name="LaunchBtn.Content" xml:space="preserve">
|
||||
<value>Launch</value>
|
||||
</data>
|
||||
<data name="EditFlyoutItem.Text" xml:space="preserve">
|
||||
<value>Edit</value>
|
||||
</data>
|
||||
<data name="RemoveFlyoutItem.Text" xml:space="preserve">
|
||||
<value>Remove</value>
|
||||
</data>
|
||||
<data name="LeftLabel.Text" xml:space="preserve">
|
||||
<value>Left</value>
|
||||
</data>
|
||||
<data name="TopLabel.Text" xml:space="preserve">
|
||||
<value>Top</value>
|
||||
</data>
|
||||
<data name="WidthLabel.Text" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
</data>
|
||||
<data name="HeightLabel.Text" xml:space="preserve">
|
||||
<value>Height</value>
|
||||
</data>
|
||||
<data name="CapturingLabel.Text" xml:space="preserve">
|
||||
<value>CAPTURING</value>
|
||||
</data>
|
||||
<data name="CapturedAppList" xml:space="preserve">
|
||||
<value>Captured Application List</value>
|
||||
</data>
|
||||
<data name="Screen" xml:space="preserve">
|
||||
<value>Screen</value>
|
||||
</data>
|
||||
<data name="CreateWorkspaceBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Create Workspace</value>
|
||||
</data>
|
||||
<data name="SortByComboBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Sort by</value>
|
||||
</data>
|
||||
<data name="MoreOptionsBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>More options</value>
|
||||
</data>
|
||||
<data name="CliArgsTextBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>CLI arguments</value>
|
||||
</data>
|
||||
<data name="WindowPositionComboBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Window position</value>
|
||||
</data>
|
||||
<data name="LeftTextBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Left</value>
|
||||
</data>
|
||||
<data name="TopTextBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Top</value>
|
||||
</data>
|
||||
<data name="WidthTextBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
</data>
|
||||
<data name="HeightTextBox.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Height</value>
|
||||
</data>
|
||||
<data name="SaveBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Save</value>
|
||||
</data>
|
||||
<data name="CapturedAppListControl.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Captured Application List</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -18,22 +18,16 @@ namespace WorkspacesEditor.Telemetry
|
||||
EventName = "Workspaces_CreateEvent";
|
||||
}
|
||||
|
||||
// True if operation successfully completely. False if failed
|
||||
public bool Successful { get; set; }
|
||||
|
||||
// Number of screens present in the project
|
||||
public int NumScreens { get; set; }
|
||||
|
||||
// Total number of apps in the project
|
||||
public int AppCount { get; set; }
|
||||
|
||||
// Number of apps with CLI args
|
||||
public int CliCount { get; set; }
|
||||
|
||||
// Number of apps with "Launch as admin" set
|
||||
public int AdminCount { get; set; }
|
||||
|
||||
// True if user checked "Create Shortcut". False if not.
|
||||
public bool ShortcutCreated { get; set; }
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -18,31 +18,22 @@ namespace WorkspacesEditor.Telemetry
|
||||
EventName = "Workspaces_EditEvent";
|
||||
}
|
||||
|
||||
// True if operation successfully completely. False if failed.
|
||||
public bool Successful { get; set; }
|
||||
|
||||
// Change in number of screens in project
|
||||
public int ScreenCountDelta { get; set; }
|
||||
|
||||
// Number of apps added to project through editing
|
||||
public int AppsAdded { get; set; }
|
||||
|
||||
// Number of apps removed from project through editing
|
||||
public int AppsRemoved { get; set; }
|
||||
|
||||
// Number of apps with CLI args added
|
||||
public int CliAdded { get; set; }
|
||||
|
||||
// Number of apps with CLI args removed
|
||||
public int CliRemoved { get; set; }
|
||||
|
||||
// Number of apps with admin added
|
||||
public int AdminAdded { get; set; }
|
||||
|
||||
// Number of apps with admin removed
|
||||
public int AdminRemoved { get; set; }
|
||||
|
||||
// True if used window pixel sizing boxes to adjust size
|
||||
public bool PixelAdjustmentsUsed { get; set; }
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -7,25 +7,24 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
using WorkspacesEditor.Models;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public class DrawHelper
|
||||
internal static class DrawHelper
|
||||
{
|
||||
private static readonly Font Font = new("Tahoma", 24);
|
||||
private static readonly Font DrawFont = new("Tahoma", 24);
|
||||
private static readonly double Scale = 0.1;
|
||||
private static double gapWidth;
|
||||
private static double gapHeight;
|
||||
|
||||
public static BitmapImage DrawPreview(Project project, Rectangle bounds, Theme currentTheme)
|
||||
public static BitmapImage DrawPreview(Project project, Rectangle bounds, bool isDarkTheme)
|
||||
{
|
||||
List<double> horizontalGaps = [];
|
||||
List<double> verticalGaps = [];
|
||||
@@ -53,15 +52,13 @@ namespace WorkspacesEditor.Utils
|
||||
{
|
||||
if (app.Maximized)
|
||||
{
|
||||
Project project = app.Parent;
|
||||
MonitorSetup monitor = project.GetMonitorForApp(app);
|
||||
if (monitor == null)
|
||||
{
|
||||
// unrealistic case, there are no monitors at all in the workspace, use original rect
|
||||
return new Rectangle(TransformX(app.ScaledPosition.X), TransformY(app.ScaledPosition.Y), Scaled(app.ScaledPosition.Width), Scaled(app.ScaledPosition.Height));
|
||||
}
|
||||
|
||||
return new Rectangle(TransformX(monitor.MonitorDpiAwareBounds.Left), TransformY(monitor.MonitorDpiAwareBounds.Top), Scaled(monitor.MonitorDpiAwareBounds.Width), Scaled(monitor.MonitorDpiAwareBounds.Height));
|
||||
return new Rectangle(TransformX(monitor.MonitorDpiAwareBounds.X), TransformY(monitor.MonitorDpiAwareBounds.Y), Scaled(monitor.MonitorDpiAwareBounds.Width), Scaled(monitor.MonitorDpiAwareBounds.Height));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -93,24 +90,16 @@ namespace WorkspacesEditor.Utils
|
||||
app.RepeatIndex = 0;
|
||||
}
|
||||
|
||||
// now that all repeat index values are set, update the repeat index strings on UI
|
||||
foreach (Application app in project.Applications)
|
||||
{
|
||||
app.OnPropertyChanged(new PropertyChangedEventArgs("RepeatIndexString"));
|
||||
}
|
||||
|
||||
foreach (MonitorSetup monitor in project.Monitors)
|
||||
{
|
||||
// check for vertical gap
|
||||
if (monitor.MonitorDpiAwareBounds.Left > bounds.Left && project.Monitors.Any(x => x.MonitorDpiAwareBounds.Right <= monitor.MonitorDpiAwareBounds.Left))
|
||||
if (monitor.MonitorDpiAwareBounds.X > bounds.Left && project.Monitors.Any(x => (x.MonitorDpiAwareBounds.X + x.MonitorDpiAwareBounds.Width) <= monitor.MonitorDpiAwareBounds.X))
|
||||
{
|
||||
verticalGaps.Add(monitor.MonitorDpiAwareBounds.Left);
|
||||
verticalGaps.Add(monitor.MonitorDpiAwareBounds.X);
|
||||
}
|
||||
|
||||
// check for horizontal gap
|
||||
if (monitor.MonitorDpiAwareBounds.Top > bounds.Top && project.Monitors.Any(x => x.MonitorDpiAwareBounds.Bottom <= monitor.MonitorDpiAwareBounds.Top))
|
||||
if (monitor.MonitorDpiAwareBounds.Y > bounds.Top && project.Monitors.Any(x => (x.MonitorDpiAwareBounds.Y + x.MonitorDpiAwareBounds.Height) <= monitor.MonitorDpiAwareBounds.Y))
|
||||
{
|
||||
horizontalGaps.Add(monitor.MonitorDpiAwareBounds.Top);
|
||||
horizontalGaps.Add(monitor.MonitorDpiAwareBounds.Y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,163 +111,34 @@ namespace WorkspacesEditor.Utils
|
||||
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
Brush brush = new SolidBrush(currentTheme == Theme.Dark ? Color.FromArgb(10, 255, 255, 255) : Color.FromArgb(10, 0, 0, 0));
|
||||
Brush brushForHighlight = new SolidBrush(currentTheme == Theme.Dark ? Color.FromArgb(192, 255, 255, 255) : Color.FromArgb(192, 0, 0, 0));
|
||||
Brush brush = new SolidBrush(isDarkTheme ? Color.FromArgb(10, 255, 255, 255) : Color.FromArgb(10, 0, 0, 0));
|
||||
Brush brushForHighlight = new SolidBrush(isDarkTheme ? Color.FromArgb(192, 255, 255, 255) : Color.FromArgb(192, 0, 0, 0));
|
||||
|
||||
// draw the monitors
|
||||
foreach (MonitorSetup monitor in project.Monitors)
|
||||
{
|
||||
Brush monitorBrush = new SolidBrush(currentTheme == Theme.Dark ? Color.FromArgb(32, 7, 91, 155) : Color.FromArgb(32, 7, 91, 155));
|
||||
g.FillRectangle(monitorBrush, new Rectangle(TransformX(monitor.MonitorDpiAwareBounds.Left), TransformY(monitor.MonitorDpiAwareBounds.Top), Scaled(monitor.MonitorDpiAwareBounds.Width), Scaled(monitor.MonitorDpiAwareBounds.Height)));
|
||||
Brush monitorBrush = new SolidBrush(Color.FromArgb(32, 7, 91, 155));
|
||||
g.FillRectangle(monitorBrush, new Rectangle(TransformX(monitor.MonitorDpiAwareBounds.X), TransformY(monitor.MonitorDpiAwareBounds.Y), Scaled(monitor.MonitorDpiAwareBounds.Width), Scaled(monitor.MonitorDpiAwareBounds.Height)));
|
||||
}
|
||||
|
||||
IEnumerable<Application> appsToDraw = appsIncluded.Where(x => !x.Minimized);
|
||||
|
||||
// draw the highlighted app at the end to have its icon in the foreground for the case there are overlapping icons
|
||||
foreach (Application app in appsToDraw.Where(x => !x.IsHighlighted))
|
||||
{
|
||||
Rectangle rect = GetAppRect(app);
|
||||
DrawWindow(g, brush, rect, app, desiredIconSize, currentTheme);
|
||||
DrawWindow(g, brush, rect, app, desiredIconSize, isDarkTheme);
|
||||
}
|
||||
|
||||
foreach (Application app in appsToDraw.Where(x => x.IsHighlighted))
|
||||
{
|
||||
Rectangle rect = GetAppRect(app);
|
||||
DrawWindow(g, brushForHighlight, rect, app, desiredIconSize, currentTheme);
|
||||
DrawWindow(g, brushForHighlight, rect, app, desiredIconSize, isDarkTheme);
|
||||
}
|
||||
|
||||
// draw the minimized windows
|
||||
Rectangle rectMinimized = new(0, Scaled((bounds.Height * 1.02) + (horizontalGaps.Count * gapHeight)), Scaled(bounds.Width + (verticalGaps.Count * gapWidth)), Scaled(bounds.Height * 0.18));
|
||||
DrawWindow(g, brush, brushForHighlight, rectMinimized, appsIncluded.Where(x => x.Minimized), currentTheme);
|
||||
DrawWindow(g, brush, brushForHighlight, rectMinimized, appsIncluded.Where(x => x.Minimized), isDarkTheme);
|
||||
}
|
||||
|
||||
using MemoryStream memory = new();
|
||||
WorkspacesCsharpLibrary.DrawHelper.SaveBitmap(previewBitmap, memory);
|
||||
|
||||
memory.Position = 0;
|
||||
|
||||
BitmapImage bitmapImage = new();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.StreamSource = memory;
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
|
||||
public static void DrawWindow(Graphics graphics, Brush brush, Rectangle bounds, Application app, double desiredIconSize, Theme currentTheme)
|
||||
{
|
||||
if (graphics == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (brush == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (GraphicsPath path = RoundedRect(bounds))
|
||||
{
|
||||
if (app.IsHighlighted)
|
||||
{
|
||||
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.White : Color.DarkGray, graphics.VisibleClipBounds.Height / 50), path);
|
||||
}
|
||||
else
|
||||
{
|
||||
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.FromArgb(128, 82, 82, 82) : Color.FromArgb(128, 160, 160, 160), graphics.VisibleClipBounds.Height / 200), path);
|
||||
}
|
||||
|
||||
graphics.FillPath(brush, path);
|
||||
}
|
||||
|
||||
double iconSize = Math.Min(Math.Min(bounds.Width - 4, bounds.Height - 4), desiredIconSize);
|
||||
Rectangle iconBounds = new((int)(bounds.Left + (bounds.Width / 2) - (iconSize / 2)), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
|
||||
|
||||
try
|
||||
{
|
||||
graphics.DrawIcon(app.Icon, iconBounds);
|
||||
if (app.RepeatIndex > 1)
|
||||
{
|
||||
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
|
||||
int indexSize = (int)(iconBounds.Width * 0.5);
|
||||
Rectangle indexBounds = new(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
|
||||
|
||||
SizeF textSize = graphics.MeasureString(indexString, Font);
|
||||
GraphicsState state = graphics.Save();
|
||||
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
|
||||
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
|
||||
graphics.DrawString(indexString, Font, Brushes.Black, PointF.Empty);
|
||||
graphics.Restore(state);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// sometimes drawing an icon throws an exception despite that the icon seems to be ok
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawWindow(Graphics graphics, Brush brush, Brush brushForHighlight, Rectangle bounds, IEnumerable<Application> apps, Theme currentTheme)
|
||||
{
|
||||
int appsCount = apps.Count();
|
||||
if (appsCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (graphics == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (brush == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (GraphicsPath path = RoundedRect(bounds))
|
||||
{
|
||||
if (apps.Where(x => x.IsHighlighted).Any())
|
||||
{
|
||||
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.White : Color.DarkGray, graphics.VisibleClipBounds.Height / 50), path);
|
||||
graphics.FillPath(brushForHighlight, path);
|
||||
}
|
||||
else
|
||||
{
|
||||
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.FromArgb(128, 82, 82, 82) : Color.FromArgb(128, 160, 160, 160), graphics.VisibleClipBounds.Height / 200), path);
|
||||
graphics.FillPath(brush, path);
|
||||
}
|
||||
}
|
||||
|
||||
double iconSize = Math.Min(bounds.Width, bounds.Height) * 0.5;
|
||||
for (int iconCounter = 0; iconCounter < appsCount; iconCounter++)
|
||||
{
|
||||
Application app = apps.ElementAt(iconCounter);
|
||||
Rectangle iconBounds = new((int)(bounds.Left + (bounds.Width / 2) - (iconSize * ((appsCount / 2) - iconCounter))), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
|
||||
|
||||
try
|
||||
{
|
||||
graphics.DrawIcon(app.Icon, iconBounds);
|
||||
if (app.RepeatIndex > 0)
|
||||
{
|
||||
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
|
||||
int indexSize = (int)(iconBounds.Width * 0.5);
|
||||
Rectangle indexBounds = new(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
|
||||
|
||||
SizeF textSize = graphics.MeasureString(indexString, Font);
|
||||
GraphicsState state = graphics.Save();
|
||||
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
|
||||
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
|
||||
graphics.DrawString(indexString, Font, Brushes.Black, PointF.Empty);
|
||||
graphics.Restore(state);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// sometimes drawing an icon throws an exception despite that the icon seems to be ok
|
||||
}
|
||||
}
|
||||
return BitmapToWinUiImage(previewBitmap);
|
||||
}
|
||||
|
||||
public static BitmapImage DrawPreviewIcons(Project project)
|
||||
@@ -300,37 +160,134 @@ namespace WorkspacesEditor.Utils
|
||||
{
|
||||
try
|
||||
{
|
||||
graphics.DrawIcon(app.Icon, new Rectangle(32 * appIndex, 0, 24, 24));
|
||||
graphics.DrawIcon(app.Icon, new Rectangle(appIndex * 32, 0, 24, 24));
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Exception while drawing the icon for app {app.AppName}. Exception message: {e.Message}");
|
||||
ManagedCommon.Logger.LogError($"Failed to draw preview icon for {app.AppName}", ex);
|
||||
}
|
||||
|
||||
appIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return BitmapToWinUiImage(previewBitmap);
|
||||
}
|
||||
|
||||
private static void DrawWindow(Graphics graphics, Brush brush, Rectangle bounds, Application app, double desiredIconSize, bool isDarkTheme)
|
||||
{
|
||||
if (graphics == null || brush == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (GraphicsPath path = RoundedRect(bounds))
|
||||
{
|
||||
if (app.IsHighlighted)
|
||||
{
|
||||
graphics.DrawPath(new Pen(isDarkTheme ? Color.White : Color.DarkGray, graphics.VisibleClipBounds.Height / 50), path);
|
||||
}
|
||||
else
|
||||
{
|
||||
graphics.DrawPath(new Pen(isDarkTheme ? Color.FromArgb(128, 82, 82, 82) : Color.FromArgb(128, 160, 160, 160), graphics.VisibleClipBounds.Height / 200), path);
|
||||
}
|
||||
|
||||
graphics.FillPath(brush, path);
|
||||
}
|
||||
|
||||
double iconSize = Math.Min(Math.Min(bounds.Width - 4, bounds.Height - 4), desiredIconSize);
|
||||
Rectangle iconBounds = new((int)(bounds.Left + (bounds.Width / 2) - (iconSize / 2)), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
|
||||
|
||||
try
|
||||
{
|
||||
graphics.DrawIcon(app.Icon, iconBounds);
|
||||
if (app.RepeatIndex > 1)
|
||||
{
|
||||
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
|
||||
int indexSize = (int)(iconBounds.Width * 0.5);
|
||||
Rectangle indexBounds = new(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
|
||||
|
||||
SizeF textSize = graphics.MeasureString(indexString, DrawFont);
|
||||
GraphicsState state = graphics.Save();
|
||||
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
|
||||
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
|
||||
graphics.DrawString(indexString, DrawFont, Brushes.Black, PointF.Empty);
|
||||
graphics.Restore(state);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ManagedCommon.Logger.LogError($"Failed to draw window for {app.AppName}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawWindow(Graphics graphics, Brush brush, Brush brushForHighlight, Rectangle bounds, IEnumerable<Application> apps, bool isDarkTheme)
|
||||
{
|
||||
int appsCount = apps.Count();
|
||||
if (appsCount == 0 || graphics == null || brush == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (GraphicsPath path = RoundedRect(bounds))
|
||||
{
|
||||
if (apps.Any(x => x.IsHighlighted))
|
||||
{
|
||||
graphics.DrawPath(new Pen(isDarkTheme ? Color.White : Color.DarkGray, graphics.VisibleClipBounds.Height / 50), path);
|
||||
graphics.FillPath(brushForHighlight, path);
|
||||
}
|
||||
else
|
||||
{
|
||||
graphics.DrawPath(new Pen(isDarkTheme ? Color.FromArgb(128, 82, 82, 82) : Color.FromArgb(128, 160, 160, 160), graphics.VisibleClipBounds.Height / 200), path);
|
||||
graphics.FillPath(brush, path);
|
||||
}
|
||||
}
|
||||
|
||||
double iconSize = Math.Min(bounds.Width, bounds.Height) * 0.5;
|
||||
for (int iconCounter = 0; iconCounter < appsCount; iconCounter++)
|
||||
{
|
||||
Application app = apps.ElementAt(iconCounter);
|
||||
Rectangle iconBounds = new((int)(bounds.Left + (bounds.Width / 2) - (iconSize * ((appsCount / 2.0) - iconCounter))), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
|
||||
|
||||
try
|
||||
{
|
||||
graphics.DrawIcon(app.Icon, iconBounds);
|
||||
if (app.RepeatIndex > 0)
|
||||
{
|
||||
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
|
||||
int indexSize = (int)(iconBounds.Width * 0.5);
|
||||
Rectangle indexBounds = new(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
|
||||
|
||||
SizeF textSize = graphics.MeasureString(indexString, DrawFont);
|
||||
GraphicsState state = graphics.Save();
|
||||
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
|
||||
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
|
||||
graphics.DrawString(indexString, DrawFont, Brushes.Black, PointF.Empty);
|
||||
graphics.Restore(state);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ManagedCommon.Logger.LogError($"Failed to draw minimized app icon", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapImage BitmapToWinUiImage(Bitmap bitmap)
|
||||
{
|
||||
using MemoryStream memory = new();
|
||||
|
||||
WorkspacesCsharpLibrary.DrawHelper.SaveBitmap(previewBitmap, memory);
|
||||
|
||||
WorkspacesCsharpLibrary.DrawHelper.SaveBitmap(bitmap, memory);
|
||||
memory.Position = 0;
|
||||
|
||||
BitmapImage bitmapImage = new();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.StreamSource = memory;
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
bitmapImage.SetSource(memory.AsRandomAccessStream());
|
||||
return bitmapImage;
|
||||
}
|
||||
|
||||
private static GraphicsPath RoundedRect(Rectangle bounds)
|
||||
{
|
||||
int minorSize = Math.Min(bounds.Width, bounds.Height);
|
||||
int radius = (int)(minorSize / 8);
|
||||
int radius = minorSize / 8;
|
||||
|
||||
int diameter = radius * 2;
|
||||
Size size = new(diameter, diameter);
|
||||
@@ -343,21 +300,13 @@ namespace WorkspacesEditor.Utils
|
||||
return path;
|
||||
}
|
||||
|
||||
// top left arc
|
||||
path.AddArc(arc, 180, 90);
|
||||
|
||||
// top right arc
|
||||
arc.X = bounds.Right - diameter;
|
||||
path.AddArc(arc, 270, 90);
|
||||
|
||||
// bottom right arc
|
||||
arc.Y = bounds.Bottom - diameter;
|
||||
path.AddArc(arc, 0, 90);
|
||||
|
||||
// bottom left arc
|
||||
arc.X = bounds.Left;
|
||||
path.AddArc(arc, 90, 90);
|
||||
|
||||
path.CloseFigure();
|
||||
return path;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public class ParsingResult
|
||||
{
|
||||
public bool Result { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public ParsingResult(bool result, string message = "")
|
||||
{
|
||||
Result = result;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -9,18 +9,18 @@ namespace WorkspacesEditor.Utils
|
||||
public class Settings
|
||||
{
|
||||
private const string WorkspacesModuleName = "Workspaces";
|
||||
private static readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
|
||||
private static readonly SettingsUtils SettingsUtilsInstance = SettingsUtils.Default;
|
||||
|
||||
public static WorkspacesSettings ReadSettings()
|
||||
{
|
||||
if (!_settingsUtils.SettingsExists(WorkspacesModuleName))
|
||||
if (!SettingsUtilsInstance.SettingsExists(WorkspacesModuleName))
|
||||
{
|
||||
WorkspacesSettings defaultWorkspacesSettings = new();
|
||||
defaultWorkspacesSettings.Save(_settingsUtils);
|
||||
defaultWorkspacesSettings.Save(SettingsUtilsInstance);
|
||||
return defaultWorkspacesSettings;
|
||||
}
|
||||
|
||||
WorkspacesSettings settings = _settingsUtils.GetSettingsOrDefault<WorkspacesSettings>(WorkspacesModuleName);
|
||||
WorkspacesSettings settings = SettingsUtilsInstance.GetSettingsOrDefault<WorkspacesSettings>(WorkspacesModuleName);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using ManagedCommon;
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesCsharpLibrary.Utils;
|
||||
@@ -16,10 +17,6 @@ namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public class WorkspacesEditorIO
|
||||
{
|
||||
public WorkspacesEditorIO()
|
||||
{
|
||||
}
|
||||
|
||||
public ParsingResult ParseWorkspaces(MainViewModel mainViewModel)
|
||||
{
|
||||
try
|
||||
@@ -39,8 +36,8 @@ namespace WorkspacesEditor.Utils
|
||||
|
||||
if (!SetWorkspaces(mainViewModel, workspaces))
|
||||
{
|
||||
Logger.LogWarning($"Workspaces storage file content could not be set. Reason: {Properties.Resources.Error_Parsing_Message}");
|
||||
return new ParsingResult(false, WorkspacesEditor.Properties.Resources.Error_Parsing_Message);
|
||||
Logger.LogWarning("Workspaces storage file content could not be set.");
|
||||
return new ParsingResult(false, "Error parsing Workspaces data.");
|
||||
}
|
||||
|
||||
return new ParsingResult(true);
|
||||
@@ -76,8 +73,7 @@ namespace WorkspacesEditor.Utils
|
||||
public void SerializeWorkspaces(List<Project> workspaces, bool useTempFile = false)
|
||||
{
|
||||
WorkspacesData serializer = new();
|
||||
WorkspacesData.WorkspacesListWrapper workspacesWrapper = new() { };
|
||||
workspacesWrapper.Workspaces = [];
|
||||
WorkspacesData.WorkspacesListWrapper workspacesWrapper = new() { Workspaces = [] };
|
||||
|
||||
foreach (Project project in workspaces)
|
||||
{
|
||||
@@ -86,16 +82,16 @@ namespace WorkspacesEditor.Utils
|
||||
Id = project.Id,
|
||||
Name = project.Name,
|
||||
CreationTime = project.CreationTime,
|
||||
LastLaunchedTime = project.LastLaunchedTime,
|
||||
IsShortcutNeeded = project.IsShortcutNeeded,
|
||||
MoveExistingWindows = project.MoveExistingWindows,
|
||||
LastLaunchedTime = project.LastLaunchedTime,
|
||||
Applications = [],
|
||||
MonitorConfiguration = [],
|
||||
};
|
||||
|
||||
foreach (Application app in project.Applications.Where(x => x.IsIncluded))
|
||||
foreach (Application app in project.Applications)
|
||||
{
|
||||
wrapper.Applications.Add(new ApplicationWrapper
|
||||
ApplicationWrapper appWrapper = new()
|
||||
{
|
||||
Id = app.Id,
|
||||
Application = app.AppName,
|
||||
@@ -107,80 +103,79 @@ namespace WorkspacesEditor.Utils
|
||||
CommandLineArguments = app.CommandLineArguments,
|
||||
IsElevated = app.IsElevated,
|
||||
CanLaunchElevated = app.CanLaunchElevated,
|
||||
Version = app.Version,
|
||||
Maximized = app.Maximized,
|
||||
Minimized = app.Minimized,
|
||||
Position = new ApplicationWrapper.WindowPositionWrapper
|
||||
Position = new ApplicationWrapper.WindowPositionWrapper()
|
||||
{
|
||||
X = app.Position.X,
|
||||
Y = app.Position.Y,
|
||||
Height = app.Position.Height,
|
||||
Width = app.Position.Width,
|
||||
Height = app.Position.Height,
|
||||
},
|
||||
Monitor = app.MonitorNumber,
|
||||
});
|
||||
Version = app.Version,
|
||||
};
|
||||
wrapper.Applications.Add(appWrapper);
|
||||
}
|
||||
|
||||
foreach (MonitorSetup monitor in project.Monitors)
|
||||
{
|
||||
wrapper.MonitorConfiguration.Add(new MonitorConfigurationWrapper
|
||||
MonitorConfigurationWrapper monitorWrapper = new()
|
||||
{
|
||||
Id = monitor.MonitorName,
|
||||
InstanceId = monitor.MonitorInstanceId,
|
||||
MonitorNumber = monitor.MonitorNumber,
|
||||
Dpi = monitor.Dpi,
|
||||
MonitorRectDpiAware = new MonitorConfigurationWrapper.MonitorRectWrapper
|
||||
MonitorRectDpiAware = new MonitorConfigurationWrapper.MonitorRectWrapper()
|
||||
{
|
||||
Left = (int)monitor.MonitorDpiAwareBounds.Left,
|
||||
Top = (int)monitor.MonitorDpiAwareBounds.Top,
|
||||
Left = (int)monitor.MonitorDpiAwareBounds.X,
|
||||
Top = (int)monitor.MonitorDpiAwareBounds.Y,
|
||||
Width = (int)monitor.MonitorDpiAwareBounds.Width,
|
||||
Height = (int)monitor.MonitorDpiAwareBounds.Height,
|
||||
},
|
||||
MonitorRectDpiUnaware = new MonitorConfigurationWrapper.MonitorRectWrapper
|
||||
MonitorRectDpiUnaware = new MonitorConfigurationWrapper.MonitorRectWrapper()
|
||||
{
|
||||
Left = (int)monitor.MonitorDpiUnawareBounds.Left,
|
||||
Top = (int)monitor.MonitorDpiUnawareBounds.Top,
|
||||
Left = (int)monitor.MonitorDpiUnawareBounds.X,
|
||||
Top = (int)monitor.MonitorDpiUnawareBounds.Y,
|
||||
Width = (int)monitor.MonitorDpiUnawareBounds.Width,
|
||||
Height = (int)monitor.MonitorDpiUnawareBounds.Height,
|
||||
},
|
||||
});
|
||||
};
|
||||
wrapper.MonitorConfiguration.Add(monitorWrapper);
|
||||
}
|
||||
|
||||
workspacesWrapper.Workspaces.Add(wrapper);
|
||||
}
|
||||
|
||||
string file = useTempFile ? TempProjectData.File : serializer.File;
|
||||
try
|
||||
{
|
||||
IOUtils ioUtils = new();
|
||||
ioUtils.WriteFile(useTempFile ? TempProjectData.File : serializer.File, serializer.Serialize(workspacesWrapper));
|
||||
WorkspacesCsharpLibrary.Utils.IOUtils ioUtils = new();
|
||||
ioUtils.WriteFile(file, serializer.Serialize(workspacesWrapper));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// TODO: show error
|
||||
Logger.LogError($"Exception while writing storage file: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool AddWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
|
||||
private static bool SetWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
|
||||
{
|
||||
mainViewModel.Workspaces.Clear();
|
||||
foreach (ProjectWrapper project in workspaces.Workspaces)
|
||||
{
|
||||
mainViewModel.Workspaces.Add(new Project(project));
|
||||
try
|
||||
{
|
||||
Project newProject = new(project);
|
||||
mainViewModel.Workspaces.Add(newProject);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Exception while adding workspace {project.Name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
mainViewModel.Initialize();
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SetWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
|
||||
{
|
||||
mainViewModel.Workspaces = [];
|
||||
return AddWorkspaces(mainViewModel, workspaces);
|
||||
}
|
||||
|
||||
internal void SerializeTempProject(Project project)
|
||||
{
|
||||
SerializeWorkspaces([project], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
internal static class WorkspacesIcon
|
||||
{
|
||||
private const int IconSize = 128;
|
||||
|
||||
private static readonly Brush LightThemeIconBackground = new SolidBrush(Color.FromArgb(255, 239, 243, 251));
|
||||
private static readonly Brush LightThemeIconForeground = new SolidBrush(Color.FromArgb(255, 47, 50, 56));
|
||||
private static readonly Brush DarkThemeIconBackground = new SolidBrush(Color.FromArgb(255, 55, 55, 55));
|
||||
private static readonly Brush DarkThemeIconForeground = new SolidBrush(Color.FromArgb(255, 228, 228, 228));
|
||||
private static readonly Font IconFont = new("Aptos", 24, FontStyle.Bold);
|
||||
|
||||
public static string IconTextFromProjectName(string projectName)
|
||||
{
|
||||
string result = string.Empty;
|
||||
char[] delimiterChars = { ' ', ',', '.', ':', '-', '\t' };
|
||||
string[] words = projectName.Split(delimiterChars);
|
||||
|
||||
foreach (string word in words)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (word.All(char.IsDigit))
|
||||
{
|
||||
result += word;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += word.ToUpper(CultureInfo.CurrentCulture)[0];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Bitmap DrawIcon(string text, bool isDarkTheme)
|
||||
{
|
||||
Brush background = isDarkTheme ? DarkThemeIconBackground : LightThemeIconBackground;
|
||||
Brush foreground = isDarkTheme ? DarkThemeIconForeground : LightThemeIconForeground;
|
||||
Bitmap bitmap = new(IconSize, IconSize);
|
||||
|
||||
using (Graphics graphics = Graphics.FromImage(bitmap))
|
||||
{
|
||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
graphics.FillEllipse(background, 0, 0, IconSize, IconSize);
|
||||
|
||||
var textSize = graphics.MeasureString(text, IconFont);
|
||||
var state = graphics.Save();
|
||||
|
||||
float scaleX = IconSize / textSize.Width;
|
||||
float scaleY = IconSize / textSize.Height;
|
||||
float scale = Math.Min(scaleX, scaleY) * 0.8f;
|
||||
|
||||
float textX = (IconSize - (textSize.Width * scale)) / 2;
|
||||
float textY = ((IconSize - (textSize.Height * scale)) / 2) + 6;
|
||||
|
||||
graphics.TranslateTransform(textX, textY);
|
||||
graphics.ScaleTransform(scale, scale);
|
||||
graphics.DrawString(text, IconFont, foreground, 0, 0);
|
||||
graphics.Restore(state);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static void SaveIcon(Bitmap icon, string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
using var fileStream = new FileStream(path, FileMode.CreateNew);
|
||||
using var memoryStream = new MemoryStream();
|
||||
WorkspacesCsharpLibrary.DrawHelper.SaveBitmap(icon, memoryStream);
|
||||
|
||||
using var iconWriter = new BinaryWriter(fileStream);
|
||||
iconWriter.Write((byte)0);
|
||||
iconWriter.Write((byte)0);
|
||||
iconWriter.Write((short)1);
|
||||
iconWriter.Write((short)1);
|
||||
iconWriter.Write((byte)IconSize);
|
||||
iconWriter.Write((byte)IconSize);
|
||||
iconWriter.Write((byte)0);
|
||||
iconWriter.Write((byte)0);
|
||||
iconWriter.Write((short)0);
|
||||
iconWriter.Write((short)32);
|
||||
iconWriter.Write((int)memoryStream.Length);
|
||||
iconWriter.Write(6 + 16);
|
||||
iconWriter.Write(memoryStream.ToArray());
|
||||
iconWriter.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesCsharpLibrary.Utils;
|
||||
using WorkspacesEditor.Helpers;
|
||||
using WorkspacesEditor.Messages;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.Utils;
|
||||
|
||||
namespace WorkspacesEditor.ViewModels
|
||||
{
|
||||
public partial class MainViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private WorkspacesEditorIO _workspacesEditorIO;
|
||||
private Project _editedProject;
|
||||
private Project _projectBeforeLaunch;
|
||||
private string _projectNameBeingEdited;
|
||||
private Microsoft.UI.Xaml.DispatcherTimer _lastUpdatedTimer;
|
||||
private WorkspacesSettings _settings;
|
||||
private bool _isDisposed;
|
||||
private bool _isExistingProjectLaunched;
|
||||
|
||||
public ObservableCollection<Project> Workspaces { get; set; } = new ObservableCollection<Project>();
|
||||
|
||||
private List<Project> _workspacesView = new();
|
||||
|
||||
public List<Project> WorkspacesView
|
||||
{
|
||||
get => _workspacesView;
|
||||
private set => SetProperty(ref _workspacesView, value);
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isWorkspacesViewEmpty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _emptyWorkspacesViewMessage;
|
||||
|
||||
public void RefreshWorkspacesView()
|
||||
{
|
||||
IEnumerable<Project> workspaces = GetFilteredWorkspaces();
|
||||
bool isEmpty = !(workspaces != null && workspaces.Any());
|
||||
IsWorkspacesViewEmpty = isEmpty;
|
||||
|
||||
if (isEmpty)
|
||||
{
|
||||
if (Workspaces != null && Workspaces.Any())
|
||||
{
|
||||
EmptyWorkspacesViewMessage = GetString("NoWorkspacesMatch");
|
||||
}
|
||||
else
|
||||
{
|
||||
EmptyWorkspacesViewMessage = GetString("No_Workspaces_Message");
|
||||
}
|
||||
|
||||
WorkspacesView = new List<Project>();
|
||||
return;
|
||||
}
|
||||
|
||||
WorkspacesData.OrderBy orderBy = (WorkspacesData.OrderBy)OrderByIndex;
|
||||
if (orderBy == WorkspacesData.OrderBy.LastViewed)
|
||||
{
|
||||
WorkspacesView = workspaces.OrderByDescending(x => x.LastLaunchedTime).ToList();
|
||||
}
|
||||
else if (orderBy == WorkspacesData.OrderBy.Created)
|
||||
{
|
||||
WorkspacesView = workspaces.OrderByDescending(x => x.CreationTime).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
WorkspacesView = workspaces.OrderBy(x => x.Name).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Project> GetFilteredWorkspaces()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
return Workspaces;
|
||||
}
|
||||
|
||||
return Workspaces.Where(x =>
|
||||
{
|
||||
if (x.Name.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x.Applications == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return x.Applications.Any(app => app.AppName.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase));
|
||||
});
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchTerm;
|
||||
|
||||
partial void OnSearchTermChanged(string value)
|
||||
{
|
||||
RefreshWorkspacesView();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private int _orderByIndex;
|
||||
|
||||
partial void OnOrderByIndexChanged(int value)
|
||||
{
|
||||
_settings.Properties.SortBy = (WorkspacesProperties.SortByProperty)value;
|
||||
_settings.Save(SettingsUtils.Default);
|
||||
RefreshWorkspacesView();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
public MainViewModel(WorkspacesEditorIO workspacesEditorIO)
|
||||
{
|
||||
_settings = Utils.Settings.ReadSettings();
|
||||
OrderByIndex = (int)_settings.Properties.SortBy;
|
||||
_workspacesEditorIO = workspacesEditorIO;
|
||||
|
||||
StrongReferenceMessenger.Default.Register<SnapshotCapturedMessage>(this, (r, m) => ((MainViewModel)r).OnSnapshotCaptured());
|
||||
StrongReferenceMessenger.Default.Register<SnapshotCancelledMessage>(this, (r, m) => ((MainViewModel)r).CancelSnapshot());
|
||||
}
|
||||
|
||||
private void OnSnapshotCaptured()
|
||||
{
|
||||
_ = SnapWorkspaceAsync();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
foreach (Project project in Workspaces)
|
||||
{
|
||||
project.InitializePreview();
|
||||
}
|
||||
|
||||
// Create DispatcherTimer here (requires UI thread / DispatcherQueue to exist)
|
||||
_lastUpdatedTimer = new Microsoft.UI.Xaml.DispatcherTimer();
|
||||
_lastUpdatedTimer.Interval = TimeSpan.FromSeconds(1);
|
||||
_lastUpdatedTimer.Tick += LastUpdatedTimerTick;
|
||||
_lastUpdatedTimer.Start();
|
||||
|
||||
RefreshWorkspacesView();
|
||||
}
|
||||
|
||||
public void SaveProject(Project projectToSave)
|
||||
{
|
||||
if (_editedProject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_editedProject.Name = projectToSave.Name;
|
||||
_editedProject.IsShortcutNeeded = projectToSave.IsShortcutNeeded;
|
||||
_editedProject.MoveExistingWindows = projectToSave.MoveExistingWindows;
|
||||
_editedProject.PreviewIcons = projectToSave.PreviewIcons;
|
||||
_editedProject.PreviewImage = projectToSave.PreviewImage;
|
||||
_editedProject.Applications = projectToSave.Applications.Where(x => x.IsIncluded).ToList();
|
||||
|
||||
_editedProject.NotifyApplicationsChanged();
|
||||
_editedProject.InitializePreview();
|
||||
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
|
||||
ApplyShortcut(_editedProject);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.EditEvent { Successful = true, PixelAdjustmentsUsed = projectToSave.IsPositionChangedManually });
|
||||
}
|
||||
|
||||
public void EditProject(Project selectedProject, bool isNewlyCreated = false)
|
||||
{
|
||||
_editedProject = selectedProject;
|
||||
|
||||
if (!isNewlyCreated)
|
||||
{
|
||||
selectedProject = new Project(selectedProject);
|
||||
}
|
||||
|
||||
if (isNewlyCreated)
|
||||
{
|
||||
string defaultNamePrefix = GetString("DefaultWorkspaceNamePrefix");
|
||||
int nextProjectIndex = 0;
|
||||
foreach (var proj in Workspaces)
|
||||
{
|
||||
if (proj.Name.StartsWith(defaultNamePrefix, StringComparison.CurrentCulture))
|
||||
{
|
||||
try
|
||||
{
|
||||
int index = int.Parse(proj.Name[(defaultNamePrefix.Length + 1)..], CultureInfo.CurrentCulture);
|
||||
if (nextProjectIndex < index)
|
||||
{
|
||||
nextProjectIndex = index;
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedProject.Name = defaultNamePrefix + " " + (nextProjectIndex + 1).ToString(CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
selectedProject.EditorWindowTitle = isNewlyCreated ? GetString("CreateWorkspace") : GetString("EditWorkspace");
|
||||
selectedProject.InitializePreview();
|
||||
|
||||
_lastUpdatedTimer.Stop();
|
||||
|
||||
// Navigate to editor page, passing the project as parameter
|
||||
StrongReferenceMessenger.Default.Send(new NavigateToEditorMessage(selectedProject));
|
||||
}
|
||||
|
||||
public void AddNewProject(Project project)
|
||||
{
|
||||
project.Applications.RemoveAll(app => !app.IsIncluded);
|
||||
project.InitializePreview();
|
||||
Workspaces.Add(project);
|
||||
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
|
||||
TempProjectData.DeleteTempFile();
|
||||
RefreshWorkspacesView();
|
||||
ApplyShortcut(project);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.CreateEvent
|
||||
{
|
||||
Successful = true,
|
||||
NumScreens = project.Monitors.Count,
|
||||
AppCount = project.Applications.Count,
|
||||
CliCount = project.Applications.FindAll(app => !string.IsNullOrEmpty(app.CommandLineArguments)).Count,
|
||||
AdminCount = project.Applications.FindAll(app => app.IsElevated).Count,
|
||||
ShortcutCreated = project.IsShortcutNeeded,
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteProject(Project selectedProject)
|
||||
{
|
||||
Workspaces.Remove(selectedProject);
|
||||
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
|
||||
RemoveShortcut(selectedProject);
|
||||
RefreshWorkspacesView();
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.DeleteEvent { Successful = true });
|
||||
}
|
||||
|
||||
public void SwitchToMainView()
|
||||
{
|
||||
StrongReferenceMessenger.Default.Send(new GoBackMessage());
|
||||
SearchTerm = string.Empty;
|
||||
OnPropertyChanged(nameof(SearchTerm));
|
||||
_lastUpdatedTimer.Start();
|
||||
_editedProject = null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task LaunchProjectAsync(Project project)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Run(() => RunLauncher(project.Id, InvokePoint.EditorButton));
|
||||
if (_workspacesEditorIO.ParseWorkspaces(this).Result == true)
|
||||
{
|
||||
foreach (Project p in Workspaces)
|
||||
{
|
||||
p.InitializePreview();
|
||||
}
|
||||
|
||||
RefreshWorkspacesView();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LaunchProjectAndExitAsync(Project project)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Run(() => RunLauncher(project.Id, InvokePoint.EditorButton));
|
||||
if (_workspacesEditorIO.ParseWorkspaces(this).Result == true)
|
||||
{
|
||||
foreach (Project p in Workspaces)
|
||||
{
|
||||
p.InitializePreview();
|
||||
}
|
||||
|
||||
RefreshWorkspacesView();
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Launched the Workspace {project.Name}. Exiting.");
|
||||
StrongReferenceMessenger.Default.Send(new CloseApplicationMessage());
|
||||
}
|
||||
|
||||
public void EnterSnapshotMode(bool isExistingProjectLaunched)
|
||||
{
|
||||
_isExistingProjectLaunched = isExistingProjectLaunched;
|
||||
|
||||
// Minimize the main window
|
||||
StrongReferenceMessenger.Default.Send(new MinimizeWindowMessage());
|
||||
|
||||
// Request the View layer to show the snapshot window
|
||||
StrongReferenceMessenger.Default.Send(new ShowSnapshotWindowMessage());
|
||||
}
|
||||
|
||||
internal void CancelSnapshot()
|
||||
{
|
||||
StrongReferenceMessenger.Default.Send(new RestoreWindowMessage());
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
internal async Task SnapWorkspaceAsync()
|
||||
{
|
||||
// Restore window immediately so user sees feedback
|
||||
StrongReferenceMessenger.Default.Send(new RestoreWindowMessage());
|
||||
IsLoading = true;
|
||||
|
||||
await Task.Run(() => RunSnapshotTool(_isExistingProjectLaunched));
|
||||
|
||||
IsLoading = false;
|
||||
|
||||
Project project = _workspacesEditorIO.ParseTempProject();
|
||||
if (project != null)
|
||||
{
|
||||
if (_isExistingProjectLaunched)
|
||||
{
|
||||
project.UpdateAfterLaunchAndEdit(_projectBeforeLaunch);
|
||||
project.EditorWindowTitle = GetString("EditWorkspace");
|
||||
|
||||
// Navigate to editor page with the updated project
|
||||
StrongReferenceMessenger.Default.Send(new NavigateToEditorMessage(project));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditProject(project, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
internal async Task LaunchAndEditAsync(Project project)
|
||||
{
|
||||
await Task.Run(() => RunLauncher(project.Id, InvokePoint.LaunchAndEdit));
|
||||
_projectBeforeLaunch = new Project(project);
|
||||
EnterSnapshotMode(true);
|
||||
}
|
||||
|
||||
internal void RevertLaunch()
|
||||
{
|
||||
if (_projectBeforeLaunch != null)
|
||||
{
|
||||
_projectBeforeLaunch.InitializePreview();
|
||||
StrongReferenceMessenger.Default.Send(new NavigateToEditorMessage(_projectBeforeLaunch));
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveProjectName(Project project)
|
||||
{
|
||||
_projectNameBeingEdited = project.Name;
|
||||
}
|
||||
|
||||
public void CancelProjectName(Project project)
|
||||
{
|
||||
project.Name = _projectNameBeingEdited;
|
||||
}
|
||||
|
||||
internal void CloseAllPopups()
|
||||
{
|
||||
foreach (Project project in Workspaces)
|
||||
{
|
||||
project.IsPopupVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LastUpdatedTimerTick(object sender, object e)
|
||||
{
|
||||
if (Workspaces == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Project project in Workspaces)
|
||||
{
|
||||
project.NotifyLastLaunchedChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void RunLauncher(string projectId, InvokePoint invokePoint)
|
||||
{
|
||||
var exeDir = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
var parentDir = Path.GetDirectoryName(exeDir);
|
||||
var launcherPath = Path.Combine(parentDir, "PowerToys.WorkspacesLauncher.exe");
|
||||
|
||||
if (!File.Exists(launcherPath))
|
||||
{
|
||||
launcherPath = Path.Combine(exeDir, "PowerToys.WorkspacesLauncher.exe");
|
||||
}
|
||||
|
||||
Process process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo(launcherPath, $"{projectId} {(int)invokePoint}")
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
if (!process.WaitForExit(120_000))
|
||||
{
|
||||
Logger.LogWarning("Workspace launcher did not exit within 120 seconds.");
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to launch workspace: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RunSnapshotTool(bool isExistingProjectLaunched)
|
||||
{
|
||||
var exeDir = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
|
||||
// Snapshot tool is in the parent directory
|
||||
var parentDir = Path.GetDirectoryName(exeDir);
|
||||
var snapshotUtilsPath = Path.Combine(parentDir, "PowerToys.WorkspacesSnapshotTool.exe");
|
||||
|
||||
if (!File.Exists(snapshotUtilsPath))
|
||||
{
|
||||
// Fallback: try same directory
|
||||
snapshotUtilsPath = Path.Combine(exeDir, "PowerToys.WorkspacesSnapshotTool.exe");
|
||||
}
|
||||
|
||||
Process process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo(snapshotUtilsPath)
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
Arguments = isExistingProjectLaunched ? $"{(int)InvokePoint.LaunchAndEdit}" : string.Empty,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
if (!process.WaitForExit(120_000))
|
||||
{
|
||||
Logger.LogWarning("Snapshot tool did not exit within 120 seconds.");
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to run snapshot tool: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetString(string key)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader?.GetString(key) ?? key;
|
||||
}
|
||||
|
||||
private static string GetDesktopShortcutAddress(Project project) => Path.Combine(WorkspacesCsharpLibrary.Utils.FolderUtils.Desktop(), project.Name + ".lnk");
|
||||
|
||||
private static string GetShortcutStoreAddress(Project project)
|
||||
{
|
||||
var dataFolder = WorkspacesCsharpLibrary.Utils.FolderUtils.DataFolder();
|
||||
Directory.CreateDirectory(dataFolder);
|
||||
var shortcutStoreFolder = Path.Combine(dataFolder, "WorkspacesIcons");
|
||||
Directory.CreateDirectory(shortcutStoreFolder);
|
||||
return Path.Combine(shortcutStoreFolder, project.Id + ".ico");
|
||||
}
|
||||
|
||||
private static void ApplyShortcut(Project project)
|
||||
{
|
||||
if (!project.IsShortcutNeeded)
|
||||
{
|
||||
RemoveShortcut(project);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var basePath = Path.GetDirectoryName(Path.GetDirectoryName(Environment.ProcessPath));
|
||||
var shortcutAddress = GetDesktopShortcutAddress(project);
|
||||
var shortcutIconFilename = GetShortcutStoreAddress(project);
|
||||
|
||||
bool isDarkTheme = Helpers.ThemeHelper.IsDarkTheme();
|
||||
|
||||
var icon = Utils.WorkspacesIcon.DrawIcon(Utils.WorkspacesIcon.IconTextFromProjectName(project.Name), isDarkTheme);
|
||||
Utils.WorkspacesIcon.SaveIcon(icon, shortcutIconFilename);
|
||||
|
||||
File.WriteAllBytes(shortcutAddress, Array.Empty<byte>());
|
||||
|
||||
Shell32.Shell shell = new Shell32.Shell();
|
||||
Shell32.Folder dir = shell.NameSpace(WorkspacesCsharpLibrary.Utils.FolderUtils.Desktop());
|
||||
Shell32.FolderItem folderItem = dir.Items().Item($"{project.Name}.lnk");
|
||||
Shell32.ShellLinkObject link = (Shell32.ShellLinkObject)folderItem.GetLink;
|
||||
|
||||
link.Description = $"Project Launcher {project.Id}";
|
||||
link.Path = Path.Combine(basePath, "PowerToys.WorkspacesLauncher.exe");
|
||||
link.Arguments = $"{project.Id} {(int)InvokePoint.Shortcut}";
|
||||
link.WorkingDirectory = basePath;
|
||||
link.SetIconLocation(shortcutIconFilename, 0);
|
||||
link.Save(shortcutAddress);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Shortcut creation error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveShortcut(Project project)
|
||||
{
|
||||
string shortcutAddress = GetDesktopShortcutAddress(project);
|
||||
string shortcutIconFilename = GetShortcutStoreAddress(project);
|
||||
|
||||
if (File.Exists(shortcutIconFilename))
|
||||
{
|
||||
File.Delete(shortcutIconFilename);
|
||||
}
|
||||
|
||||
if (File.Exists(shortcutAddress))
|
||||
{
|
||||
File.Delete(shortcutAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckShortcutPresence(Project project)
|
||||
{
|
||||
string shortcutAddress = Path.Combine(WorkspacesCsharpLibrary.Utils.FolderUtils.Desktop(), project.Name + ".lnk");
|
||||
project.IsShortcutNeeded = File.Exists(shortcutAddress);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_lastUpdatedTimer?.Stop();
|
||||
StrongReferenceMessenger.Default.UnregisterAll(this);
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/modules/Workspaces/WorkspacesEditor.WinUI/Views/App.xaml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application
|
||||
x:Class="WorkspacesEditor.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:WorkspacesEditor">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<FontFamily x:Key="SymbolThemeFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using WorkspacesEditor.Messages;
|
||||
using WorkspacesEditor.Telemetry;
|
||||
using WorkspacesEditor.Utils;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private MainWindow _mainWindow;
|
||||
private bool _isDisposed;
|
||||
|
||||
public static DispatcherQueue DispatcherQueue { get; private set; }
|
||||
|
||||
public static WorkspacesEditorIO WorkspacesEditorIO { get; private set; }
|
||||
|
||||
public static MainViewModel MainViewModel { get; private set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new WorkspacesEditorStartEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
|
||||
|
||||
string languageTag = LanguageHelper.LoadLanguage();
|
||||
if (!string.IsNullOrEmpty(languageTag))
|
||||
{
|
||||
try
|
||||
{
|
||||
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = languageTag;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to set language override: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
this.InitializeComponent();
|
||||
this.UnhandledException += OnUnhandledException;
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
DispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
WorkspacesEditorIO = new WorkspacesEditorIO();
|
||||
MainViewModel = new MainViewModel(WorkspacesEditorIO);
|
||||
WorkspacesEditorIO.ParseWorkspaces(MainViewModel);
|
||||
MainViewModel.Initialize();
|
||||
|
||||
_mainWindow = new MainWindow();
|
||||
_mainWindow.Activate();
|
||||
|
||||
StrongReferenceMessenger.Default.Register<CloseApplicationMessage>(this, (r, m) =>
|
||||
{
|
||||
Logger.LogInfo("CloseApplicationMessage received. Shutting down.");
|
||||
((App)r).Exit();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception occurred", e.Exception);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
MainViewModel?.Dispose();
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="WorkspacesEditor.Views.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:WorkspacesEditor.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:WorkspacesEditor.Models"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<converters:BooleanToInvertedVisibilityConverter x:Key="BooleanToInvertedVisibilityConverter" />
|
||||
<converters:LaunchButtonNameConverter x:Key="LaunchButtonNameConverter" />
|
||||
<converters:MoreOptionsButtonNameConverter x:Key="MoreOptionsButtonNameConverter" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- header + create button -->
|
||||
<TextBlock
|
||||
x:Name="WorkspacesHeaderBlock"
|
||||
Grid.Row="0"
|
||||
Margin="24,0,48,16"
|
||||
AutomationProperties.HeadingLevel="Level1"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
|
||||
<Button
|
||||
x:Name="NewProjectButton"
|
||||
x:Uid="CreateWorkspaceBtn"
|
||||
Margin="0,0,24,36"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Click="NewProjectButton_Click"
|
||||
Style="{ThemeResource AccentButtonStyle}"
|
||||
TabIndex="3">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
x:Name="CreateWorkspaceText"
|
||||
Margin="8,0,0,0"
|
||||
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- search + sort -->
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="24,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBox
|
||||
x:Name="SearchTextBox"
|
||||
x:Uid="SearchTextBox"
|
||||
Width="320"
|
||||
PlaceholderText="Search for Workspaces or apps"
|
||||
Text="{x:Bind ViewModel.SearchTerm, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,0,24,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
x:Name="SortByLabel"
|
||||
Margin="12,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<ComboBox
|
||||
x:Uid="SortByComboBox"
|
||||
MinWidth="140"
|
||||
SelectedIndex="{x:Bind ViewModel.OrderByIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="LastLaunchedItem" />
|
||||
<ComboBoxItem x:Uid="CreatedItem" />
|
||||
<ComboBoxItem x:Uid="NameItem" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- empty state -->
|
||||
<TextBlock
|
||||
x:Name="EmptyStateText"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.LiveSetting="Polite"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.EmptyWorkspacesViewMessage, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
Visibility="{x:Bind ViewModel.IsWorkspacesViewEmpty, Mode=OneWay, Converter={StaticResource BoolToVis}}" />
|
||||
|
||||
<!-- workspace list -->
|
||||
<ListView
|
||||
x:Name="WorkspacesList"
|
||||
Grid.Row="2"
|
||||
Margin="24,24,24,0"
|
||||
AutomationProperties.Name="Workspace list"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="WorkspaceItemClicked"
|
||||
ItemsSource="{x:Bind ViewModel.WorkspacesView, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Visibility="{x:Bind ViewModel.IsWorkspacesViewEmpty, Mode=OneWay, Converter={StaticResource BooleanToInvertedVisibilityConverter}}">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0,4,0,0" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Project">
|
||||
<Grid
|
||||
DataContext="{x:Bind}"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.Name="{x:Bind Name}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel
|
||||
Margin="12,8,8,8"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Vertical">
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
<StackPanel Margin="0,0,0,8" Orientation="Horizontal">
|
||||
<Image
|
||||
Height="16"
|
||||
Source="{x:Bind PreviewIcons, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="8,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind AppsCountString, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind LastLaunched, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="12"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="0,0,8,0"
|
||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay, Converter={StaticResource LaunchButtonNameConverter}}"
|
||||
Click="LaunchButton_Click"
|
||||
x:Uid="LaunchBtn" />
|
||||
<Button
|
||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay, Converter={StaticResource MoreOptionsButtonNameConverter}}"
|
||||
x:Uid="MoreOptionsBtn"
|
||||
Padding="8">
|
||||
<TextBlock
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
Click="EditButtonClicked"
|
||||
x:Uid="EditFlyoutItem">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
Click="DeleteButtonClicked"
|
||||
x:Uid="RemoveFlyoutItem">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
using WorkspacesEditor.Helpers;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor.Views
|
||||
{
|
||||
public sealed partial class MainPage : Page
|
||||
{
|
||||
public MainViewModel ViewModel { get; private set; }
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
WorkspacesHeaderBlock.Text = ResourceLoaderInstance.ResourceLoader?.GetString("Workspaces") ?? "Workspaces";
|
||||
CreateWorkspaceText.Text = ResourceLoaderInstance.ResourceLoader?.GetString("CreateWorkspace") ?? "Create Workspace";
|
||||
SortByLabel.Text = ResourceLoaderInstance.ResourceLoader?.GetString("SortBy") ?? "Sort by";
|
||||
SearchTextBox.PlaceholderText = ResourceLoaderInstance.ResourceLoader?.GetString("SearchExplanation") ?? "Search for Workspaces or apps";
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
if (e.Parameter is MainViewModel vm)
|
||||
{
|
||||
ViewModel = vm;
|
||||
this.DataContext = vm;
|
||||
Bindings.Update();
|
||||
|
||||
vm.PropertyChanged += (s, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(vm.IsWorkspacesViewEmpty) && vm.IsWorkspacesViewEmpty)
|
||||
{
|
||||
var peer = Microsoft.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer.CreatePeerForElement(EmptyStateText);
|
||||
peer?.RaiseAutomationEvent(Microsoft.UI.Xaml.Automation.Peers.AutomationEvents.LiveRegionChanged);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void NewProjectButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.EnterSnapshotMode(false);
|
||||
}
|
||||
|
||||
private void EditButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.CloseAllPopups();
|
||||
Project selectedProject = GetProjectFromSender(sender);
|
||||
if (selectedProject != null)
|
||||
{
|
||||
ViewModel.EditProject(selectedProject);
|
||||
}
|
||||
}
|
||||
|
||||
private void WorkspaceItemClicked(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is Project project)
|
||||
{
|
||||
ViewModel.CloseAllPopups();
|
||||
ViewModel.EditProject(project);
|
||||
}
|
||||
}
|
||||
|
||||
private static Project GetProjectFromSender(object sender)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
// Direct DataContext (works for card button with DataContext="{x:Bind}")
|
||||
if (element.DataContext is Project project)
|
||||
{
|
||||
return project;
|
||||
}
|
||||
|
||||
// For MenuFlyoutItems inside a flyout, walk up the visual tree
|
||||
var parent = element;
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent.DataContext is Project p)
|
||||
{
|
||||
return p;
|
||||
}
|
||||
|
||||
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent) as FrameworkElement;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async void DeleteButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Project selectedProject = GetProjectFromSender(sender);
|
||||
if (selectedProject != null)
|
||||
{
|
||||
selectedProject.IsPopupVisible = false;
|
||||
|
||||
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
|
||||
{
|
||||
Title = ResourceLoaderInstance.ResourceLoader?.GetString("Are_You_Sure") ?? "Are you sure?",
|
||||
Content = ResourceLoaderInstance.ResourceLoader?.GetString("Are_You_Sure_Description") ?? "Are you sure you want to delete this Workspace?",
|
||||
PrimaryButtonText = ResourceLoaderInstance.ResourceLoader?.GetString("Delete") ?? "Remove",
|
||||
CloseButtonText = ResourceLoaderInstance.ResourceLoader?.GetString("Cancel") ?? "Cancel",
|
||||
DefaultButton = Microsoft.UI.Xaml.Controls.ContentDialogButton.Close,
|
||||
XamlRoot = this.XamlRoot,
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary)
|
||||
{
|
||||
ViewModel.DeleteProject(selectedProject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void LaunchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Project selectedProject = GetProjectFromSender(sender);
|
||||
if (selectedProject != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ViewModel.LaunchProjectAsync(selectedProject);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
ManagedCommon.Logger.LogError($"LaunchProject failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Window
|
||||
x:Class="WorkspacesEditor.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="Workspaces"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="0,16,0,0">
|
||||
<Frame x:Name="ContentFrame" />
|
||||
<ProgressRing
|
||||
x:Name="LoadingRing"
|
||||
Width="48"
|
||||
Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Loading"
|
||||
AutomationProperties.LiveSetting="Polite"
|
||||
IsActive="False"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,313 @@
|
||||
// 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;
|
||||
using System.Threading;
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using WinRT.Interop;
|
||||
using WorkspacesEditor.Helpers;
|
||||
using WorkspacesEditor.Messages;
|
||||
using WorkspacesEditor.Views;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
public sealed partial class MainWindow : Window, IDisposable
|
||||
{
|
||||
public const int MinWindowWidth = 750;
|
||||
public const int MinWindowHeight = 680;
|
||||
|
||||
private readonly CancellationTokenSource _cancellationToken = new();
|
||||
private readonly AppWindow _appWindow;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
SetMinSize(hwnd, MinWindowWidth, MinWindowHeight);
|
||||
RestoreWindowState(hwnd);
|
||||
|
||||
// Set title from resource or fallback
|
||||
try
|
||||
{
|
||||
this.Title = ResourceLoaderInstance.ResourceLoader?.GetString("MainTitle") ?? "Workspaces";
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.Title = "Workspaces";
|
||||
}
|
||||
|
||||
this.Closed += OnClosed;
|
||||
|
||||
// Listen for hotkey toggle event
|
||||
StartHotkeyEventLoop(hwnd);
|
||||
|
||||
// Wire ViewModel navigation via messenger
|
||||
// Use StrongReferenceMessenger for MainWindow since Window is not rooted
|
||||
// in the visual tree and WeakReferenceMessenger may GC the registration.
|
||||
var vm = App.MainViewModel;
|
||||
StrongReferenceMessenger.Default.Register<NavigateToEditorMessage>(this, (r, m) =>
|
||||
{
|
||||
ContentFrame.Navigate(typeof(Views.WorkspacesEditorPage), (vm, m.Project));
|
||||
});
|
||||
StrongReferenceMessenger.Default.Register<GoBackMessage>(this, (r, m) =>
|
||||
{
|
||||
if (ContentFrame.CanGoBack)
|
||||
{
|
||||
ContentFrame.GoBack();
|
||||
}
|
||||
});
|
||||
StrongReferenceMessenger.Default.Register<MinimizeWindowMessage>(this, (r, m) =>
|
||||
{
|
||||
ShowWindow(WindowNative.GetWindowHandle(this), 6); // SW_MINIMIZE
|
||||
});
|
||||
StrongReferenceMessenger.Default.Register<RestoreWindowMessage>(this, (r, m) =>
|
||||
{
|
||||
ShowWindow(WindowNative.GetWindowHandle(this), 9); // SW_RESTORE
|
||||
});
|
||||
|
||||
// Listen for snapshot window requests from ViewModel
|
||||
OverlayBorder overlayBorder = null;
|
||||
StrongReferenceMessenger.Default.Register<ShowSnapshotWindowMessage>(this, (r, m) =>
|
||||
{
|
||||
// Show red border overlay around all displays
|
||||
var displays = OverlayBorder.GetAllMonitorBounds();
|
||||
overlayBorder = OverlayBorder.CreateForAllMonitors(displays);
|
||||
|
||||
var snapshotWindow = new Views.SnapshotWindow();
|
||||
snapshotWindow.Closed += (s, args) =>
|
||||
{
|
||||
overlayBorder?.Dispose();
|
||||
overlayBorder = null;
|
||||
};
|
||||
snapshotWindow.Activate();
|
||||
});
|
||||
|
||||
// Bind loading ring to ViewModel.IsLoading
|
||||
vm.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(vm.IsLoading))
|
||||
{
|
||||
LoadingRing.IsActive = vm.IsLoading;
|
||||
LoadingRing.Visibility = vm.IsLoading
|
||||
? Microsoft.UI.Xaml.Visibility.Visible
|
||||
: Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
};
|
||||
|
||||
// Navigate to main page
|
||||
ContentFrame.Navigate(typeof(Views.MainPage), vm);
|
||||
|
||||
Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.WorkspacesEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
|
||||
}
|
||||
|
||||
private void RestoreWindowState(IntPtr hwnd)
|
||||
{
|
||||
var state = WindowStateHelper.Load();
|
||||
|
||||
if (state != null && state.IsValid())
|
||||
{
|
||||
// Use AppWindow for positioning — it handles DPI correctly for WinUI windows
|
||||
_appWindow.Move(new Windows.Graphics.PointInt32((int)state.Left, (int)state.Top));
|
||||
_appWindow.Resize(new Windows.Graphics.SizeInt32((int)state.Width, (int)state.Height));
|
||||
|
||||
if (state.Maximized)
|
||||
{
|
||||
ShowWindow(hwnd, 3); // SW_SHOWMAXIMIZED
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// First launch: center on current display at 90% height, 75% width
|
||||
var displayArea = DisplayArea.GetFromWindowId(
|
||||
Win32Interop.GetWindowIdFromWindow(hwnd),
|
||||
DisplayAreaFallback.Primary);
|
||||
var workArea = displayArea.WorkArea;
|
||||
|
||||
int width = (int)(workArea.Width * 0.75);
|
||||
int height = (int)(workArea.Height * 0.90);
|
||||
int x = workArea.X + (int)(workArea.Width * 0.125);
|
||||
int y = workArea.Y + (int)(workArea.Height * 0.05);
|
||||
|
||||
_appWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
private void StartHotkeyEventLoop(IntPtr hwnd)
|
||||
{
|
||||
var token = _cancellationToken.Token;
|
||||
new Thread(() =>
|
||||
{
|
||||
var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, PowerToys.Interop.Constants.WorkspacesHotkeyEvent());
|
||||
while (true)
|
||||
{
|
||||
if (WaitHandle.WaitAny(new WaitHandle[] { token.WaitHandle, eventHandle }) == 1)
|
||||
{
|
||||
App.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (ApplicationIsInFocus())
|
||||
{
|
||||
StrongReferenceMessenger.Default.Send(new CloseApplicationMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowHelpers.BringToForeground(hwnd);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}) { IsBackground = true }.Start();
|
||||
}
|
||||
|
||||
private void SaveWindowState()
|
||||
{
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
bool isMaximized = IsWindowMaximized(hwnd);
|
||||
|
||||
// Use AppWindow for both save and restore — same coordinate space, no DPI mismatch
|
||||
var pos = _appWindow.Position;
|
||||
var size = _appWindow.Size;
|
||||
WindowStateHelper.Save(new WindowStateData
|
||||
{
|
||||
Top = pos.Y,
|
||||
Left = pos.X,
|
||||
Width = size.Width,
|
||||
Height = size.Height,
|
||||
Maximized = isMaximized,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
SaveWindowState();
|
||||
_cancellationToken.Dispose();
|
||||
(Application.Current as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
private static bool ApplicationIsInFocus()
|
||||
{
|
||||
var activatedHandle = GetForegroundWindow();
|
||||
if (activatedHandle == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var procId = Environment.ProcessId;
|
||||
_ = GetWindowThreadProcessId(activatedHandle, out int activeProcId);
|
||||
|
||||
return activeProcId == procId;
|
||||
}
|
||||
|
||||
private static void SetMinSize(IntPtr hwnd, int minWidth, int minHeight)
|
||||
{
|
||||
var subclassId = (nuint)1;
|
||||
SubclassProc callback = (hWnd, msg, wParam, lParam, id, data) =>
|
||||
{
|
||||
if (msg == WmGetminmaxinfo)
|
||||
{
|
||||
var mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
|
||||
mmi.PtMinTrackSize.X = minWidth;
|
||||
mmi.PtMinTrackSize.Y = minHeight;
|
||||
Marshal.StructureToPtr(mmi, lParam, false);
|
||||
}
|
||||
|
||||
return DefSubclassProc(hWnd, msg, wParam, lParam);
|
||||
};
|
||||
|
||||
// prevent GC of delegate
|
||||
_subclassCallback = callback;
|
||||
SetWindowSubclass(hwnd, callback, subclassId, 0);
|
||||
}
|
||||
|
||||
private static SubclassProc _subclassCallback;
|
||||
|
||||
private const uint WmGetminmaxinfo = 0x0024;
|
||||
|
||||
private delegate IntPtr SubclassProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, nuint id, nuint data);
|
||||
|
||||
[DllImport("comctl32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetWindowSubclass(IntPtr hWnd, SubclassProc pfnSubclass, nuint uIdSubclass, nuint dwRefData);
|
||||
|
||||
[DllImport("comctl32.dll")]
|
||||
private static extern IntPtr DefSubclassProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MINMAXINFO
|
||||
{
|
||||
public POINT PtReserved;
|
||||
public POINT PtMaxSize;
|
||||
public POINT PtMaxPosition;
|
||||
public POINT PtMinTrackSize;
|
||||
public POINT PtMaxTrackSize;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationToken?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// Win32 interop
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl);
|
||||
|
||||
private static bool IsWindowMaximized(IntPtr hwnd)
|
||||
{
|
||||
GetWindowPlacement(hwnd, out WINDOWPLACEMENT placement);
|
||||
return placement.ShowCmd == 3; // SW_SHOWMAXIMIZED
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WINDOWPLACEMENT
|
||||
{
|
||||
public uint Length;
|
||||
public uint Flags;
|
||||
public uint ShowCmd;
|
||||
public POINT PtMinPosition;
|
||||
public POINT PtMaxPosition;
|
||||
public RECT RcNormalPosition;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
using Windows.Graphics;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace WorkspacesEditor.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates 4 thin opaque red bar windows forming a border frame around a display area.
|
||||
/// Click-through so the user can interact with their desktop beneath.
|
||||
/// </summary>
|
||||
internal sealed class OverlayBorder : IDisposable
|
||||
{
|
||||
private const int BorderThickness = 6;
|
||||
private readonly List<Window> _windows = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bounds of all monitors via Win32 EnumDisplayMonitors.
|
||||
/// </summary>
|
||||
public static List<RectInt32> GetAllMonitorBounds()
|
||||
{
|
||||
var monitors = new List<RectInt32>();
|
||||
EnumDisplayMonitors(
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
(IntPtr hMonitor, IntPtr hdc, ref Rect lprcMonitor, IntPtr dwData) =>
|
||||
{
|
||||
monitors.Add(new RectInt32(
|
||||
lprcMonitor.Left,
|
||||
lprcMonitor.Top,
|
||||
lprcMonitor.Right - lprcMonitor.Left,
|
||||
lprcMonitor.Bottom - lprcMonitor.Top));
|
||||
return true;
|
||||
},
|
||||
IntPtr.Zero);
|
||||
return monitors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates overlay borders around all monitors.
|
||||
/// </summary>
|
||||
public static OverlayBorder CreateForAllMonitors(IEnumerable<RectInt32> monitorBounds)
|
||||
{
|
||||
var overlay = new OverlayBorder();
|
||||
foreach (var bounds in monitorBounds)
|
||||
{
|
||||
overlay.CreateBorderForRect(bounds);
|
||||
}
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates 4 strip windows (top, bottom, left, right) forming a red frame.
|
||||
/// All bars extend to full length so corners connect cleanly.
|
||||
/// </summary>
|
||||
private void CreateBorderForRect(RectInt32 bounds)
|
||||
{
|
||||
// Top bar — full width
|
||||
CreateStrip(bounds.X, bounds.Y, bounds.Width, BorderThickness);
|
||||
|
||||
// Bottom bar — full width
|
||||
CreateStrip(bounds.X, bounds.Y + bounds.Height - BorderThickness, bounds.Width, BorderThickness);
|
||||
|
||||
// Left bar — full height (overlaps corners)
|
||||
CreateStrip(bounds.X, bounds.Y, BorderThickness, bounds.Height);
|
||||
|
||||
// Right bar — full height (overlaps corners)
|
||||
CreateStrip(bounds.X + bounds.Width - BorderThickness, bounds.Y, BorderThickness, bounds.Height);
|
||||
}
|
||||
|
||||
private void CreateStrip(int x, int y, int width, int height)
|
||||
{
|
||||
var window = new Window();
|
||||
window.Content = new Microsoft.UI.Xaml.Controls.Grid
|
||||
{
|
||||
Background = new SolidColorBrush(Microsoft.UI.Colors.Red),
|
||||
};
|
||||
|
||||
// Get native handle and configure
|
||||
var hwnd = WindowNative.GetWindowHandle(window);
|
||||
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
var appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
// Remove title bar and borders
|
||||
if (appWindow.Presenter is OverlappedPresenter presenter)
|
||||
{
|
||||
presenter.IsAlwaysOnTop = true;
|
||||
presenter.IsResizable = false;
|
||||
presenter.IsMaximizable = false;
|
||||
presenter.IsMinimizable = false;
|
||||
presenter.SetBorderAndTitleBar(false, false);
|
||||
}
|
||||
|
||||
// Disable DWM shadow/gradient and window chrome completely
|
||||
int ncrpDisabled = 2; // DWMNCRP_DISABLED
|
||||
_ = DwmSetWindowAttribute(hwnd, 2, ref ncrpDisabled, sizeof(int)); // DWMWA_NCRENDERING_POLICY
|
||||
|
||||
// Remove rounded corners (Windows 11)
|
||||
int cornerPref = 1; // DWMWCP_DONOTROUND
|
||||
_ = DwmSetWindowAttribute(hwnd, 33, ref cornerPref, sizeof(int)); // DWMWA_WINDOW_CORNER_PREFERENCE
|
||||
|
||||
// Remove window border color
|
||||
int colorNone = unchecked((int)0xFFFFFFFE); // DWMWA_COLOR_NONE
|
||||
_ = DwmSetWindowAttribute(hwnd, 34, ref colorNone, sizeof(int)); // DWMWA_BORDER_COLOR
|
||||
|
||||
// Disable shadow
|
||||
var margins = new Margins { Left = 0, Right = 0, Top = 0, Bottom = 0 };
|
||||
_ = DwmExtendFrameIntoClientArea(hwnd, ref margins);
|
||||
|
||||
// Remove WS_OVERLAPPEDWINDOW style, set WS_POPUP for minimal chrome
|
||||
int style = GetWindowLong(hwnd, GwlStyle);
|
||||
style &= ~WsOverlappedwindow;
|
||||
style |= WsPopup;
|
||||
_ = SetWindowLong(hwnd, GwlStyle, style);
|
||||
|
||||
// Make click-through + no taskbar entry
|
||||
int exStyle = GetWindowLong(hwnd, GwlExstyle);
|
||||
_ = SetWindowLong(hwnd, GwlExstyle, exStyle | WsExTransparent | WsExToolwindow | WsExTopmost);
|
||||
|
||||
// Position and size via SetWindowPos (bypasses AppWindow min-size constraints)
|
||||
_ = SetWindowPos(hwnd, HwndTopmost, x, y, width, height, SwpNoactivate | SwpShowwindow);
|
||||
|
||||
// Show
|
||||
window.Activate();
|
||||
|
||||
_windows.Add(window);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var window in _windows)
|
||||
{
|
||||
try
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
_windows.Clear();
|
||||
}
|
||||
|
||||
// Win32 interop
|
||||
private const int GwlStyle = -16;
|
||||
private const int GwlExstyle = -20;
|
||||
private const int WsOverlappedwindow = 0x00CF0000;
|
||||
private const int WsPopup = unchecked((int)0x80000000);
|
||||
private const int WsExTransparent = 0x00000020;
|
||||
private const int WsExToolwindow = 0x00000080;
|
||||
private const int WsExTopmost = 0x00000008;
|
||||
private const int SwpNoactivate = 0x0010;
|
||||
private const int SwpShowwindow = 0x0040;
|
||||
private static readonly IntPtr HwndTopmost = new IntPtr(-1);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref Margins margins);
|
||||
|
||||
private delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdc, ref Rect lprcMonitor, IntPtr dwData);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct Rect
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct Margins
|
||||
{
|
||||
public int Left;
|
||||
public int Right;
|
||||
public int Top;
|
||||
public int Bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Window
|
||||
x:Class="WorkspacesEditor.Views.SnapshotWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="Snapshot Creator"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentControl
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="8"
|
||||
VerticalAlignment="Center"
|
||||
IsTabStop="True"
|
||||
AutomationProperties.Name="{Binding Text, ElementName=DescriptionText}">
|
||||
<TextBlock
|
||||
x:Name="DescriptionText"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
HorizontalTextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</ContentControl>
|
||||
<Button
|
||||
x:Name="SnapshotButton"
|
||||
Grid.Row="1"
|
||||
Margin="8,8,4,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="SnapshotButtonClicked"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="4,8,8,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="CancelButtonClicked" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
using WinRT.Interop;
|
||||
using WorkspacesEditor.Helpers;
|
||||
using WorkspacesEditor.Messages;
|
||||
|
||||
namespace WorkspacesEditor.Views
|
||||
{
|
||||
public sealed partial class SnapshotWindow : Window
|
||||
{
|
||||
private bool _captured;
|
||||
|
||||
public SnapshotWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
this.Title = ResourceLoaderInstance.ResourceLoader?.GetString("SnapshotWindowTitle") ?? "Snapshot Creator";
|
||||
string description = ResourceLoaderInstance.ResourceLoader?.GetString("SnapshotDescription") ?? "Edit your layout and click \"Capture\" when finished.";
|
||||
DescriptionText.Text = description;
|
||||
|
||||
string captureText = ResourceLoaderInstance.ResourceLoader?.GetString("Take_Snapshot") ?? "Capture";
|
||||
SnapshotButton.Content = captureText;
|
||||
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(SnapshotButton, captureText);
|
||||
|
||||
string cancelText = ResourceLoaderInstance.ResourceLoader?.GetString("Cancel") ?? "Cancel";
|
||||
CancelButton.Content = cancelText;
|
||||
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(CancelButton, cancelText);
|
||||
|
||||
// Configure window: small, centered, no resize, topmost
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
var appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
appWindow.Resize(new Windows.Graphics.SizeInt32(420, 200));
|
||||
|
||||
if (appWindow.Presenter is OverlappedPresenter presenter)
|
||||
{
|
||||
presenter.IsResizable = false;
|
||||
presenter.IsMaximizable = false;
|
||||
presenter.IsAlwaysOnTop = true;
|
||||
}
|
||||
|
||||
// Center on primary display
|
||||
var displayArea = DisplayArea.Primary;
|
||||
var workArea = displayArea.WorkArea;
|
||||
int x = workArea.X + ((workArea.Width - 420) / 2);
|
||||
int y = workArea.Y + ((workArea.Height - 200) / 2);
|
||||
appWindow.Move(new Windows.Graphics.PointInt32(x, y));
|
||||
|
||||
this.Closed += OnClosed;
|
||||
|
||||
// Set focus to the Capture button when window loads
|
||||
this.Activated += (s, e) =>
|
||||
{
|
||||
var snapshotHwnd = WindowNative.GetWindowHandle(this);
|
||||
SetForegroundWindow(snapshotHwnd);
|
||||
SnapshotButton.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
};
|
||||
|
||||
// Handle Escape key to cancel
|
||||
this.Content.KeyDown += (s, e) =>
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Escape)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void SnapshotButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_captured = true;
|
||||
this.Close();
|
||||
StrongReferenceMessenger.Default.Send(new SnapshotCapturedMessage());
|
||||
}
|
||||
|
||||
private void CancelButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void OnClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
if (!_captured)
|
||||
{
|
||||
StrongReferenceMessenger.Default.Send(new SnapshotCancelledMessage());
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="WorkspacesEditor.Views.WorkspacesEditorPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:WorkspacesEditor.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:WorkspacesEditor.Models"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
|
||||
<DataTemplate x:Key="headerTemplate" x:DataType="models:MonitorHeaderRow">
|
||||
<Border HorizontalAlignment="Stretch">
|
||||
<TextBlock
|
||||
Margin="0,16,0,8"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding MonitorName}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="appTemplate" x:DataType="models:Application">
|
||||
<Border Margin="0,4,0,0">
|
||||
<Expander
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
AutomationProperties.AutomationId="{Binding AppName}"
|
||||
AutomationProperties.Name="{Binding AppName}"
|
||||
IsEnabled="{Binding IsIncluded, Mode=OneWay}"
|
||||
IsExpanded="{Binding IsExpanded, Mode=TwoWay}">
|
||||
<Expander.Header>
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Column="1"
|
||||
Width="20"
|
||||
Height="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Source="{Binding IconImage, Mode=OneWay}" />
|
||||
|
||||
<StackPanel Grid.Column="3" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding AppName}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding RepeatIndexString, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||
Text=""
|
||||
Visibility="{Binding IsNotFound, Converter={StaticResource BoolToVis}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Text="{Binding AppMainParams, Mode=OneWay}"
|
||||
Visibility="{Binding IsAppMainParamVisible, Converter={StaticResource BoolToVis}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Width="Auto"
|
||||
Margin="12,4"
|
||||
AutomationProperties.Name="{Binding DeleteButtonAccessibleName, Mode=OneWay}"
|
||||
Click="DeleteButtonClicked"
|
||||
Content="{Binding DeleteButtonContent, Mode=OneWay}"
|
||||
IsEnabled="True" />
|
||||
</Grid>
|
||||
</Expander.Header>
|
||||
<Grid Margin="52,8,48,8" HorizontalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox
|
||||
MinWidth="12"
|
||||
IsChecked="{Binding IsElevated, Mode=TwoWay}"
|
||||
IsEnabled="{Binding CanLaunchElevated, Mode=OneWay}">
|
||||
<TextBlock x:Uid="LaunchAsAdminLabel" />
|
||||
</CheckBox>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,16,0,0"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
x:Uid="CliArgumentsLabel" />
|
||||
<TextBox
|
||||
x:Uid="CliArgsTextBox"
|
||||
Margin="12,0,0,0"
|
||||
MinWidth="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
Text="{Binding CommandLineArguments, Mode=TwoWay}"
|
||||
TextChanged="CommandLineTextBox_TextChanged" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,16,0,0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
x:Uid="WindowPositionLabel" />
|
||||
<ComboBox
|
||||
x:Uid="WindowPositionComboBox"
|
||||
VerticalAlignment="Center"
|
||||
SelectedIndex="{Binding PositionComboboxIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="CustomItem" />
|
||||
<ComboBoxItem x:Uid="MaximizedItem" />
|
||||
<ComboBoxItem x:Uid="MinimizedItem" />
|
||||
</ComboBox>
|
||||
<TextBlock VerticalAlignment="Center" x:Uid="LeftLabel" />
|
||||
<TextBox
|
||||
x:Uid="LeftTextBox"
|
||||
MinWidth="60"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay}"
|
||||
Text="{Binding Position.X, Mode=OneWay}"
|
||||
TextChanged="LeftTextBox_TextChanged" />
|
||||
<TextBlock VerticalAlignment="Center" x:Uid="TopLabel" />
|
||||
<TextBox
|
||||
x:Uid="TopTextBox"
|
||||
MinWidth="60"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay}"
|
||||
Text="{Binding Position.Y, Mode=OneWay}"
|
||||
TextChanged="TopTextBox_TextChanged" />
|
||||
<TextBlock VerticalAlignment="Center" x:Uid="WidthLabel" />
|
||||
<TextBox
|
||||
x:Uid="WidthTextBox"
|
||||
MinWidth="60"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay}"
|
||||
Text="{Binding Position.Width, Mode=OneWay}"
|
||||
TextChanged="WidthTextBox_TextChanged" />
|
||||
<TextBlock VerticalAlignment="Center" x:Uid="HeightLabel" />
|
||||
<TextBox
|
||||
x:Uid="HeightTextBox"
|
||||
MinWidth="60"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay}"
|
||||
Text="{Binding Position.Height, Mode=OneWay}"
|
||||
TextChanged="HeightTextBox_TextChanged" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
<models:AppListDataTemplateSelector
|
||||
x:Key="AppListDataTemplateSelector"
|
||||
AppTemplate="{StaticResource appTemplate}"
|
||||
HeaderTemplate="{StaticResource headerTemplate}" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- breadcrumb + Save/Cancel -->
|
||||
<Grid Margin="24,0,24,24">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel VerticalAlignment="Top" Orientation="Horizontal">
|
||||
<Button
|
||||
AutomationProperties.Name="Back to Workspaces"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
Click="CancelButtonClicked"
|
||||
FontSize="24">
|
||||
<TextBlock x:Name="WorkspacesBackText" Text="Workspaces" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
AutomationProperties.HeadingLevel="Level1"
|
||||
Text="{Binding EditorWindowTitle, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="SaveButton"
|
||||
x:Uid="SaveBtn"
|
||||
Click="SaveButtonClicked"
|
||||
IsEnabled="{Binding CanBeSaved, Mode=OneWay}"
|
||||
Style="{ThemeResource AccentButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
x:Name="SaveText"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="Save" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
Margin="8,0,0,0"
|
||||
Click="CancelButtonClicked">
|
||||
<TextBlock x:Name="CancelText" Text="Cancel" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- properties -->
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="24,0,24,0"
|
||||
Orientation="Vertical">
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}">
|
||||
<Run x:Name="WorkspaceNameLabel" Text="Workspace name" />
|
||||
</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="24">
|
||||
<TextBox
|
||||
x:Name="EditNameTextBox"
|
||||
x:Uid="EditNameTextBox"
|
||||
Width="300"
|
||||
HorizontalAlignment="Left"
|
||||
GotFocus="EditNameTextBox_GotFocus"
|
||||
KeyDown="EditNameTextBoxKeyDown"
|
||||
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextChanged="EditNameTextBox_TextChanged" />
|
||||
<CheckBox
|
||||
VerticalAlignment="Bottom"
|
||||
IsChecked="{Binding IsShortcutNeeded, Mode=TwoWay}">
|
||||
<TextBlock x:Name="CreateShortcutLabel" Text="Create desktop shortcut" />
|
||||
</CheckBox>
|
||||
<CheckBox
|
||||
VerticalAlignment="Bottom"
|
||||
IsChecked="{Binding MoveExistingWindows, Mode=TwoWay}">
|
||||
<TextBlock x:Name="MoveIfExistLabel" Text="Move existing windows" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Launch&Edit / Revert -->
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="24,16,24,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button
|
||||
x:Name="LaunchEditButton"
|
||||
Click="LaunchEditButtonClicked">
|
||||
<TextBlock x:Name="LaunchEditText" Text="Launch & edit" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="RevertButton"
|
||||
Click="RevertButtonClicked"
|
||||
IsEnabled="{Binding IsRevertEnabled, Mode=OneWay}">
|
||||
<TextBlock x:Name="RevertText" Text="Revert" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- app list -->
|
||||
<ScrollViewer
|
||||
Grid.Row="3"
|
||||
Margin="0,24,0,0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="24,0,24,24" Orientation="Vertical">
|
||||
<ItemsControl
|
||||
x:Name="CapturedAppList"
|
||||
x:Uid="CapturedAppListControl"
|
||||
ItemTemplateSelector="{StaticResource AppListDataTemplateSelector}"
|
||||
ItemsSource="{Binding ApplicationsListed, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
using Windows.System;
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesEditor.Helpers;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
using Application = WorkspacesEditor.Models.Application;
|
||||
using Project = WorkspacesEditor.Models.Project;
|
||||
|
||||
namespace WorkspacesEditor.Views
|
||||
{
|
||||
public sealed partial class WorkspacesEditorPage : Page
|
||||
{
|
||||
private MainViewModel _mainViewModel;
|
||||
|
||||
public WorkspacesEditorPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
SetLocalizedStrings();
|
||||
|
||||
this.KeyDown += (s, e) =>
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Escape)
|
||||
{
|
||||
TempProjectData.DeleteTempFile();
|
||||
_mainViewModel?.SwitchToMainView();
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
if (e.Parameter is (MainViewModel vm, Project project))
|
||||
{
|
||||
_mainViewModel = vm;
|
||||
this.DataContext = project;
|
||||
|
||||
// Set focus to the name field so Narrator announces the page context
|
||||
this.Loaded += (s, args) => EditNameTextBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLocalizedStrings()
|
||||
{
|
||||
WorkspacesBackText.Text = ResourceLoaderInstance.ResourceLoader?.GetString("Workspaces") ?? "Workspaces";
|
||||
SaveText.Text = ResourceLoaderInstance.ResourceLoader?.GetString("Save_Workspace") ?? "Save";
|
||||
CancelText.Text = ResourceLoaderInstance.ResourceLoader?.GetString("Cancel") ?? "Cancel";
|
||||
WorkspaceNameLabel.Text = ResourceLoaderInstance.ResourceLoader?.GetString("WorkspaceName") ?? "Workspace name";
|
||||
CreateShortcutLabel.Text = ResourceLoaderInstance.ResourceLoader?.GetString("CreateShortcut") ?? "Create desktop shortcut";
|
||||
MoveIfExistLabel.Text = ResourceLoaderInstance.ResourceLoader?.GetString("MoveIfExist") ?? "Move existing windows";
|
||||
LaunchEditText.Text = ResourceLoaderInstance.ResourceLoader?.GetString("LaunchEdit") ?? "Launch & edit";
|
||||
RevertText.Text = ResourceLoaderInstance.ResourceLoader?.GetString("Revert") ?? "Revert";
|
||||
}
|
||||
|
||||
private void SaveButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (this.DataContext is Project projectToSave)
|
||||
{
|
||||
projectToSave.CloseExpanders();
|
||||
|
||||
if (_mainViewModel.Workspaces.Any(x => x.Id == projectToSave.Id))
|
||||
{
|
||||
_mainViewModel.SaveProject(projectToSave);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainViewModel.AddNewProject(projectToSave);
|
||||
}
|
||||
|
||||
_mainViewModel.SwitchToMainView();
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
TempProjectData.DeleteTempFile();
|
||||
_mainViewModel.SwitchToMainView();
|
||||
}
|
||||
|
||||
private void DeleteButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element && element.DataContext is Application app)
|
||||
{
|
||||
app.SwitchDeletion();
|
||||
}
|
||||
}
|
||||
|
||||
private void EditNameTextBoxKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Enter)
|
||||
{
|
||||
e.Handled = true;
|
||||
if (this.DataContext is Project project && sender is TextBox textBox)
|
||||
{
|
||||
project.Name = textBox.Text;
|
||||
}
|
||||
}
|
||||
else if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
e.Handled = true;
|
||||
if (this.DataContext is Project project)
|
||||
{
|
||||
_mainViewModel.CancelProjectName(project);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EditNameTextBox_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.SaveProjectName(DataContext as Project);
|
||||
}
|
||||
|
||||
private void EditNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (this.DataContext is Project project && sender is TextBox textBox)
|
||||
{
|
||||
project.Name = textBox.Text;
|
||||
}
|
||||
}
|
||||
|
||||
private void LeftTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox textBox && textBox.DataContext is Application app)
|
||||
{
|
||||
if (!int.TryParse(textBox.Text, out int newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
app.Position = new Application.WindowPosition() { X = newPos, Y = app.Position.Y, Width = app.Position.Width, Height = app.Position.Height };
|
||||
app.Parent.IsPositionChangedManually = true;
|
||||
app.Parent.InitializePreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void TopTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox textBox && textBox.DataContext is Application app)
|
||||
{
|
||||
if (!int.TryParse(textBox.Text, out int newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
app.Position = new Application.WindowPosition() { X = app.Position.X, Y = newPos, Width = app.Position.Width, Height = app.Position.Height };
|
||||
app.Parent.IsPositionChangedManually = true;
|
||||
app.Parent.InitializePreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void WidthTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox textBox && textBox.DataContext is Application app)
|
||||
{
|
||||
if (!int.TryParse(textBox.Text, out int newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
app.Position = new Application.WindowPosition() { X = app.Position.X, Y = app.Position.Y, Width = newPos, Height = app.Position.Height };
|
||||
app.Parent.IsPositionChangedManually = true;
|
||||
app.Parent.InitializePreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void HeightTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox textBox && textBox.DataContext is Application app)
|
||||
{
|
||||
if (!int.TryParse(textBox.Text, out int newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
app.Position = new Application.WindowPosition() { X = app.Position.X, Y = app.Position.Y, Width = app.Position.Width, Height = newPos };
|
||||
app.Parent.IsPositionChangedManually = true;
|
||||
app.Parent.InitializePreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void CommandLineTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (sender is TextBox textBox && textBox.DataContext is Application app)
|
||||
{
|
||||
app.CommandLineTextChanged(textBox.Text);
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchEditButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (this.DataContext is Project project)
|
||||
{
|
||||
_ = _mainViewModel.LaunchAndEditAsync(project);
|
||||
}
|
||||
}
|
||||
|
||||
private void RevertButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.RevertLaunch();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,31 +4,39 @@
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>PowerToys.WorkspacesEditor</AssemblyTitle>
|
||||
<AssemblyDescription>PowerToys Workspaces Editor</AssemblyDescription>
|
||||
<Description>PowerToys Workspaces Editor</Description>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<RootNamespace>WorkspacesEditor</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{367D7543-7DBA-4381-99F1-BF6142A996C4}</ProjectGuid>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>..\Assets\Workspaces\Workspaces.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<AssemblyName>PowerToys.WorkspacesEditor</AssemblyName>
|
||||
<ApplicationIcon>..\Assets\Workspaces\Workspaces.ico</ApplicationIcon>
|
||||
<ProjectPriFileName>PowerToys.WorkspacesEditor.pri</ProjectPriFileName>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="Views\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="Views\App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<COMReference Include="IWshRuntimeLibrary">
|
||||
<WrapperTool>tlbimp</WrapperTool>
|
||||
@@ -49,59 +57,30 @@
|
||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\Assets\**\*.*">
|
||||
<Link>Assets\Workspaces\%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<None Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Data\WorkspacesData.cs" />
|
||||
<Compile Remove="Data\ProjectData.cs" />
|
||||
<Compile Remove="Data\WorkspacesEditorData`1.cs" />
|
||||
<Compile Remove="Utils\IOUtils.cs" />
|
||||
<Compile Remove="Utils\FolderUtils.cs" />
|
||||
<Compile Remove="Utils\DashCaseNamingPolicy.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Properties\Settings.Designer.cs">
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
17
src/modules/Workspaces/WorkspacesEditor.WinUI/app.manifest
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.WorkspacesEditor.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 compatibility for unpackaged WinUI 3 apps -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="WorkspacesEditor.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||
</startup>
|
||||
<runtime>
|
||||
<AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false" />
|
||||
</runtime>
|
||||
<userSettings>
|
||||
<WorkspacesEditor.Properties.Settings>
|
||||
<setting name="Top" serializeAs="String">
|
||||
<value>-1</value>
|
||||
</setting>
|
||||
<setting name="Left" serializeAs="String">
|
||||
<value>-1</value>
|
||||
</setting>
|
||||
<setting name="Height" serializeAs="String">
|
||||
<value>-1</value>
|
||||
</setting>
|
||||
<setting name="Width" serializeAs="String">
|
||||
<value>-1</value>
|
||||
</setting>
|
||||
<setting name="Maximized" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
</WorkspacesEditor.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
@@ -1,57 +0,0 @@
|
||||
<Application
|
||||
x:Class="WorkspacesEditor.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:WorkspacesEditor"
|
||||
Exit="OnExit"
|
||||
Startup="OnStartup"
|
||||
ThemeMode="System">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<FontFamily x:Key="SymbolThemeFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||
<Style x:Key="HeadingTextBlock" TargetType="TextBlock" />
|
||||
<Style
|
||||
x:Key="SubtleButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type Button}}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Focusable="False"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||