mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-02 08:28:55 +02:00
Compare commits
63 Commits
dev/crutka
...
workspaces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87a5fac4bc | ||
|
|
d48501be9f | ||
|
|
0362e0d5fc | ||
|
|
0697cd8774 | ||
|
|
9fb18f5bfb | ||
|
|
03c97e0366 | ||
|
|
77c53e6f9a | ||
|
|
0a2e4b5253 | ||
|
|
af03fd610a | ||
|
|
3f67465fc6 | ||
|
|
027124f98a | ||
|
|
df2d162275 | ||
|
|
123ae05e1b | ||
|
|
2a4919fe41 | ||
|
|
70e08ddd63 | ||
|
|
c17554482c | ||
|
|
bcf3c98c8a | ||
|
|
5b8eaef852 | ||
|
|
d2ba3d9ae4 | ||
|
|
285db8d4a9 | ||
|
|
740e18e081 | ||
|
|
606e03b085 | ||
|
|
d7b8fe006d | ||
|
|
35d04b6fd3 | ||
|
|
8a8887eaf8 | ||
|
|
27633f6f7d | ||
|
|
7c194bd108 | ||
|
|
1539e6e061 | ||
|
|
ecc737d0e5 | ||
|
|
b72ae3b8b5 | ||
|
|
e055f303e1 | ||
|
|
8fcdc199a0 | ||
|
|
b7bb10f5f8 | ||
|
|
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 |
@@ -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",
|
||||
|
||||
@@ -1015,7 +1015,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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
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
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
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
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>
|
||||
@@ -1,173 +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.Globalization;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.Win32;
|
||||
using WorkspacesEditor.Telemetry;
|
||||
using WorkspacesEditor.Utils;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private static Mutex _instanceMutex;
|
||||
|
||||
public static WorkspacesEditorIO WorkspacesEditorIO { get; private set; }
|
||||
|
||||
private MainWindow _mainWindow;
|
||||
|
||||
private MainViewModel _mainViewModel;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
private ETWTrace etwTrace = new ETWTrace();
|
||||
|
||||
public App()
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new WorkspacesEditorStartEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
|
||||
|
||||
WorkspacesEditorIO = new WorkspacesEditorIO();
|
||||
}
|
||||
|
||||
private void OnStartup(object sender, StartupEventArgs e)
|
||||
{
|
||||
Logger.InitializeLogger("\\Workspaces\\Logs");
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
|
||||
var languageTag = LanguageHelper.LoadLanguage();
|
||||
|
||||
if (!string.IsNullOrEmpty(languageTag))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(languageTag);
|
||||
}
|
||||
catch (CultureNotFoundException ex)
|
||||
{
|
||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
const string appName = "Local\\PowerToys_Workspaces_Editor_InstanceMutex";
|
||||
bool createdNew;
|
||||
_instanceMutex = new Mutex(true, appName, out createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
Logger.LogWarning("Another instance of Workspaces Editor is already running. Exiting this instance.");
|
||||
_instanceMutex = null;
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredWorkspacesEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
var args = e?.Args;
|
||||
int powerToysRunnerPid;
|
||||
if (args?.Length > 0)
|
||||
{
|
||||
_ = int.TryParse(args[0], out powerToysRunnerPid);
|
||||
|
||||
Logger.LogInfo($"WorkspacesEditor started from the PowerToys Runner. Runner pid={powerToysRunnerPid}");
|
||||
RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
|
||||
{
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting WorkspacesEditor");
|
||||
Dispatcher.Invoke(Shutdown);
|
||||
});
|
||||
}
|
||||
|
||||
if (_mainViewModel == null)
|
||||
{
|
||||
_mainViewModel = new MainViewModel(WorkspacesEditorIO);
|
||||
}
|
||||
|
||||
var parseResult = WorkspacesEditorIO.ParseWorkspaces(_mainViewModel);
|
||||
|
||||
// normal start of editor
|
||||
if (_mainWindow == null)
|
||||
{
|
||||
_mainWindow = new MainWindow(_mainViewModel);
|
||||
}
|
||||
|
||||
// reset main window owner to keep it on the top
|
||||
_mainWindow.ShowActivated = true;
|
||||
_mainWindow.Topmost = true;
|
||||
_mainWindow.Show();
|
||||
|
||||
// we can reset topmost flag after it's opened
|
||||
_mainWindow.Topmost = false;
|
||||
}
|
||||
|
||||
public static Theme GetCurrentTheme()
|
||||
{
|
||||
if (SystemParameters.HighContrast)
|
||||
{
|
||||
return Theme.HighContrastOne;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var useLightTheme = Registry.GetValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
||||
"AppsUseLightTheme",
|
||||
1);
|
||||
return (useLightTheme is int value && value == 0) ? Theme.Dark : Theme.Light;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Theme.Light;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExit(object sender, ExitEventArgs e)
|
||||
{
|
||||
if (_instanceMutex != null)
|
||||
{
|
||||
_instanceMutex.ReleaseMutex();
|
||||
}
|
||||
|
||||
Dispose();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
|
||||
{
|
||||
Logger.LogError("Unhandled exception occurred", args.ExceptionObject as Exception);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_instanceMutex?.Dispose();
|
||||
etwTrace?.Dispose();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +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.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WorkspacesEditor.Controls
|
||||
{
|
||||
public class ResetIsEnabled : ContentControl
|
||||
{
|
||||
static ResetIsEnabled()
|
||||
{
|
||||
IsEnabledProperty.OverrideMetadata(
|
||||
typeof(ResetIsEnabled),
|
||||
new UIPropertyMetadata(
|
||||
defaultValue: true,
|
||||
propertyChangedCallback: (_, __) => { },
|
||||
coerceValueCallback: (_, x) => x));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +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.Windows;
|
||||
using System.Windows.Automation.Peers;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
public class HeadingTextBlock : TextBlock
|
||||
{
|
||||
protected override AutomationPeer OnCreateAutomationPeer()
|
||||
{
|
||||
return new HeadingTextBlockAutomationPeer(this);
|
||||
}
|
||||
|
||||
internal sealed class HeadingTextBlockAutomationPeer : TextBlockAutomationPeer
|
||||
{
|
||||
public HeadingTextBlockAutomationPeer(HeadingTextBlock owner)
|
||||
: base(owner)
|
||||
{
|
||||
}
|
||||
|
||||
protected override AutomationControlType GetAutomationControlTypeCore()
|
||||
{
|
||||
return AutomationControlType.Header;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
<Page
|
||||
x:Class="WorkspacesEditor.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:WorkspacesEditor.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:WorkspacesEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
|
||||
Title="MainPage"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<converters:BooleanToInvertedVisibilityConverter x:Key="BooleanToInvertedVisibilityConverter" />
|
||||
<Thickness x:Key="ContentDialogPadding">24,16,0,24</Thickness>
|
||||
<Thickness x:Key="ContentDialogCommandSpaceMargin">0,24,24,0</Thickness>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- header + button -->
|
||||
<local:HeadingTextBlock
|
||||
x:Name="WorkspacesHeaderBlock"
|
||||
Grid.Row="0"
|
||||
Margin="24,0,48,16"
|
||||
AutomationProperties.HeadingLevel="Level1"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.Workspaces}" />
|
||||
|
||||
<Button
|
||||
x:Name="NewProjectButton"
|
||||
Margin="0,0,24,36"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
AutomationProperties.Name="{x:Static props:Resources.CreateWorkspace}"
|
||||
Click="NewProjectButton_Click"
|
||||
Style="{DynamicResource AccentButtonStyle}"
|
||||
TabIndex="3">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
AutomationProperties.Name="{x:Static props:Resources.CreateWorkspace}"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.CreateWorkspace}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- search + sort -->
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="24,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Grid>
|
||||
<TextBox
|
||||
x:Name="SearchTextBox"
|
||||
Width="320"
|
||||
Text="{Binding SearchTerm, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="{x:Static props:Resources.SearchExplanation}" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsHitTestVisible="False"
|
||||
Text="{x:Static props:Resources.Search}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Text, ElementName=SearchTextBox}" Value="">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Margin="-48,0,34,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Search}"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsHitTestVisible="False"
|
||||
Text="" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,0,24,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="12,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.SortBy}" />
|
||||
<ComboBox MinWidth="140" SelectedIndex="{Binding OrderByIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ComboBoxItem Content="{x:Static props:Resources.LastLaunched}" />
|
||||
<ComboBoxItem Content="{x:Static props:Resources.Created}" />
|
||||
<ComboBoxItem Content="{x:Static props:Resources.Name}" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- content -->
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding EmptyWorkspacesViewMessage, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextAlignment="Center"
|
||||
Visibility="{Binding IsWorkspacesViewEmpty, Mode=OneWay, Converter={StaticResource BoolToVis}, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<ScrollViewer
|
||||
Grid.Row="2"
|
||||
Margin="0,24,0,0"
|
||||
VerticalContentAlignment="Stretch"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Visibility="{Binding IsWorkspacesViewEmpty, Mode=OneWay, Converter={StaticResource BooleanToInvertedVisibilityConverter}, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ItemsControl
|
||||
x:Name="WorkspacesItemsControl"
|
||||
Margin="24,0,24,24"
|
||||
ItemsSource="{Binding WorkspacesView, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel
|
||||
HorizontalAlignment="Stretch"
|
||||
IsItemsHost="True"
|
||||
Orientation="Vertical" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="models:Project">
|
||||
<Button
|
||||
x:Name="EditButton"
|
||||
Margin="0,4,0,0"
|
||||
Padding="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Edit}"
|
||||
Click="EditButtonClicked">
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<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="{Binding Name, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<StackPanel Margin="0,0,0,8" Orientation="Horizontal">
|
||||
<Image Height="16" Source="{Binding PreviewIcons, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock
|
||||
Margin="8,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding AppsCountString}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding LastLaunched, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="12,12,12,12"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="0,0,8,0"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Launch}"
|
||||
Click="LaunchButton_Click"
|
||||
Content="{x:Static props:Resources.Launch}" />
|
||||
<StackPanel x:Name="WorkspaceActionGroup" Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="MoreButton"
|
||||
Padding="8"
|
||||
HorizontalAlignment="Right"
|
||||
Click="MoreButton_Click"
|
||||
Style="{DynamicResource SubtleButtonStyle}">
|
||||
<TextBlock
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
</Button>
|
||||
<Popup
|
||||
AllowsTransparency="True"
|
||||
Closed="PopupClosed"
|
||||
IsOpen="{Binding IsPopupVisible, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Placement="Left"
|
||||
PlacementTarget="{Binding ElementName=MoreButton}"
|
||||
StaysOpen="False">
|
||||
<Grid>
|
||||
<Border
|
||||
Background="{DynamicResource SolidBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Button
|
||||
Padding="8,8,24,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Edit}"
|
||||
Click="EditButtonClicked"
|
||||
Style="{DynamicResource SubtleButtonStyle}">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Edit}"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Edit}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.Edit}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Rectangle
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{DynamicResource DividerStrokeColorDefaultBrush}" />
|
||||
<Button
|
||||
Padding="8,8,24,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Delete}"
|
||||
Click="DeleteButtonClicked"
|
||||
Style="{DynamicResource SubtleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Delete}"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Delete}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.Delete}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,76 +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.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using ManagedCommon;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainPage.xaml
|
||||
/// </summary>
|
||||
public partial class MainPage : Page
|
||||
{
|
||||
private MainViewModel _mainViewModel;
|
||||
|
||||
public MainPage(MainViewModel mainViewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
_mainViewModel = mainViewModel;
|
||||
this.DataContext = _mainViewModel;
|
||||
}
|
||||
|
||||
private /*async*/ void NewProjectButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.EnterSnapshotMode(false);
|
||||
}
|
||||
|
||||
private void EditButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.CloseAllPopups();
|
||||
Button button = sender as Button;
|
||||
Project selectedProject = button.DataContext as Project;
|
||||
_mainViewModel.EditProject(selectedProject);
|
||||
}
|
||||
|
||||
private void DeleteButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
Button button = sender as Button;
|
||||
Project selectedProject = button.DataContext as Project;
|
||||
selectedProject.IsPopupVisible = false;
|
||||
|
||||
_mainViewModel.DeleteProject(selectedProject);
|
||||
}
|
||||
|
||||
private void MoreButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.CloseAllPopups();
|
||||
e.Handled = true;
|
||||
Button button = sender as Button;
|
||||
Project project = button.DataContext as Project;
|
||||
project.IsPopupVisible = true;
|
||||
}
|
||||
|
||||
private void PopupClosed(object sender, object e)
|
||||
{
|
||||
if (sender is Popup p && p.DataContext is Project proj)
|
||||
{
|
||||
proj.IsPopupVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
Button button = sender as Button;
|
||||
Project project = button.DataContext as Project;
|
||||
_mainViewModel.LaunchProject(project);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<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"
|
||||
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
|
||||
x:Name="WorkspacesMainWindow"
|
||||
Title="{x:Static props:Resources.MainTitle}"
|
||||
MinWidth="750"
|
||||
MinHeight="680"
|
||||
AutomationProperties.Name="Workspaces Editor"
|
||||
Closing="OnClosing"
|
||||
ContentRendered="OnContentRendered"
|
||||
ResizeMode="CanResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="0,16,0,0">
|
||||
<Frame x:Name="ContentFrame" NavigationUIVisibility="Hidden" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,179 +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.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using WorkspacesEditor.Telemetry;
|
||||
using WorkspacesEditor.Utils;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window, IDisposable
|
||||
{
|
||||
public MainViewModel MainViewModel { get; set; }
|
||||
|
||||
private CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
|
||||
private static MainPage _mainPage;
|
||||
|
||||
public MainWindow(MainViewModel mainViewModel)
|
||||
{
|
||||
MainViewModel = mainViewModel;
|
||||
mainViewModel.SetMainWindow(this);
|
||||
|
||||
if (Properties.Settings.Default.Height == -1 || !IsEditorInsideVisibleArea())
|
||||
{
|
||||
// This is the very first time the window is created or it would be placed outside the visible area (monitor rearrangement). Place it on the screen center
|
||||
WindowInteropHelper windowInteropHelper = new WindowInteropHelper(this);
|
||||
System.Windows.Forms.Screen screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle);
|
||||
double dpi = MonitorHelper.GetScreenDpiFromScreen(screen);
|
||||
this.Height = screen.WorkingArea.Height / dpi * 0.90;
|
||||
this.Width = screen.WorkingArea.Width / dpi * 0.75;
|
||||
this.Top = screen.WorkingArea.Top + (int)(screen.WorkingArea.Height / dpi * 0.05);
|
||||
this.Left = screen.WorkingArea.Left + (int)(screen.WorkingArea.Width / dpi * 0.125);
|
||||
SavePosition();
|
||||
}
|
||||
|
||||
this.Top = Properties.Settings.Default.Top;
|
||||
this.Left = Properties.Settings.Default.Left;
|
||||
this.Height = Properties.Settings.Default.Height;
|
||||
this.Width = Properties.Settings.Default.Width;
|
||||
|
||||
if (Properties.Settings.Default.Maximized)
|
||||
{
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
_mainPage = new MainPage(mainViewModel);
|
||||
|
||||
ContentFrame.Navigate(_mainPage);
|
||||
|
||||
MaxWidth = SystemParameters.PrimaryScreenWidth;
|
||||
MaxHeight = SystemParameters.PrimaryScreenHeight;
|
||||
|
||||
Common.UI.NativeEventWaiter.WaitForEventLoop(
|
||||
PowerToys.Interop.Constants.WorkspacesHotkeyEvent(),
|
||||
() =>
|
||||
{
|
||||
if (ApplicationIsInFocus())
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (WindowState == WindowState.Minimized)
|
||||
{
|
||||
WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
// Get the window handle of the Workspaces Editor window
|
||||
IntPtr handle = new WindowInteropHelper(this).Handle;
|
||||
WindowHelpers.BringToForeground(handle);
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
},
|
||||
Application.Current.Dispatcher,
|
||||
cancellationToken.Token);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new WorkspacesEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
|
||||
}
|
||||
|
||||
private bool IsEditorInsideVisibleArea()
|
||||
{
|
||||
System.Windows.Forms.Screen[] allScreens = MonitorHelper.GetDpiUnawareScreens();
|
||||
Rectangle commonBounds = allScreens[0].Bounds;
|
||||
for (int screenIndex = 1; screenIndex < allScreens.Length; screenIndex++)
|
||||
{
|
||||
Rectangle rectangle = allScreens[screenIndex].Bounds;
|
||||
commonBounds = Rectangle.Union(rectangle, commonBounds);
|
||||
}
|
||||
|
||||
Rectangle editorBounds = new Rectangle((int)Properties.Settings.Default.Left, (int)Properties.Settings.Default.Top, (int)Properties.Settings.Default.Width, (int)Properties.Settings.Default.Height);
|
||||
return editorBounds.IntersectsWith(commonBounds);
|
||||
}
|
||||
|
||||
private void SavePosition()
|
||||
{
|
||||
if (WindowState == WindowState.Maximized)
|
||||
{
|
||||
// Use the RestoreBounds as the current values will be 0, 0 and the size of the screen
|
||||
Properties.Settings.Default.Top = RestoreBounds.Top;
|
||||
Properties.Settings.Default.Left = RestoreBounds.Left;
|
||||
Properties.Settings.Default.Height = RestoreBounds.Height;
|
||||
Properties.Settings.Default.Width = RestoreBounds.Width;
|
||||
Properties.Settings.Default.Maximized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Properties.Settings.Default.Top = this.Top;
|
||||
Properties.Settings.Default.Left = this.Left;
|
||||
Properties.Settings.Default.Height = this.Height;
|
||||
Properties.Settings.Default.Width = this.Width;
|
||||
Properties.Settings.Default.Maximized = false;
|
||||
}
|
||||
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
|
||||
private void OnClosing(object sender, EventArgs e)
|
||||
{
|
||||
SavePosition();
|
||||
cancellationToken.Dispose();
|
||||
App.Current.Shutdown();
|
||||
}
|
||||
|
||||
// This is required to fix a WPF rendering bug when using custom chrome
|
||||
private void OnContentRendered(object sender, EventArgs e)
|
||||
{
|
||||
// Get the window handle of the Workspaces Editor window
|
||||
IntPtr handle = new WindowInteropHelper(this).Handle;
|
||||
WindowHelpers.BringToForeground(handle);
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
public void ShowPage(ProjectEditor editPage)
|
||||
{
|
||||
ContentFrame.Navigate(editPage);
|
||||
}
|
||||
|
||||
public void SwitchToMainView()
|
||||
{
|
||||
ContentFrame.GoBack();
|
||||
}
|
||||
|
||||
public static bool ApplicationIsInFocus()
|
||||
{
|
||||
var activatedHandle = NativeMethods.GetForegroundWindow();
|
||||
if (activatedHandle == IntPtr.Zero)
|
||||
{
|
||||
return false; // No window is currently activated
|
||||
}
|
||||
|
||||
var procId = Environment.ProcessId;
|
||||
int activeProcId;
|
||||
_ = NativeMethods.GetWindowThreadProcessId(activatedHandle, out activeProcId);
|
||||
|
||||
return activeProcId == procId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +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.
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
public sealed class AppListDataTemplateSelector : System.Windows.Controls.DataTemplateSelector
|
||||
{
|
||||
public System.Windows.DataTemplate HeaderTemplate { get; set; }
|
||||
|
||||
public System.Windows.DataTemplate AppTemplate { get; set; }
|
||||
|
||||
public AppListDataTemplateSelector()
|
||||
{
|
||||
HeaderTemplate = new System.Windows.DataTemplate();
|
||||
AppTemplate = new System.Windows.DataTemplate();
|
||||
}
|
||||
|
||||
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
|
||||
{
|
||||
return item is MonitorHeaderRow ? HeaderTemplate : AppTemplate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,411 +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.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesEditor.Utils;
|
||||
|
||||
namespace WorkspacesEditor.Models
|
||||
{
|
||||
public class Project : INotifyPropertyChanged
|
||||
{
|
||||
[JsonIgnore]
|
||||
public string EditorWindowTitle { get; set; }
|
||||
|
||||
public string Id { get; private set; }
|
||||
|
||||
private string _name;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
|
||||
set
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Name)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(CanBeSaved)));
|
||||
}
|
||||
}
|
||||
|
||||
public long CreationTime { get; } // in seconds
|
||||
|
||||
public long LastLaunchedTime { get; } // in seconds
|
||||
|
||||
public bool IsShortcutNeeded { get; set; }
|
||||
|
||||
public bool MoveExistingWindows { get; set; }
|
||||
|
||||
public string LastLaunched
|
||||
{
|
||||
get
|
||||
{
|
||||
string lastLaunched = WorkspacesEditor.Properties.Resources.LastLaunched + ": ";
|
||||
if (LastLaunchedTime == 0)
|
||||
{
|
||||
return lastLaunched + WorkspacesEditor.Properties.Resources.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 + WorkspacesEditor.Properties.Resources.Recently;
|
||||
}
|
||||
|
||||
if (delta < 2 * MINUTE)
|
||||
{
|
||||
return lastLaunched + WorkspacesEditor.Properties.Resources.OneMinuteAgo;
|
||||
}
|
||||
|
||||
if (delta < 45 * MINUTE)
|
||||
{
|
||||
return lastLaunched + ts.Minutes + " " + WorkspacesEditor.Properties.Resources.MinutesAgo;
|
||||
}
|
||||
|
||||
if (delta < 90 * MINUTE)
|
||||
{
|
||||
return lastLaunched + WorkspacesEditor.Properties.Resources.OneHourAgo;
|
||||
}
|
||||
|
||||
if (delta < 24 * HOUR)
|
||||
{
|
||||
return lastLaunched + ts.Hours + " " + WorkspacesEditor.Properties.Resources.HoursAgo;
|
||||
}
|
||||
|
||||
if (delta < 48 * HOUR)
|
||||
{
|
||||
return lastLaunched + WorkspacesEditor.Properties.Resources.Yesterday;
|
||||
}
|
||||
|
||||
if (delta < 30 * DAY)
|
||||
{
|
||||
return lastLaunched + ts.Days + " " + WorkspacesEditor.Properties.Resources.DaysAgo;
|
||||
}
|
||||
|
||||
if (delta < 12 * MONTH)
|
||||
{
|
||||
int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
|
||||
return lastLaunched + (months <= 1 ? WorkspacesEditor.Properties.Resources.OneMonthAgo : months + " " + WorkspacesEditor.Properties.Resources.MonthsAgo);
|
||||
}
|
||||
else
|
||||
{
|
||||
int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
|
||||
return lastLaunched + (years <= 1 ? WorkspacesEditor.Properties.Resources.OneYearAgo : years + " " + WorkspacesEditor.Properties.Resources.YearsAgo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanBeSaved => Name.Length > 0 && Applications.Count > 0;
|
||||
|
||||
private bool _isRevertEnabled;
|
||||
|
||||
public bool IsRevertEnabled
|
||||
{
|
||||
get => _isRevertEnabled;
|
||||
set
|
||||
{
|
||||
if (_isRevertEnabled != value)
|
||||
{
|
||||
_isRevertEnabled = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsRevertEnabled)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isPopupVisible;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsPopupVisible
|
||||
{
|
||||
get => _isPopupVisible;
|
||||
|
||||
set
|
||||
{
|
||||
_isPopupVisible = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(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.Left).ThenBy(x => x.Key.MonitorDpiUnawareBounds.Top))
|
||||
{
|
||||
MonitorHeaderRow headerRow = new() { MonitorName = "Screen " + appItem.Key.MonitorNumber, SelectString = Properties.Resources.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 = Properties.Resources.Minimized_Apps, SelectString = Properties.Resources.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 ? Properties.Resources.App : Properties.Resources.Apps);
|
||||
}
|
||||
}
|
||||
|
||||
public List<MonitorSetup> Monitors { get; }
|
||||
|
||||
public bool IsPositionChangedManually { get; set; } // telemetry
|
||||
|
||||
private BitmapImage _previewIcons;
|
||||
private BitmapImage _previewImage;
|
||||
private double _previewImageWidth;
|
||||
|
||||
public Project(Project selectedProject)
|
||||
{
|
||||
Id = selectedProject.Id;
|
||||
Name = selectedProject.Name;
|
||||
PreviewIcons = selectedProject.PreviewIcons;
|
||||
PreviewImage = selectedProject.PreviewImage;
|
||||
IsShortcutNeeded = selectedProject.IsShortcutNeeded;
|
||||
MoveExistingWindows = selectedProject.MoveExistingWindows;
|
||||
|
||||
int screenIndex = 1;
|
||||
|
||||
Monitors = [];
|
||||
foreach (MonitorSetup item in selectedProject.Monitors.OrderBy(x => x.MonitorDpiAwareBounds.Left).ThenBy(x => x.MonitorDpiAwareBounds.Top))
|
||||
{
|
||||
Monitors.Add(item);
|
||||
screenIndex++;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Models.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 Models.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)
|
||||
{
|
||||
System.Windows.Rect dpiAware = new(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
|
||||
System.Windows.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 BitmapImage PreviewIcons
|
||||
{
|
||||
get => _previewIcons;
|
||||
|
||||
set
|
||||
{
|
||||
_previewIcons = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewIcons)));
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapImage PreviewImage
|
||||
{
|
||||
get => _previewImage;
|
||||
|
||||
set
|
||||
{
|
||||
_previewImage = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImage)));
|
||||
}
|
||||
}
|
||||
|
||||
public double PreviewImageWidth
|
||||
{
|
||||
get => _previewImageWidth;
|
||||
|
||||
set
|
||||
{
|
||||
_previewImageWidth = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImageWidth)));
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public async void Initialize(Theme currentTheme)
|
||||
{
|
||||
PreviewIcons = await Task.Run(() => DrawHelper.DrawPreviewIcons(this));
|
||||
Rectangle commonBounds = GetCommonBounds();
|
||||
PreviewImage = await Task.Run(() => DrawHelper.DrawPreview(this, commonBounds, currentTheme));
|
||||
PreviewImageWidth = commonBounds.Width / (commonBounds.Height * 1.2 / 200);
|
||||
}
|
||||
|
||||
private Rectangle GetCommonBounds()
|
||||
{
|
||||
double minX = Monitors.First().MonitorDpiAwareBounds.Left;
|
||||
double minY = Monitors.First().MonitorDpiAwareBounds.Top;
|
||||
double maxX = Monitors.First().MonitorDpiAwareBounds.Right;
|
||||
double maxY = Monitors.First().MonitorDpiAwareBounds.Bottom;
|
||||
for (int monitorIndex = 1; monitorIndex < Monitors.Count; monitorIndex++)
|
||||
{
|
||||
Monitor monitor = Monitors[monitorIndex];
|
||||
minX = Math.Min(minX, monitor.MonitorDpiAwareBounds.Left);
|
||||
minY = Math.Min(minY, monitor.MonitorDpiAwareBounds.Top);
|
||||
maxX = Math.Max(maxX, monitor.MonitorDpiAwareBounds.Right);
|
||||
maxY = Math.Max(maxY, monitor.MonitorDpiAwareBounds.Bottom);
|
||||
}
|
||||
|
||||
return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
|
||||
}
|
||||
|
||||
public void UpdateAfterLaunchAndEdit(Project projectBeforeLaunch)
|
||||
{
|
||||
Id = projectBeforeLaunch.Id;
|
||||
Name = projectBeforeLaunch.Name;
|
||||
IsRevertEnabled = true;
|
||||
MoveExistingWindows = projectBeforeLaunch.MoveExistingWindows;
|
||||
foreach (Application app in Applications)
|
||||
{
|
||||
var sameAppBefore = projectBeforeLaunch.Applications.Where(x => x.Id.Equals(app.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (sameAppBefore.Any())
|
||||
{
|
||||
var appBefore = sameAppBefore.FirstOrDefault();
|
||||
app.CommandLineArguments = appBefore.CommandLineArguments;
|
||||
app.IsElevated = appBefore.IsElevated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void CloseExpanders()
|
||||
{
|
||||
foreach (Application app in Applications)
|
||||
{
|
||||
app.IsExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal MonitorSetup GetMonitorForApp(Application app)
|
||||
{
|
||||
MonitorSetup monitorSetup = Monitors.Where(x => x.MonitorNumber == app.MonitorNumber).FirstOrDefault();
|
||||
if (monitorSetup == null)
|
||||
{
|
||||
// monitors changed: try to determine monitor id based on middle point
|
||||
int middleX = app.Position.X + (app.Position.Width / 2);
|
||||
int middleY = app.Position.Y + (app.Position.Height / 2);
|
||||
MonitorSetup monitorCandidate = Monitors.Where(x =>
|
||||
(x.MonitorDpiUnawareBounds.Left < middleX) &&
|
||||
(x.MonitorDpiUnawareBounds.Right > middleX) &&
|
||||
(x.MonitorDpiUnawareBounds.Top < middleY) &&
|
||||
(x.MonitorDpiUnawareBounds.Bottom > middleY)).FirstOrDefault();
|
||||
if (monitorCandidate != null)
|
||||
{
|
||||
app.MonitorNumber = monitorCandidate.MonitorNumber;
|
||||
return monitorCandidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// monitors and even the app's area unknown, set the main monitor (which is closer to (0,0)) as the app's monitor
|
||||
monitorCandidate = Monitors.OrderBy(x => Math.Abs(x.MonitorDpiUnawareBounds.Left) + Math.Abs(x.MonitorDpiUnawareBounds.Top)).FirstOrDefault();
|
||||
if (monitorCandidate != null)
|
||||
{
|
||||
app.MonitorNumber = monitorCandidate.MonitorNumber;
|
||||
return monitorCandidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// no monitors defined at all.
|
||||
Logger.LogError($"Wrong workspace setup. No monitors defined for the workspace: {Name}.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return monitorSetup;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<Window
|
||||
x:Class="WorkspacesEditor.OverlayWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:WorkspacesEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
ShowActivated="False"
|
||||
ShowInTaskbar="False"
|
||||
WindowStyle="None"
|
||||
mc:Ignorable="d">
|
||||
<Border
|
||||
Background="Transparent"
|
||||
BorderBrush="Red"
|
||||
BorderThickness="4" />
|
||||
</Window>
|
||||
@@ -1,53 +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.Windows;
|
||||
|
||||
using WorkspacesEditor.Utils;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for OverlayWindow.xaml
|
||||
/// </summary>
|
||||
public partial class OverlayWindow : Window
|
||||
{
|
||||
private int _targetX;
|
||||
private int _targetY;
|
||||
private int _targetWidth;
|
||||
private int _targetHeight;
|
||||
|
||||
public OverlayWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += OnWindowSourceInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target bounds for the overlay window.
|
||||
/// The window will be positioned using DPI-unaware context after initialization.
|
||||
/// </summary>
|
||||
public void SetTargetBounds(int x, int y, int width, int height)
|
||||
{
|
||||
_targetX = x;
|
||||
_targetY = y;
|
||||
_targetWidth = width;
|
||||
_targetHeight = height;
|
||||
|
||||
// Set initial WPF properties (will be corrected after HWND creation)
|
||||
Left = x;
|
||||
Top = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
private void OnWindowSourceInitialized(object sender, EventArgs e)
|
||||
{
|
||||
// Reposition window using DPI-unaware context to match the virtual coordinates.
|
||||
// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,702 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace WorkspacesEditor.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WorkspacesEditor.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Add back.
|
||||
/// </summary>
|
||||
public static string AddBack {
|
||||
get {
|
||||
return ResourceManager.GetString("AddBack", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Admin.
|
||||
/// </summary>
|
||||
public static string Admin {
|
||||
get {
|
||||
return ResourceManager.GetString("Admin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to app.
|
||||
/// </summary>
|
||||
public static string App {
|
||||
get {
|
||||
return ResourceManager.GetString("App", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to App name.
|
||||
/// </summary>
|
||||
public static string App_name {
|
||||
get {
|
||||
return ResourceManager.GetString("App_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to apps.
|
||||
/// </summary>
|
||||
public static string Apps {
|
||||
get {
|
||||
return ResourceManager.GetString("Apps", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Are you sure?.
|
||||
/// </summary>
|
||||
public static string Are_You_Sure {
|
||||
get {
|
||||
return ResourceManager.GetString("Are_You_Sure", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Are you sure you want to delete this Workspace?.
|
||||
/// </summary>
|
||||
public static string Are_You_Sure_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Are_You_Sure_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Args.
|
||||
/// </summary>
|
||||
public static string Args {
|
||||
get {
|
||||
return ResourceManager.GetString("Args", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cancel.
|
||||
/// </summary>
|
||||
public static string Cancel {
|
||||
get {
|
||||
return ResourceManager.GetString("Cancel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to CLI arguments.
|
||||
/// </summary>
|
||||
public static string CliArguments {
|
||||
get {
|
||||
return ResourceManager.GetString("CliArguments", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Created.
|
||||
/// </summary>
|
||||
public static string Created {
|
||||
get {
|
||||
return ResourceManager.GetString("Created", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create desktop shortcut.
|
||||
/// </summary>
|
||||
public static string CreateShortcut {
|
||||
get {
|
||||
return ResourceManager.GetString("CreateShortcut", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create Workspace.
|
||||
/// </summary>
|
||||
public static string CreateWorkspace {
|
||||
get {
|
||||
return ResourceManager.GetString("CreateWorkspace", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Custom.
|
||||
/// </summary>
|
||||
public static string Custom {
|
||||
get {
|
||||
return ResourceManager.GetString("Custom", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to days ago.
|
||||
/// </summary>
|
||||
public static string DaysAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("DaysAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Workspace.
|
||||
/// </summary>
|
||||
public static string DefaultWorkspaceNamePrefix {
|
||||
get {
|
||||
return ResourceManager.GetString("DefaultWorkspaceNamePrefix", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Remove.
|
||||
/// </summary>
|
||||
public static string Delete {
|
||||
get {
|
||||
return ResourceManager.GetString("Delete", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Delete Workspace dialog..
|
||||
/// </summary>
|
||||
public static string Delete_Workspace_Dialog_Announce {
|
||||
get {
|
||||
return ResourceManager.GetString("Delete_Workspace_Dialog_Announce", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Remove selected apps.
|
||||
/// </summary>
|
||||
public static string DeleteSelected {
|
||||
get {
|
||||
return ResourceManager.GetString("DeleteSelected", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit.
|
||||
/// </summary>
|
||||
public static string Edit {
|
||||
get {
|
||||
return ResourceManager.GetString("Edit", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to opened.
|
||||
/// </summary>
|
||||
public static string Edit_Project_Open_Announce {
|
||||
get {
|
||||
return ResourceManager.GetString("Edit_Project_Open_Announce", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit Workspace.
|
||||
/// </summary>
|
||||
public static string EditWorkspace {
|
||||
get {
|
||||
return ResourceManager.GetString("EditWorkspace", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error parsing Workspaces data..
|
||||
/// </summary>
|
||||
public static string Error_Parsing_Message {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_Parsing_Message", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Height.
|
||||
/// </summary>
|
||||
public static string Height {
|
||||
get {
|
||||
return ResourceManager.GetString("Height", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to hours ago.
|
||||
/// </summary>
|
||||
public static string HoursAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("HoursAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Last launched.
|
||||
/// </summary>
|
||||
public static string LastLaunched {
|
||||
get {
|
||||
return ResourceManager.GetString("LastLaunched", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Launch.
|
||||
/// </summary>
|
||||
public static string Launch {
|
||||
get {
|
||||
return ResourceManager.GetString("Launch", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Launch args.
|
||||
/// </summary>
|
||||
public static string Launch_args {
|
||||
get {
|
||||
return ResourceManager.GetString("Launch_args", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Launch as Admin.
|
||||
/// </summary>
|
||||
public static string LaunchAsAdmin {
|
||||
get {
|
||||
return ResourceManager.GetString("LaunchAsAdmin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Launch & edit.
|
||||
/// </summary>
|
||||
public static string LaunchEdit {
|
||||
get {
|
||||
return ResourceManager.GetString("LaunchEdit", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Left.
|
||||
/// </summary>
|
||||
public static string Left {
|
||||
get {
|
||||
return ResourceManager.GetString("Left", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Workspaces Editor.
|
||||
/// </summary>
|
||||
public static string MainTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("MainTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Maximized.
|
||||
/// </summary>
|
||||
public static string Maximized {
|
||||
get {
|
||||
return ResourceManager.GetString("Maximized", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimized.
|
||||
/// </summary>
|
||||
public static string Minimized {
|
||||
get {
|
||||
return ResourceManager.GetString("Minimized", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimized apps.
|
||||
/// </summary>
|
||||
public static string Minimized_Apps {
|
||||
get {
|
||||
return ResourceManager.GetString("Minimized_Apps", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to minutes ago.
|
||||
/// </summary>
|
||||
public static string MinutesAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("MinutesAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to months ago.
|
||||
/// </summary>
|
||||
public static string MonthsAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("MonthsAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Move existing windows.
|
||||
/// </summary>
|
||||
public static string MoveIfExist {
|
||||
get {
|
||||
return ResourceManager.GetString("MoveIfExist", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Name.
|
||||
/// </summary>
|
||||
public static string Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to never.
|
||||
/// </summary>
|
||||
public static string Never {
|
||||
get {
|
||||
return ResourceManager.GetString("Never", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to New Workspace.
|
||||
/// </summary>
|
||||
public static string New_Workspace {
|
||||
get {
|
||||
return ResourceManager.GetString("New_Workspace", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to There are no saved Workspaces..
|
||||
/// </summary>
|
||||
public static string No_Workspaces_Message {
|
||||
get {
|
||||
return ResourceManager.GetString("No_Workspaces_Message", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The application cannot be found.
|
||||
/// </summary>
|
||||
public static string NotFoundTooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("NotFoundTooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No Workspaces match the current search..
|
||||
/// </summary>
|
||||
public static string NoWorkspacesMatch {
|
||||
get {
|
||||
return ResourceManager.GetString("NoWorkspacesMatch", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to an hour ago.
|
||||
/// </summary>
|
||||
public static string OneHourAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("OneHourAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to a minute ago.
|
||||
/// </summary>
|
||||
public static string OneMinuteAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("OneMinuteAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to one month ago.
|
||||
/// </summary>
|
||||
public static string OneMonthAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("OneMonthAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to one second ago.
|
||||
/// </summary>
|
||||
public static string OneSecondAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("OneSecondAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to one year ago.
|
||||
/// </summary>
|
||||
public static string OneYearAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("OneYearAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pin Workspaces to taskbar.
|
||||
/// </summary>
|
||||
public static string PinToTaskbar {
|
||||
get {
|
||||
return ResourceManager.GetString("PinToTaskbar", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to recently.
|
||||
/// </summary>
|
||||
public static string Recently {
|
||||
get {
|
||||
return ResourceManager.GetString("Recently", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Revert.
|
||||
/// </summary>
|
||||
public static string Revert {
|
||||
get {
|
||||
return ResourceManager.GetString("Revert", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save.
|
||||
/// </summary>
|
||||
public static string Save_Workspace {
|
||||
get {
|
||||
return ResourceManager.GetString("Save_Workspace", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Search.
|
||||
/// </summary>
|
||||
public static string Search {
|
||||
get {
|
||||
return ResourceManager.GetString("Search", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Search for Workspaces or apps.
|
||||
/// </summary>
|
||||
public static string SearchExplanation {
|
||||
get {
|
||||
return ResourceManager.GetString("SearchExplanation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to seconds ago.
|
||||
/// </summary>
|
||||
public static string SecondsAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("SecondsAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Select all apps on.
|
||||
/// </summary>
|
||||
public static string SelectAllAppsOnMonitor {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectAllAppsOnMonitor", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Select all minimized apps.
|
||||
/// </summary>
|
||||
public static string SelectAllMinimizedApps {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectAllMinimizedApps", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Select all apps in Workspace.
|
||||
/// </summary>
|
||||
public static string SelectedAllInWorkspace {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectedAllInWorkspace", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit your layout and click "Capture" when finished..
|
||||
/// </summary>
|
||||
public static string SnapshotDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("SnapshotDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Snapshot Creator.
|
||||
/// </summary>
|
||||
public static string SnapshotWindowTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("SnapshotWindowTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Sort by.
|
||||
/// </summary>
|
||||
public static string SortBy {
|
||||
get {
|
||||
return ResourceManager.GetString("SortBy", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Capture.
|
||||
/// </summary>
|
||||
public static string Take_Snapshot {
|
||||
get {
|
||||
return ResourceManager.GetString("Take_Snapshot", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Top.
|
||||
/// </summary>
|
||||
public static string Top {
|
||||
get {
|
||||
return ResourceManager.GetString("Top", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Width.
|
||||
/// </summary>
|
||||
public static string Width {
|
||||
get {
|
||||
return ResourceManager.GetString("Width", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Window position.
|
||||
/// </summary>
|
||||
public static string WindowPosition {
|
||||
get {
|
||||
return ResourceManager.GetString("WindowPosition", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Workspace name.
|
||||
/// </summary>
|
||||
public static string WorkspaceName {
|
||||
get {
|
||||
return ResourceManager.GetString("WorkspaceName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Workspaces.
|
||||
/// </summary>
|
||||
public static string Workspaces {
|
||||
get {
|
||||
return ResourceManager.GetString("Workspaces", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Write arguments here.
|
||||
/// </summary>
|
||||
public static string WriteArgs {
|
||||
get {
|
||||
return ResourceManager.GetString("WriteArgs", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to years ago.
|
||||
/// </summary>
|
||||
public static string YearsAgo {
|
||||
get {
|
||||
return ResourceManager.GetString("YearsAgo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to yesterday.
|
||||
/// </summary>
|
||||
public static string Yesterday {
|
||||
get {
|
||||
return ResourceManager.GetString("Yesterday", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace WorkspacesEditor.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("-1")]
|
||||
public double Top {
|
||||
get {
|
||||
return ((double)(this["Top"]));
|
||||
}
|
||||
set {
|
||||
this["Top"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("-1")]
|
||||
public double Left {
|
||||
get {
|
||||
return ((double)(this["Left"]));
|
||||
}
|
||||
set {
|
||||
this["Left"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("-1")]
|
||||
public double Height {
|
||||
get {
|
||||
return ((double)(this["Height"]));
|
||||
}
|
||||
set {
|
||||
this["Height"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("-1")]
|
||||
public double Width {
|
||||
get {
|
||||
return ((double)(this["Width"]));
|
||||
}
|
||||
set {
|
||||
this["Width"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("False")]
|
||||
public bool Maximized {
|
||||
get {
|
||||
return ((bool)(this["Maximized"]));
|
||||
}
|
||||
set {
|
||||
this["Maximized"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="WorkspacesEditor.Properties" GeneratedClassName="Settings">
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="Top" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">-1</Value>
|
||||
</Setting>
|
||||
<Setting Name="Left" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">-1</Value>
|
||||
</Setting>
|
||||
<Setting Name="Height" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">-1</Value>
|
||||
</Setting>
|
||||
<Setting Name="Width" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">-1</Value>
|
||||
</Setting>
|
||||
<Setting Name="Maximized" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">False</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
@@ -1,52 +0,0 @@
|
||||
<Window
|
||||
x:Class="WorkspacesEditor.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:local="clr-namespace:WorkspacesEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
|
||||
Title="{x:Static props:Resources.SnapshotWindowTitle}"
|
||||
Width="420"
|
||||
Closing="Window_Closing"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="8,16,8,16"
|
||||
Text="{x:Static props:Resources.SnapshotDescription}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="SnapshotButton"
|
||||
Grid.Row="1"
|
||||
Margin="8,8,4,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Take_Snapshot}"
|
||||
Click="SnapshotButtonClicked"
|
||||
Content="{x:Static props:Resources.Take_Snapshot}"
|
||||
Style="{DynamicResource AccentButtonStyle}" />
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="4,8,8,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
|
||||
Click="CancelButtonClicked"
|
||||
Content="{x:Static props:Resources.Cancel}" />
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,41 +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.Windows;
|
||||
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SnapshotWindow.xaml
|
||||
/// </summary>
|
||||
public partial class SnapshotWindow : Window
|
||||
{
|
||||
private MainViewModel _mainViewModel;
|
||||
|
||||
public SnapshotWindow(MainViewModel mainViewModel)
|
||||
{
|
||||
_mainViewModel = mainViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void CancelButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
_mainViewModel.CancelSnapshot();
|
||||
}
|
||||
|
||||
private void SnapshotButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
_mainViewModel.SnapWorkspace();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
_mainViewModel.CancelSnapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +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 System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public class MonitorHelper
|
||||
{
|
||||
private const int DpiAwarenessContextUnaware = -1;
|
||||
|
||||
private Screen[] screens;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||
|
||||
private void SaveDpiUnawareScreens()
|
||||
{
|
||||
SetThreadDpiAwarenessContext(DpiAwarenessContextUnaware);
|
||||
screens = Screen.AllScreens;
|
||||
}
|
||||
|
||||
private Screen[] GetDpiUnawareScreenBounds()
|
||||
{
|
||||
Thread dpiUnawareThread = new(new ThreadStart(SaveDpiUnawareScreens));
|
||||
dpiUnawareThread.Start();
|
||||
dpiUnawareThread.Join();
|
||||
|
||||
return screens;
|
||||
}
|
||||
|
||||
public static Screen[] GetDpiUnawareScreens()
|
||||
{
|
||||
MonitorHelper monitorHelper = new();
|
||||
return monitorHelper.GetDpiUnawareScreenBounds();
|
||||
}
|
||||
|
||||
internal static double GetScreenDpiFromScreen(Screen screen)
|
||||
{
|
||||
System.Drawing.Point point = new(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
|
||||
nint monitor = NativeMethods.MonitorFromPoint(point, NativeMethods._MONITOR_DEFAULTTONEAREST);
|
||||
_ = NativeMethods.GetDpiForMonitor(monitor, NativeMethods.DpiType.EFFECTIVE, out uint dpiX, out _);
|
||||
return dpiX / 96.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +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 System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
internal sealed class NativeMethods
|
||||
{
|
||||
public const int SW_RESTORE = 9;
|
||||
public const int SW_NORMAL = 1;
|
||||
public const int SW_MINIMIZE = 6;
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
|
||||
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates.
|
||||
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
/// </summary>
|
||||
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
|
||||
{
|
||||
var helper = new WindowInteropHelper(window).Handle;
|
||||
if (helper != IntPtr.Zero)
|
||||
{
|
||||
// Temporarily switch to DPI-unaware context to position window.
|
||||
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
|
||||
try
|
||||
{
|
||||
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetThreadDpiAwarenessContext(oldContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("USER32.DLL")]
|
||||
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern uint GetCurrentThreadId();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
||||
|
||||
public enum DpiType
|
||||
{
|
||||
EFFECTIVE = 0,
|
||||
ANGULAR = 1,
|
||||
RAW = 2,
|
||||
}
|
||||
|
||||
[DllImport("User32.dll")]
|
||||
public static extern IntPtr MonitorFromPoint([In] System.Drawing.Point pt, [In] uint dwFlags);
|
||||
|
||||
[DllImport("Shcore.dll")]
|
||||
public static extern IntPtr GetDpiForMonitor([In] IntPtr hmonitor, [In] DpiType dpiType, [Out] out uint dpiX, [Out] out uint dpiY);
|
||||
|
||||
public const int _S_OK = 0;
|
||||
public const int _MONITOR_DEFAULTTONEAREST = 2;
|
||||
public const int _E_INVALIDARG = -2147024809;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +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.
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public readonly struct ParsingResult(bool result, string message = "", string data = "")
|
||||
{
|
||||
public bool Result { get; } = result;
|
||||
|
||||
public string Message { get; } = message;
|
||||
|
||||
public string MalformedData { get; } = data;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +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.Linq;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public static class StringUtils
|
||||
{
|
||||
public static string UpperCamelCaseToDashCase(this string str)
|
||||
{
|
||||
// If it's a single letter variable, leave it as it is
|
||||
return str.Length == 1
|
||||
? str
|
||||
: string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString())).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,154 +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.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using ManagedCommon;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
public class WorkspacesIcon : IDisposable
|
||||
{
|
||||
private const int IconSize = 128;
|
||||
|
||||
public static readonly Brush LightThemeIconBackground = new SolidBrush(Color.FromArgb(255, 239, 243, 251));
|
||||
public static readonly Brush LightThemeIconForeground = new SolidBrush(Color.FromArgb(255, 47, 50, 56));
|
||||
public static readonly Brush DarkThemeIconBackground = new SolidBrush(Color.FromArgb(255, 55, 55, 55));
|
||||
public static readonly Brush DarkThemeIconForeground = new SolidBrush(Color.FromArgb(255, 228, 228, 228));
|
||||
|
||||
public 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(System.Globalization.CultureInfo.CurrentCulture).ToCharArray()[0];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Bitmap DrawIcon(string text, Theme currentTheme)
|
||||
{
|
||||
Brush background = currentTheme == Theme.Dark ? DarkThemeIconBackground : LightThemeIconBackground;
|
||||
Brush foreground = currentTheme == Theme.Dark ? DarkThemeIconForeground : LightThemeIconForeground;
|
||||
Bitmap bitmap = new Bitmap(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();
|
||||
|
||||
// Calculate scaling factors
|
||||
float scaleX = (float)IconSize / textSize.Width;
|
||||
float scaleY = (float)IconSize / textSize.Height;
|
||||
float scale = Math.Min(scaleX, scaleY) * 0.8f; // Use the smaller scale factor to maintain aspect ratio
|
||||
|
||||
// Calculate the position to center the text
|
||||
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 (Path.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
FileStream fileStream = new FileStream(path, FileMode.CreateNew);
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
WorkspacesCsharpLibrary.DrawHelper.SaveBitmap(icon, memoryStream);
|
||||
|
||||
BinaryWriter iconWriter = new BinaryWriter(fileStream);
|
||||
if (fileStream != null && iconWriter != null)
|
||||
{
|
||||
// 0-1 reserved, 0
|
||||
iconWriter.Write((byte)0);
|
||||
iconWriter.Write((byte)0);
|
||||
|
||||
// 2-3 image type, 1 = icon, 2 = cursor
|
||||
iconWriter.Write((short)1);
|
||||
|
||||
// 4-5 number of images
|
||||
iconWriter.Write((short)1);
|
||||
|
||||
// image entry 1
|
||||
// 0 image width
|
||||
iconWriter.Write((byte)IconSize);
|
||||
|
||||
// 1 image height
|
||||
iconWriter.Write((byte)IconSize);
|
||||
|
||||
// 2 number of colors
|
||||
iconWriter.Write((byte)0);
|
||||
|
||||
// 3 reserved
|
||||
iconWriter.Write((byte)0);
|
||||
|
||||
// 4-5 color planes
|
||||
iconWriter.Write((short)0);
|
||||
|
||||
// 6-7 bits per pixel
|
||||
iconWriter.Write((short)32);
|
||||
|
||||
// 8-11 size of image data
|
||||
iconWriter.Write((int)memoryStream.Length);
|
||||
|
||||
// 12-15 offset of image data
|
||||
iconWriter.Write((int)(6 + 16));
|
||||
|
||||
// write image data
|
||||
// png data must contain the whole png data file
|
||||
iconWriter.Write(memoryStream.ToArray());
|
||||
|
||||
iconWriter.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
fileStream.Flush();
|
||||
fileStream.Close();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,612 +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.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using WorkspacesCsharpLibrary;
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesCsharpLibrary.Utils;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.Telemetry;
|
||||
using WorkspacesEditor.Utils;
|
||||
using static WorkspacesCsharpLibrary.Data.WorkspacesData;
|
||||
|
||||
namespace WorkspacesEditor.ViewModels
|
||||
{
|
||||
public class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private WorkspacesEditorIO _workspacesEditorIO;
|
||||
private ProjectEditor editPage;
|
||||
private SnapshotWindow _snapshotWindow;
|
||||
private List<OverlayWindow> _overlayWindows = new List<OverlayWindow>();
|
||||
private Project editedProject;
|
||||
private Project projectBeforeLaunch;
|
||||
private string projectNameBeingEdited;
|
||||
private MainWindow _mainWindow;
|
||||
private Timer lastUpdatedTimer;
|
||||
private WorkspacesSettings settings;
|
||||
private PwaHelper _pwaHelper;
|
||||
private bool _isExistingProjectLaunched;
|
||||
|
||||
public ObservableCollection<Project> Workspaces { get; set; } = new ObservableCollection<Project>();
|
||||
|
||||
public IEnumerable<Project> WorkspacesView
|
||||
{
|
||||
get
|
||||
{
|
||||
IEnumerable<Project> workspaces = GetFilteredWorkspaces();
|
||||
IsWorkspacesViewEmpty = !(workspaces != null && workspaces.Any());
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsWorkspacesViewEmpty)));
|
||||
if (IsWorkspacesViewEmpty)
|
||||
{
|
||||
if (Workspaces != null && Workspaces.Any())
|
||||
{
|
||||
EmptyWorkspacesViewMessage = Properties.Resources.NoWorkspacesMatch;
|
||||
}
|
||||
else
|
||||
{
|
||||
EmptyWorkspacesViewMessage = Properties.Resources.No_Workspaces_Message;
|
||||
}
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(EmptyWorkspacesViewMessage)));
|
||||
|
||||
return Enumerable.Empty<Project>();
|
||||
}
|
||||
|
||||
OrderBy orderBy = (OrderBy)_orderByIndex;
|
||||
if (orderBy == OrderBy.LastViewed)
|
||||
{
|
||||
return workspaces.OrderByDescending(x => x.LastLaunchedTime);
|
||||
}
|
||||
else if (orderBy == OrderBy.Created)
|
||||
{
|
||||
return workspaces.OrderByDescending(x => x.CreationTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
return workspaces.OrderBy(x => x.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsWorkspacesViewEmpty { get; set; }
|
||||
|
||||
public string EmptyWorkspacesViewMessage { get; set; }
|
||||
|
||||
// return those workspaces where the project name or any of the selected apps' name contains the search term
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
private string _searchTerm;
|
||||
|
||||
public string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
set
|
||||
{
|
||||
_searchTerm = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
|
||||
}
|
||||
}
|
||||
|
||||
private int _orderByIndex;
|
||||
|
||||
public int OrderByIndex
|
||||
{
|
||||
get => _orderByIndex;
|
||||
set
|
||||
{
|
||||
_orderByIndex = value;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
|
||||
settings.Properties.SortBy = (WorkspacesProperties.SortByProperty)value;
|
||||
settings.Save(SettingsUtils.Default);
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public MainViewModel(WorkspacesEditorIO workspacesEditorIO)
|
||||
{
|
||||
settings = Utils.Settings.ReadSettings();
|
||||
_orderByIndex = (int)settings.Properties.SortBy;
|
||||
_workspacesEditorIO = workspacesEditorIO;
|
||||
_pwaHelper = new PwaHelper();
|
||||
lastUpdatedTimer = new System.Timers.Timer();
|
||||
lastUpdatedTimer.Interval = 1000;
|
||||
lastUpdatedTimer.Elapsed += LastUpdatedTimerElapsed;
|
||||
lastUpdatedTimer.Start();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
foreach (Project project in Workspaces)
|
||||
{
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
|
||||
}
|
||||
|
||||
public void SetEditedProject(Project editedProject)
|
||||
{
|
||||
this.editedProject = editedProject;
|
||||
}
|
||||
|
||||
public void SaveProject(Project projectToSave)
|
||||
{
|
||||
SendEditTelemetryEvent(projectToSave, editedProject);
|
||||
|
||||
if (editedProject.Name != projectToSave.Name)
|
||||
{
|
||||
RemoveShortcut(editedProject);
|
||||
}
|
||||
|
||||
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.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("AppsCountString"));
|
||||
editedProject.Initialize(App.GetCurrentTheme());
|
||||
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
|
||||
ApplyShortcut(editedProject);
|
||||
}
|
||||
|
||||
private string GetDesktopShortcutAddress(Project project) => Path.Combine(FolderUtils.Desktop(), project.Name + ".lnk");
|
||||
|
||||
private string GetShortcutStoreAddress(Project project)
|
||||
{
|
||||
var dataFolder = FolderUtils.DataFolder();
|
||||
Directory.CreateDirectory(dataFolder);
|
||||
var shortcutStoreFolder = Path.Combine(dataFolder, "WorkspacesIcons");
|
||||
Directory.CreateDirectory(shortcutStoreFolder);
|
||||
return Path.Combine(shortcutStoreFolder, project.Id + ".ico");
|
||||
}
|
||||
|
||||
private void ApplyShortcut(Project project)
|
||||
{
|
||||
if (!project.IsShortcutNeeded)
|
||||
{
|
||||
RemoveShortcut(project);
|
||||
return;
|
||||
}
|
||||
|
||||
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var shortcutAddress = GetDesktopShortcutAddress(project);
|
||||
var shortcutIconFilename = GetShortcutStoreAddress(project);
|
||||
|
||||
Bitmap icon = WorkspacesIcon.DrawIcon(WorkspacesIcon.IconTextFromProjectName(project.Name), App.GetCurrentTheme());
|
||||
WorkspacesIcon.SaveIcon(icon, shortcutIconFilename);
|
||||
|
||||
try
|
||||
{
|
||||
// Workaround to be able to create a shortcut with unicode filename
|
||||
File.WriteAllBytes(shortcutAddress, Array.Empty<byte>());
|
||||
|
||||
// Create a ShellLinkObject that references the .lnk file
|
||||
Shell32.Shell shell = new Shell32.Shell();
|
||||
Shell32.Folder dir = shell.NameSpace(FolderUtils.Desktop());
|
||||
Shell32.FolderItem folderItem = dir.Items().Item($"{project.Name}.lnk");
|
||||
Shell32.ShellLinkObject link = (Shell32.ShellLinkObject)folderItem.GetLink;
|
||||
|
||||
// Set the .lnk file properties
|
||||
link.Description = $"Project Launcher {project.Id}";
|
||||
link.Path = Path.Combine(basePath, "PowerToys.WorkspacesLauncher.exe");
|
||||
link.Arguments = $"{project.Id.ToString()} {(int)InvokePoint.Shortcut}";
|
||||
link.WorkingDirectory = basePath;
|
||||
link.SetIconLocation(shortcutIconFilename, 0);
|
||||
|
||||
link.Save(shortcutAddress);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Shortcut creation error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveProjectName(Project project)
|
||||
{
|
||||
projectNameBeingEdited = project.Name;
|
||||
}
|
||||
|
||||
public void CancelProjectName(Project project)
|
||||
{
|
||||
project.Name = projectNameBeingEdited;
|
||||
}
|
||||
|
||||
public async void SnapWorkspace()
|
||||
{
|
||||
CancelSnapshot();
|
||||
|
||||
await Task.Run(() => RunSnapshotTool(_isExistingProjectLaunched));
|
||||
|
||||
Project project = _workspacesEditorIO.ParseTempProject();
|
||||
if (project != null)
|
||||
{
|
||||
if (_isExistingProjectLaunched)
|
||||
{
|
||||
project.UpdateAfterLaunchAndEdit(projectBeforeLaunch);
|
||||
project.EditorWindowTitle = Properties.Resources.EditWorkspace;
|
||||
editPage.DataContext = project;
|
||||
CheckShortcutPresence(project);
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
else
|
||||
{
|
||||
EditProject(project, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RevertLaunch()
|
||||
{
|
||||
CheckShortcutPresence(projectBeforeLaunch);
|
||||
editPage.DataContext = projectBeforeLaunch;
|
||||
projectBeforeLaunch.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
public void EditProject(Project selectedProject, bool isNewlyCreated = false)
|
||||
{
|
||||
editPage = new ProjectEditor(this);
|
||||
|
||||
SetEditedProject(selectedProject);
|
||||
if (!isNewlyCreated)
|
||||
{
|
||||
selectedProject = new Project(selectedProject);
|
||||
}
|
||||
|
||||
if (isNewlyCreated)
|
||||
{
|
||||
// generate a default name for the new project
|
||||
string defaultNamePrefix = Properties.Resources.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 (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedProject.Name = defaultNamePrefix + " " + (nextProjectIndex + 1).ToString(CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
selectedProject.EditorWindowTitle = isNewlyCreated ? Properties.Resources.CreateWorkspace : Properties.Resources.EditWorkspace;
|
||||
selectedProject.Initialize(App.GetCurrentTheme());
|
||||
|
||||
CheckShortcutPresence(selectedProject);
|
||||
|
||||
editPage.DataContext = selectedProject;
|
||||
_mainWindow.ShowPage(editPage);
|
||||
lastUpdatedTimer.Stop();
|
||||
}
|
||||
|
||||
private void CheckShortcutPresence(Project project)
|
||||
{
|
||||
string basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
string shortcutAddress = Path.Combine(FolderUtils.Desktop(), project.Name + ".lnk");
|
||||
project.IsShortcutNeeded = File.Exists(shortcutAddress);
|
||||
}
|
||||
|
||||
public void AddNewProject(Project project)
|
||||
{
|
||||
project.Applications.RemoveAll(app => !app.IsIncluded);
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
Workspaces.Add(project);
|
||||
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
|
||||
TempProjectData.DeleteTempFile();
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
|
||||
ApplyShortcut(project);
|
||||
SendCreateTelemetryEvent(project);
|
||||
}
|
||||
|
||||
public void DeleteProject(Project selectedProject)
|
||||
{
|
||||
Workspaces.Remove(selectedProject);
|
||||
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
|
||||
RemoveShortcut(selectedProject);
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
|
||||
SendDeleteTelemetryEvent();
|
||||
}
|
||||
|
||||
private void RemoveShortcut(Project selectedProject)
|
||||
{
|
||||
string shortcutAddress = GetDesktopShortcutAddress(selectedProject);
|
||||
string shortcutIconFilename = GetShortcutStoreAddress(selectedProject);
|
||||
|
||||
if (File.Exists(shortcutIconFilename))
|
||||
{
|
||||
File.Delete(shortcutIconFilename);
|
||||
}
|
||||
|
||||
if (File.Exists(shortcutAddress))
|
||||
{
|
||||
File.Delete(shortcutAddress);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMainWindow(MainWindow mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
public void SwitchToMainView()
|
||||
{
|
||||
_mainWindow.SwitchToMainView();
|
||||
SearchTerm = string.Empty;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchTerm)));
|
||||
lastUpdatedTimer.Start();
|
||||
editedProject = null;
|
||||
}
|
||||
|
||||
public void LaunchProject(string projectId)
|
||||
{
|
||||
if (!Workspaces.Where(x => x.Id == projectId).Any())
|
||||
{
|
||||
Logger.LogWarning($"Workspace to launch not found. Id: {projectId}");
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchProject(Workspaces.Where(x => x.Id == projectId).First(), true);
|
||||
}
|
||||
|
||||
public async void LaunchProject(Project project, bool exitAfterLaunch = false)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Run(() => RunLauncher(project.Id, InvokePoint.EditorButton));
|
||||
if (_workspacesEditorIO.ParseWorkspaces(this).Result == true)
|
||||
{
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
|
||||
}
|
||||
|
||||
if (exitAfterLaunch)
|
||||
{
|
||||
Logger.LogInfo($"Launched the Workspace {project.Name}. Exiting.");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void LastUpdatedTimerElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (Workspaces == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Project project in Workspaces)
|
||||
{
|
||||
project.OnPropertyChanged(new PropertyChangedEventArgs("LastLaunched"));
|
||||
}
|
||||
}
|
||||
|
||||
private void RunSnapshotTool(bool isExistingProjectLaunched)
|
||||
{
|
||||
Process process = new Process();
|
||||
|
||||
var exeDir = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
var snapshotUtilsPath = Path.Combine(exeDir, "PowerToys.WorkspacesSnapshotTool.exe");
|
||||
|
||||
process.StartInfo = new ProcessStartInfo(snapshotUtilsPath);
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
process.StartInfo.Arguments = isExistingProjectLaunched ? $"{(int)InvokePoint.LaunchAndEdit}" : string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"An error occurred: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RunLauncher(string projectId, InvokePoint invokePoint)
|
||||
{
|
||||
Process process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo(@".\PowerToys.WorkspacesLauncher.exe", $"{projectId} {(int)invokePoint}");
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"An error occurred: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
internal void CloseAllPopups()
|
||||
{
|
||||
foreach (Project project in Workspaces)
|
||||
{
|
||||
project.IsPopupVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void EnterSnapshotMode(bool isExistingProjectLaunched)
|
||||
{
|
||||
_isExistingProjectLaunched = isExistingProjectLaunched;
|
||||
_mainWindow.WindowState = System.Windows.WindowState.Minimized;
|
||||
_overlayWindows.Clear();
|
||||
foreach (var screen in MonitorHelper.GetDpiUnawareScreens())
|
||||
{
|
||||
var bounds = screen.Bounds;
|
||||
OverlayWindow overlayWindow = new OverlayWindow();
|
||||
|
||||
// Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups
|
||||
overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
|
||||
|
||||
overlayWindow.ShowActivated = true;
|
||||
overlayWindow.Topmost = true;
|
||||
overlayWindow.Show();
|
||||
_overlayWindows.Add(overlayWindow);
|
||||
}
|
||||
|
||||
_snapshotWindow = new SnapshotWindow(this);
|
||||
_snapshotWindow.ShowActivated = true;
|
||||
_snapshotWindow.Topmost = true;
|
||||
_snapshotWindow.Show();
|
||||
}
|
||||
|
||||
internal void CancelSnapshot()
|
||||
{
|
||||
foreach (OverlayWindow overlayWindow in _overlayWindows)
|
||||
{
|
||||
overlayWindow.Close();
|
||||
}
|
||||
|
||||
_mainWindow.WindowState = System.Windows.WindowState.Normal;
|
||||
}
|
||||
|
||||
internal async void LaunchAndEdit(Project project)
|
||||
{
|
||||
await Task.Run(() => RunLauncher(project.Id, InvokePoint.LaunchAndEdit));
|
||||
projectBeforeLaunch = new Project(project);
|
||||
EnterSnapshotMode(true);
|
||||
}
|
||||
|
||||
private void SendCreateTelemetryEvent(Project project)
|
||||
{
|
||||
var telemetryEvent = new CreateEvent();
|
||||
telemetryEvent.Successful = true;
|
||||
telemetryEvent.NumScreens = project.Monitors.Count;
|
||||
telemetryEvent.AppCount = project.Applications.Count;
|
||||
telemetryEvent.CliCount = project.Applications.FindAll(app => app.CommandLineArguments.Length > 0).Count;
|
||||
telemetryEvent.ShortcutCreated = project.IsShortcutNeeded;
|
||||
telemetryEvent.AdminCount = project.Applications.FindAll(app => app.IsElevated).Count;
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
}
|
||||
|
||||
private void SendEditTelemetryEvent(Project updatedProject, Project prevProject)
|
||||
{
|
||||
int appsRemovedCount = updatedProject.Applications.FindAll(val => !val.IsIncluded).Count;
|
||||
foreach (var app in prevProject.Applications)
|
||||
{
|
||||
var updatedApp = updatedProject.Applications.Find(val => app.AppName == val.AppName && app.Position == val.Position);
|
||||
if (updatedApp == null)
|
||||
{
|
||||
appsRemovedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
int appsAddedCount = 0;
|
||||
int cliAdded = 0, cliRemoved = 0;
|
||||
int adminAdded = 0, adminRemoved = 0;
|
||||
foreach (var app in updatedProject.Applications)
|
||||
{
|
||||
var prevApp = prevProject.Applications.Find(val => app.AppName == val.AppName && app.Position == val.Position);
|
||||
if (prevApp == null)
|
||||
{
|
||||
if (app.IsIncluded)
|
||||
{
|
||||
appsAddedCount++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (app.CommandLineArguments.Length > 0 && prevApp.CommandLineArguments.Length == 0)
|
||||
{
|
||||
cliAdded++;
|
||||
}
|
||||
|
||||
if (prevApp.CommandLineArguments.Length > 0 && app.CommandLineArguments.Length == 0)
|
||||
{
|
||||
cliRemoved++;
|
||||
}
|
||||
|
||||
if (app.IsElevated && !prevApp.IsElevated)
|
||||
{
|
||||
adminAdded++;
|
||||
}
|
||||
|
||||
if (!app.IsElevated && prevApp.IsElevated)
|
||||
{
|
||||
adminRemoved++;
|
||||
}
|
||||
}
|
||||
|
||||
var telemetryEvent = new EditEvent();
|
||||
telemetryEvent.Successful = true;
|
||||
telemetryEvent.ScreenCountDelta = updatedProject.Monitors.Count - prevProject.Monitors.Count;
|
||||
telemetryEvent.AppsAdded = appsAddedCount;
|
||||
telemetryEvent.AppsRemoved = appsRemovedCount;
|
||||
telemetryEvent.CliAdded = cliAdded;
|
||||
telemetryEvent.CliRemoved = cliRemoved;
|
||||
telemetryEvent.AdminAdded = adminAdded;
|
||||
telemetryEvent.AdminRemoved = adminRemoved;
|
||||
telemetryEvent.PixelAdjustmentsUsed = updatedProject.IsPositionChangedManually;
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
}
|
||||
|
||||
private void SendDeleteTelemetryEvent()
|
||||
{
|
||||
var telemetryEvent = new EditEvent();
|
||||
telemetryEvent.Successful = true;
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
<Page
|
||||
x:Class="WorkspacesEditor.ProjectEditor"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:WorkspacesEditor.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:WorkspacesEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="clr-namespace:WorkspacesEditor.Models"
|
||||
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
|
||||
Title="Workspaces Editor"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
|
||||
<Style x:Key="TextBlockEnabledStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="headerTemplate">
|
||||
<Border HorizontalAlignment="Stretch">
|
||||
<TextBlock
|
||||
Margin="0,16,0,8"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Left"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding MonitorName}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="appTemplate">
|
||||
<Border
|
||||
Margin="0,4,0,0"
|
||||
MouseEnter="AppBorder_MouseEnter"
|
||||
MouseLeave="AppBorder_MouseLeave">
|
||||
<Expander
|
||||
AutomationProperties.AutomationId="{Binding AppName}"
|
||||
AutomationProperties.Name="{Binding AppName}"
|
||||
IsEnabled="{Binding IsIncluded, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsExpanded="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<Expander.Header>
|
||||
<Grid HorizontalAlignment="{Binding HorizontalAlignment, RelativeSource={RelativeSource AncestorType=ContentPresenter}, Mode=OneWayToSource}" FlowDirection="LeftToRight">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Column="1"
|
||||
Width="20"
|
||||
Height="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Source="{Binding IconBitmapImage, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<StackPanel Grid.Column="3" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding AppName}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding RepeatIndexString, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource SystemFillColorCautionBrush}"
|
||||
Text=""
|
||||
Visibility="{Binding IsNotFound, Converter={StaticResource BoolToVis}, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<TextBlock.ToolTip>
|
||||
<ToolTip>
|
||||
<TextBlock
|
||||
FontFamily="Segoe UI Variable,SegoeUI"
|
||||
Text="{x:Static props:Resources.NotFoundTooltip}"
|
||||
TextWrapping="Wrap" />
|
||||
</ToolTip>
|
||||
</TextBlock.ToolTip>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
|
||||
Text="{Binding AppMainParams, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Visibility="{Binding IsAppMainParamVisible, Converter={StaticResource BoolToVis}, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
<controls:ResetIsEnabled Grid.Column="4">
|
||||
<Button
|
||||
Width="Auto"
|
||||
Margin="12,4"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Delete}"
|
||||
Click="DeleteButtonClicked"
|
||||
Content="{Binding DeleteButtonContent, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="True" />
|
||||
</controls:ResetIsEnabled>
|
||||
</Grid>
|
||||
</Expander.Header>
|
||||
<Grid
|
||||
Margin="52,8,48,8"
|
||||
HorizontalAlignment="{Binding HorizontalAlignment, RelativeSource={RelativeSource AncestorType=ContentPresenter}, Mode=OneWayToSource}"
|
||||
FlowDirection="LeftToRight">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox
|
||||
MinWidth="12"
|
||||
Content="{x:Static props:Resources.LaunchAsAdmin}"
|
||||
IsChecked="{Binding IsElevated, Mode=TwoWay}"
|
||||
IsEnabled="{Binding CanLaunchElevated, Mode=OneWay}" />
|
||||
<DockPanel Grid.Row="1" Margin="0,16,0,0">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.CliArguments}" />
|
||||
<TextBox
|
||||
x:Name="CommandLineTextBox"
|
||||
Margin="12,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding CommandLineArguments, Mode=TwoWay}"
|
||||
TextChanged="CommandLineTextBox_TextChanged" />
|
||||
</DockPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,16,0,0"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource TextBlockEnabledStyle}"
|
||||
Text="{x:Static props:Resources.WindowPosition}" />
|
||||
<ComboBox
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
SelectedIndex="{Binding PositionComboboxIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ComboBoxItem Content="{x:Static props:Resources.Custom}" />
|
||||
<ComboBoxItem Content="{x:Static props:Resources.Maximized}" />
|
||||
<ComboBoxItem Content="{x:Static props:Resources.Minimized}" />
|
||||
</ComboBox>
|
||||
<TextBlock
|
||||
Margin="24,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource TextBlockEnabledStyle}"
|
||||
Text="{x:Static props:Resources.Left}" />
|
||||
<TextBox
|
||||
x:Name="LeftTextBox"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Text="{Binding Position.X, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextChanged="LeftTextBox_TextChanged" />
|
||||
<TextBlock
|
||||
Margin="24,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource TextBlockEnabledStyle}"
|
||||
Text="{x:Static props:Resources.Top}" />
|
||||
<TextBox
|
||||
x:Name="TopTextBox"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Text="{Binding Position.Y, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextChanged="TopTextBox_TextChanged" />
|
||||
<TextBlock
|
||||
Margin="24,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource TextBlockEnabledStyle}"
|
||||
Text="{x:Static props:Resources.Width}" />
|
||||
<TextBox
|
||||
x:Name="WidthTextBox"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Text="{Binding Position.Width, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextChanged="WidthTextBox_TextChanged" />
|
||||
<TextBlock
|
||||
Margin="24,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource TextBlockEnabledStyle}"
|
||||
Text="{x:Static props:Resources.Height}" />
|
||||
<TextBox
|
||||
x:Name="HeightTextBox"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Text="{Binding Position.Height, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
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="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="24,0,24,24">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- faux BreadcrumbBar -->
|
||||
<StackPanel VerticalAlignment="Top" Orientation="Horizontal">
|
||||
<Button
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
Click="CancelButtonClicked"
|
||||
Content="{x:Static props:Resources.Workspaces}"
|
||||
FontSize="24"
|
||||
Style="{DynamicResource SubtleButtonStyle}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding EditorWindowTitle}" />
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="SaveButton"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Save_Workspace}"
|
||||
Click="SaveButtonClicked"
|
||||
IsEnabled="{Binding CanBeSaved, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{DynamicResource AccentButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Save_Workspace}"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.Save_Workspace}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
Margin="8,0,0,0"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
|
||||
Click="CancelButtonClicked"
|
||||
Content="{x:Static props:Resources.Cancel}" />
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- preview -->
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="24,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<DockPanel Margin="16">
|
||||
<Image
|
||||
Width="{Binding PreviewImageWidth, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="200"
|
||||
Margin="2"
|
||||
DockPanel.Dock="Top"
|
||||
Source="{Binding PreviewImage, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Stretch="Fill" />
|
||||
<Button
|
||||
x:Name="RevertButton"
|
||||
HorizontalAlignment="Right"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Revert}"
|
||||
Click="RevertButtonClicked"
|
||||
Content="{x:Static props:Resources.Revert}"
|
||||
DockPanel.Dock="Right"
|
||||
IsEnabled="{Binding IsRevertEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
x:Name="LaunchEditButton"
|
||||
Margin="0,0,8,0"
|
||||
HorizontalAlignment="Right"
|
||||
AutomationProperties.Name="{x:Static props:Resources.LaunchEdit}"
|
||||
Click="LaunchEditButtonClicked"
|
||||
Content="{x:Static props:Resources.LaunchEdit}"
|
||||
DockPanel.Dock="Right" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- properties -->
|
||||
<DockPanel
|
||||
Grid.Row="2"
|
||||
Margin="24,16,24,0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{x:Static props:Resources.WorkspaceName}" />
|
||||
<TextBox
|
||||
x:Name="EditNameTextBox"
|
||||
Width="300"
|
||||
HorizontalAlignment="Left"
|
||||
GotFocus="EditNameTextBox_GotFocus"
|
||||
KeyDown="EditNameTextBoxKeyDown"
|
||||
Text="{Binding Name, Mode=TwoWay}"
|
||||
TextChanged="EditNameTextBox_TextChanged" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
DockPanel.Dock="Right"
|
||||
Orientation="Horizontal">
|
||||
<CheckBox
|
||||
Margin="24,0,0,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Content="{x:Static props:Resources.CreateShortcut}"
|
||||
IsChecked="{Binding IsShortcutNeeded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<CheckBox
|
||||
Margin="16,0,0,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Content="{x:Static props:Resources.MoveIfExist}"
|
||||
IsChecked="{Binding MoveExistingWindows, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<ScrollViewer
|
||||
Grid.Row="3"
|
||||
Margin="0,24,0,0"
|
||||
PreviewMouseWheel="ScrollViewer_PreviewMouseWheel"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="24,0,24,24" Orientation="Vertical">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ItemsControl
|
||||
x:Name="CapturedAppList"
|
||||
AutomationProperties.Name="Captured Application List"
|
||||
ItemTemplateSelector="{StaticResource AppListDataTemplateSelector}"
|
||||
ItemsSource="{Binding ApplicationsListed, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,208 +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.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
using WorkspacesCsharpLibrary.Data;
|
||||
using WorkspacesEditor.Models;
|
||||
using WorkspacesEditor.ViewModels;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for ProjectEditor.xaml
|
||||
/// </summary>
|
||||
public partial class ProjectEditor : Page
|
||||
{
|
||||
private const double ScrollSpeed = 15;
|
||||
private MainViewModel _mainViewModel;
|
||||
|
||||
public ProjectEditor(MainViewModel mainViewModel)
|
||||
{
|
||||
_mainViewModel = mainViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void SaveButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Project projectToSave = this.DataContext as Project;
|
||||
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)
|
||||
{
|
||||
// delete the temp file created by the snapshot tool
|
||||
TempProjectData.DeleteTempFile();
|
||||
|
||||
_mainViewModel.SwitchToMainView();
|
||||
}
|
||||
|
||||
private void DeleteButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Button button = sender as Button;
|
||||
Models.Application app = button.DataContext as Models.Application;
|
||||
app.SwitchDeletion();
|
||||
}
|
||||
|
||||
private void EditNameTextBoxKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
e.Handled = true;
|
||||
Project project = this.DataContext as Project;
|
||||
TextBox textBox = sender as TextBox;
|
||||
project.Name = textBox.Text;
|
||||
}
|
||||
else if (e.Key == Key.Escape)
|
||||
{
|
||||
e.Handled = true;
|
||||
Project project = this.DataContext as Project;
|
||||
_mainViewModel.CancelProjectName(project);
|
||||
}
|
||||
}
|
||||
|
||||
private void EditNameTextBox_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.SaveProjectName(DataContext as Project);
|
||||
}
|
||||
|
||||
private void AppBorder_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
Border border = sender as Border;
|
||||
Models.Application app = border.DataContext as Models.Application;
|
||||
app.IsHighlighted = true;
|
||||
Project project = app.Parent;
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
private void AppBorder_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
Border border = sender as Border;
|
||||
Models.Application app = border.DataContext as Models.Application;
|
||||
if (app == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
app.IsHighlighted = false;
|
||||
Project project = app.Parent;
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
private void EditNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
Project project = this.DataContext as Project;
|
||||
TextBox textBox = sender as TextBox;
|
||||
project.Name = textBox.Text;
|
||||
project.OnPropertyChanged(new PropertyChangedEventArgs(nameof(Project.CanBeSaved)));
|
||||
}
|
||||
|
||||
private void LeftTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
TextBox textBox = sender as TextBox;
|
||||
Models.Application application = textBox.DataContext as Models.Application;
|
||||
int newPos;
|
||||
if (!int.TryParse(textBox.Text, out newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
application.Position = new Models.Application.WindowPosition() { X = newPos, Y = application.Position.Y, Width = application.Position.Width, Height = application.Position.Height };
|
||||
Project project = application.Parent;
|
||||
project.IsPositionChangedManually = true;
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
private void TopTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
TextBox textBox = sender as TextBox;
|
||||
Models.Application application = textBox.DataContext as Models.Application;
|
||||
int newPos;
|
||||
if (!int.TryParse(textBox.Text, out newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
application.Position = new Models.Application.WindowPosition() { X = application.Position.X, Y = newPos, Width = application.Position.Width, Height = application.Position.Height };
|
||||
Project project = application.Parent;
|
||||
project.IsPositionChangedManually = true;
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
private void WidthTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
TextBox textBox = sender as TextBox;
|
||||
Models.Application application = textBox.DataContext as Models.Application;
|
||||
int newPos;
|
||||
if (!int.TryParse(textBox.Text, out newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
application.Position = new Models.Application.WindowPosition() { X = application.Position.X, Y = application.Position.Y, Width = newPos, Height = application.Position.Height };
|
||||
Project project = application.Parent;
|
||||
project.IsPositionChangedManually = true;
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
private void HeightTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
TextBox textBox = sender as TextBox;
|
||||
Models.Application application = textBox.DataContext as Models.Application;
|
||||
int newPos;
|
||||
if (!int.TryParse(textBox.Text, out newPos))
|
||||
{
|
||||
newPos = 0;
|
||||
}
|
||||
|
||||
application.Position = new Models.Application.WindowPosition() { X = application.Position.X, Y = application.Position.Y, Width = application.Position.Width, Height = newPos };
|
||||
Project project = application.Parent;
|
||||
project.IsPositionChangedManually = true;
|
||||
project.Initialize(App.GetCurrentTheme());
|
||||
}
|
||||
|
||||
private void CommandLineTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
TextBox textBox = sender as TextBox;
|
||||
Models.Application application = textBox.DataContext as Models.Application;
|
||||
application.CommandLineTextChanged(textBox.Text);
|
||||
}
|
||||
|
||||
private void LaunchEditButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Button button = sender as Button;
|
||||
Project project = button.DataContext as Project;
|
||||
_mainViewModel.LaunchAndEdit(project);
|
||||
}
|
||||
|
||||
private void RevertButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.RevertLaunch();
|
||||
}
|
||||
|
||||
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
ScrollViewer scrollViewer = sender as ScrollViewer;
|
||||
double scrollAmount = Math.Sign(e.Delta) * ScrollSpeed;
|
||||
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - scrollAmount);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?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="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- UAC Manifest Options
|
||||
If you want to change the Windows User Account Control level replace the
|
||||
requestedExecutionLevel node with one of the following.
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||
Remove this element if your application requires this virtualization for backwards
|
||||
compatibility.
|
||||
-->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
|
||||
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
|
||||
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
|
||||
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
|
||||
<!--
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,152 @@
|
||||
// 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.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace WorkspacesEditorUITest;
|
||||
|
||||
/// <summary>
|
||||
/// Design validation tests for the Workspace Editing page.
|
||||
/// This page appears when a user clicks "Edit" on a workspace
|
||||
/// and shows the app list with positioning controls.
|
||||
///
|
||||
/// UI elements that must be preserved:
|
||||
/// - Workspace name text box
|
||||
/// - App list with per-app controls
|
||||
/// - Save/Cancel buttons
|
||||
/// - Position controls (X, Y, Width, Height or Maximized/Minimized dropdown)
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class EditingPageDesignTests : WorkspacesUiAutomationBase
|
||||
{
|
||||
public EditingPageDesignTests()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
// Ensure at least one workspace exists
|
||||
AttachWorkspacesEditor();
|
||||
if (!Has<Element>(By.AccessibilityId("WorkspacesItemsControl")))
|
||||
{
|
||||
CreateTestWorkspace("EditDesignTest");
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod("EditingPage.HasNameTextBox")]
|
||||
[TestCategory("Design.EditingPage")]
|
||||
public void EditingPage_HasWorkspaceNameInput()
|
||||
{
|
||||
NavigateToEditPage();
|
||||
|
||||
Assert.IsTrue(
|
||||
Has<TextBox>(By.AccessibilityId("EditNameTextBox")) || Has<TextBox>(By.Name("Workspace name")),
|
||||
"Editing page should have a workspace name text box");
|
||||
|
||||
CancelAndReturn();
|
||||
}
|
||||
|
||||
[TestMethod("EditingPage.HasSaveButton")]
|
||||
[TestCategory("Design.EditingPage")]
|
||||
public void EditingPage_HasSaveButton()
|
||||
{
|
||||
NavigateToEditPage();
|
||||
|
||||
Assert.IsTrue(
|
||||
Has<Button>("Save Workspace") || Has<Button>("Save"),
|
||||
"Editing page should have a Save button");
|
||||
|
||||
CancelAndReturn();
|
||||
}
|
||||
|
||||
[TestMethod("EditingPage.HasCancelButton")]
|
||||
[TestCategory("Design.EditingPage")]
|
||||
public void EditingPage_HasCancelButton()
|
||||
{
|
||||
NavigateToEditPage();
|
||||
|
||||
Assert.IsTrue(Has<Button>("Cancel"), "Editing page should have a Cancel button");
|
||||
|
||||
CancelAndReturn();
|
||||
}
|
||||
|
||||
[TestMethod("EditingPage.HasLaunchAndEditButton")]
|
||||
[TestCategory("Design.EditingPage")]
|
||||
public void EditingPage_HasLaunchAndEditButton()
|
||||
{
|
||||
NavigateToEditPage();
|
||||
|
||||
Assert.IsTrue(
|
||||
Has<Button>("Launch & Edit") || Has<Button>("Launch and Edit"),
|
||||
"Editing page should have a 'Launch & Edit' button");
|
||||
|
||||
CancelAndReturn();
|
||||
}
|
||||
|
||||
[TestMethod("EditingPage.HasAppList")]
|
||||
[TestCategory("Design.EditingPage")]
|
||||
public void EditingPage_HasApplicationsList()
|
||||
{
|
||||
NavigateToEditPage();
|
||||
|
||||
// Should have some app items visible
|
||||
Assert.IsTrue(
|
||||
Has<Element>(By.AccessibilityId("AppList")) || Has<Custom>("AppList"),
|
||||
"Editing page should have an application list");
|
||||
|
||||
CancelAndReturn();
|
||||
}
|
||||
|
||||
[TestMethod("EditingPage.Cancel_ReturnsToMainPage")]
|
||||
[TestCategory("Design.EditingPage")]
|
||||
public void EditingPage_Cancel_ReturnsToMainList()
|
||||
{
|
||||
NavigateToEditPage();
|
||||
|
||||
Find<Button>("Cancel").Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
Assert.IsTrue(Has<Button>("Create Workspace"), "After cancel, should return to main page");
|
||||
}
|
||||
|
||||
private void NavigateToEditPage()
|
||||
{
|
||||
AttachWorkspacesEditor();
|
||||
try
|
||||
{
|
||||
var root = Find<Element>(By.AccessibilityId("WorkspacesItemsControl"));
|
||||
var moreButton = root.Find<Button>(By.AccessibilityId("MoreButton"));
|
||||
moreButton.Click();
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
var editButton = Find<Button>(By.Name("Edit"));
|
||||
editButton.Click();
|
||||
Task.Delay(1000).Wait();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If edit via more menu doesn't work, try direct edit button
|
||||
var editButton = Find<Button>(By.Name("Edit"));
|
||||
editButton?.Click();
|
||||
Task.Delay(1000).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelAndReturn()
|
||||
{
|
||||
try
|
||||
{
|
||||
Find<Button>("Cancel").Click();
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace WorkspacesEditorUITest;
|
||||
|
||||
/// <summary>
|
||||
/// Design validation tests for the Workspaces Editor main window.
|
||||
/// These verify that all expected UI elements are present and accessible,
|
||||
/// serving as a contract that the WinUI migration must satisfy.
|
||||
///
|
||||
/// Window: MainWindow / WorkspacesEditorPage
|
||||
/// Tests cover: header elements, action buttons, workspace list, search, sort.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class EditorMainWindowDesignTests : WorkspacesUiAutomationBase
|
||||
{
|
||||
public EditorMainWindowDesignTests()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Header.TitleTextPresent")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_HasWorkspacesTitleText()
|
||||
{
|
||||
Assert.IsTrue(Has<TextBlock>(By.Name("Workspaces")), "Should display 'Workspaces' title");
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Header.CreateWorkspaceButtonPresent")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_HasCreateWorkspaceButton()
|
||||
{
|
||||
Assert.IsTrue(Has<Button>("Create Workspace"), "Should have 'Create Workspace' button");
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Header.SearchBoxPresent")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_HasSearchBox()
|
||||
{
|
||||
Assert.IsTrue(
|
||||
Has<TextBox>(By.AccessibilityId("SearchBox")) || Has<TextBox>(By.Name("Search")),
|
||||
"Should have a search input");
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Header.SortByPresent")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_HasSortByDropdown()
|
||||
{
|
||||
Assert.IsTrue(
|
||||
Has<ComboBox>(By.AccessibilityId("SortByComboBox")) || Has<ComboBox>(By.Name("SortBy")),
|
||||
"Should have 'Sort by' dropdown");
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Content.WorkspacesListPresent")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_HasWorkspacesList()
|
||||
{
|
||||
// The workspaces list container should exist even when empty
|
||||
Assert.IsTrue(
|
||||
Has<Element>(By.AccessibilityId("WorkspacesItemsControl")) || Has<Custom>("WorkspacesList"),
|
||||
"Should have workspace list container");
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Content.EmptyStateMessagePresent")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_EmptyState_ShowsMessage()
|
||||
{
|
||||
// When no workspaces exist, should show a message
|
||||
var hasEmptyMessage = Has<TextBlock>(By.Name("There are no saved Workspaces"))
|
||||
|| Has<TextBlock>(By.Name("No saved Workspaces"));
|
||||
|
||||
// This test is informational — may not have empty state if workspaces exist
|
||||
if (!Has<Custom>("WorkspacesList") || !Has<Element>(By.ClassName("WorkspaceItem")))
|
||||
{
|
||||
Assert.IsTrue(hasEmptyMessage, "Empty state should show a message when no workspaces exist");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Keyboard.TabNavigationWorks")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_TabNavigation_MovesForwardThroughControls()
|
||||
{
|
||||
// Press Tab and verify focus moves to an interactive element
|
||||
SendKeys(Key.Tab);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// At least one focusable element should have focus
|
||||
// This verifies keyboard navigation isn't broken
|
||||
Assert.IsTrue(true, "Tab navigation executed without crash");
|
||||
}
|
||||
|
||||
[TestMethod("MainWindow.Accessibility.CreateButtonHasAutomationName")]
|
||||
[TestCategory("Design.MainWindow")]
|
||||
public void MainWindow_CreateButton_HasAccessibleName()
|
||||
{
|
||||
var button = Find<Button>("Create Workspace");
|
||||
Assert.IsNotNull(button, "Create Workspace button should be findable by its accessible name");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace WorkspacesEditorUITest;
|
||||
|
||||
/// <summary>
|
||||
/// Design validation tests for the Snapshot/Capture window.
|
||||
/// This window appears when creating a new workspace and shows
|
||||
/// a "Capture" button overlay on the desktop.
|
||||
///
|
||||
/// These tests validate the capture flow UI elements exist
|
||||
/// and are accessible for the WinUI migration.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SnapshotWindowDesignTests : WorkspacesUiAutomationBase
|
||||
{
|
||||
public SnapshotWindowDesignTests()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("SnapshotWindow.HasCaptureButton")]
|
||||
[TestCategory("Design.SnapshotWindow")]
|
||||
public void SnapshotWindow_HasCaptureButton()
|
||||
{
|
||||
AttachWorkspacesEditor();
|
||||
|
||||
var createButton = Find<Button>("Create Workspace");
|
||||
createButton.Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
AttachSnapshotWindow();
|
||||
|
||||
Assert.IsTrue(Has<Button>("Capture"), "Snapshot window should have a Capture button");
|
||||
|
||||
// Cancel to clean up
|
||||
var cancelButton = Find<Button>("Cancel");
|
||||
cancelButton.Click();
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
[TestMethod("SnapshotWindow.HasCancelButton")]
|
||||
[TestCategory("Design.SnapshotWindow")]
|
||||
public void SnapshotWindow_HasCancelButton()
|
||||
{
|
||||
AttachWorkspacesEditor();
|
||||
|
||||
var createButton = Find<Button>("Create Workspace");
|
||||
createButton.Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
AttachSnapshotWindow();
|
||||
|
||||
Assert.IsTrue(Has<Button>("Cancel"), "Snapshot window should have a Cancel button");
|
||||
|
||||
// Clean up
|
||||
Find<Button>("Cancel").Click();
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
[TestMethod("SnapshotWindow.CancelReturnsToEditor")]
|
||||
[TestCategory("Design.SnapshotWindow")]
|
||||
public void SnapshotWindow_CancelButton_ReturnsToEditor()
|
||||
{
|
||||
AttachWorkspacesEditor();
|
||||
|
||||
var createButton = Find<Button>("Create Workspace");
|
||||
createButton.Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
AttachSnapshotWindow();
|
||||
|
||||
Find<Button>("Cancel").Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Should be back in the editor
|
||||
AttachWorkspacesEditor();
|
||||
Assert.IsTrue(Has<Button>("Create Workspace"), "After cancel, should return to editor with Create button visible");
|
||||
}
|
||||
|
||||
[TestMethod("SnapshotWindow.Accessibility.ButtonsHaveNames")]
|
||||
[TestCategory("Design.SnapshotWindow")]
|
||||
public void SnapshotWindow_Buttons_HaveAccessibleNames()
|
||||
{
|
||||
AttachWorkspacesEditor();
|
||||
|
||||
var createButton = Find<Button>("Create Workspace");
|
||||
createButton.Click();
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
AttachSnapshotWindow();
|
||||
|
||||
// Both buttons should be findable by name (meaning they have accessible names)
|
||||
var capture = Find<Button>("Capture");
|
||||
var cancel = Find<Button>("Cancel");
|
||||
|
||||
Assert.IsNotNull(capture, "Capture button should have an accessible name");
|
||||
Assert.IsNotNull(cancel, "Cancel button should have an accessible name");
|
||||
|
||||
// Clean up
|
||||
cancel.Click();
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// 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.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace WorkspacesEditorUITest;
|
||||
|
||||
/// <summary>
|
||||
/// Design validation tests for workspace items in the list.
|
||||
/// When workspaces exist, each item must have: name, app count, launch button,
|
||||
/// edit button, more options button.
|
||||
///
|
||||
/// These define the per-item UI contract the migration must preserve.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class WorkspaceItemDesignTests : WorkspacesUiAutomationBase
|
||||
{
|
||||
public WorkspaceItemDesignTests()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
// Ensure at least one workspace exists for item-level tests
|
||||
if (!HasWorkspaceItem())
|
||||
{
|
||||
CreateTestWorkspace("DesignTest");
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod("WorkspaceItem.HasName")]
|
||||
[TestCategory("Design.WorkspaceItem")]
|
||||
public void WorkspaceItem_DisplaysName()
|
||||
{
|
||||
if (!HasWorkspaceItem())
|
||||
{
|
||||
Assert.Inconclusive("No workspace items available for testing");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = GetFirstWorkspaceItem();
|
||||
Assert.IsNotNull(item, "Should have at least one workspace item");
|
||||
}
|
||||
|
||||
[TestMethod("WorkspaceItem.HasLaunchButton")]
|
||||
[TestCategory("Design.WorkspaceItem")]
|
||||
public void WorkspaceItem_HasLaunchButton()
|
||||
{
|
||||
if (!HasWorkspaceItem())
|
||||
{
|
||||
Assert.Inconclusive("No workspace items available for testing");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = GetFirstWorkspaceItem();
|
||||
var launchButton = item.Find<Button>(By.Name("Launch"));
|
||||
Assert.IsNotNull(launchButton, "Workspace item should have a Launch button");
|
||||
}
|
||||
|
||||
[TestMethod("WorkspaceItem.HasEditButton")]
|
||||
[TestCategory("Design.WorkspaceItem")]
|
||||
public void WorkspaceItem_HasEditButton()
|
||||
{
|
||||
if (!HasWorkspaceItem())
|
||||
{
|
||||
Assert.Inconclusive("No workspace items available for testing");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = GetFirstWorkspaceItem();
|
||||
var editButton = item.Find<Button>(By.Name("Edit"));
|
||||
Assert.IsNotNull(editButton, "Workspace item should have an Edit button");
|
||||
}
|
||||
|
||||
[TestMethod("WorkspaceItem.HasMoreOptionsButton")]
|
||||
[TestCategory("Design.WorkspaceItem")]
|
||||
public void WorkspaceItem_HasMoreOptionsButton()
|
||||
{
|
||||
if (!HasWorkspaceItem())
|
||||
{
|
||||
Assert.Inconclusive("No workspace items available for testing");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = GetFirstWorkspaceItem();
|
||||
var moreButton = item.Find<Button>(By.AccessibilityId("MoreButton"));
|
||||
Assert.IsNotNull(moreButton, "Workspace item should have a More options button");
|
||||
}
|
||||
|
||||
[TestMethod("WorkspaceItem.HasAppCountText")]
|
||||
[TestCategory("Design.WorkspaceItem")]
|
||||
public void WorkspaceItem_DisplaysAppCount()
|
||||
{
|
||||
if (!HasWorkspaceItem())
|
||||
{
|
||||
Assert.Inconclusive("No workspace items available for testing");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = GetFirstWorkspaceItem();
|
||||
|
||||
// App count text should contain a number followed by "App" or "Apps"
|
||||
var textBlocks = item.FindAll<TextBlock>(By.ClassName("TextBlock"));
|
||||
bool hasAppCount = textBlocks.Any(t =>
|
||||
{
|
||||
var text = t.GetAttribute("Name") ?? string.Empty;
|
||||
return text.Contains("App", System.StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
Assert.IsTrue(hasAppCount, "Workspace item should display app count");
|
||||
}
|
||||
|
||||
[TestMethod("WorkspaceItem.HasLastLaunchedText")]
|
||||
[TestCategory("Design.WorkspaceItem")]
|
||||
public void WorkspaceItem_DisplaysLastLaunchedTime()
|
||||
{
|
||||
if (!HasWorkspaceItem())
|
||||
{
|
||||
Assert.Inconclusive("No workspace items available for testing");
|
||||
return;
|
||||
}
|
||||
|
||||
var item = GetFirstWorkspaceItem();
|
||||
|
||||
// Should contain "Last launched" text
|
||||
var textBlocks = item.FindAll<TextBlock>(By.ClassName("TextBlock"));
|
||||
bool hasLastLaunched = textBlocks.Any(t =>
|
||||
{
|
||||
var text = t.GetAttribute("Name") ?? string.Empty;
|
||||
return text.Contains("Last", System.StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
Assert.IsTrue(hasLastLaunched, "Workspace item should display last launched time");
|
||||
}
|
||||
|
||||
private bool HasWorkspaceItem()
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = Find<Element>(By.AccessibilityId("WorkspacesItemsControl"));
|
||||
return root != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Element GetFirstWorkspaceItem()
|
||||
{
|
||||
var root = Find<Element>(By.AccessibilityId("WorkspacesItemsControl"));
|
||||
var items = root.FindAll<Element>(By.ClassName("WorkspaceItem"));
|
||||
return items.Count > 0 ? items[0] : root;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesEditor\WorkspacesEditor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
236
src/modules/Workspaces/WorkspacesEditorWinUiMigration.md
Normal file
236
src/modules/Workspaces/WorkspacesEditorWinUiMigration.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Migrate Workspaces Editor from WPF to WinUI 3
|
||||
|
||||
## Background
|
||||
|
||||
The Workspaces Launcher UI has been successfully migrated from WPF to WinUI 3 ([PR #48700](https://github.com/microsoft/PowerToys/pull/48700)), establishing reusable patterns for the codebase. The Workspaces Editor is the remaining WPF-based UI surface in the Workspaces module and represents a larger, more complex migration effort.
|
||||
|
||||
The Editor is the primary user-facing window for creating, editing, and managing workspaces. It includes multiple pages, complex data templates, and COM interop for shortcut creation.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Migrate the Workspaces Editor from WPF to WinUI 3 to:
|
||||
|
||||
- Complete the Workspaces module WinUI modernization
|
||||
- Remove all WPF dependencies from the Workspaces module
|
||||
- Maintain feature parity with existing Editor functionality
|
||||
- Leverage patterns established in the Launcher UI migration
|
||||
- Improve long-term maintainability and UI consistency
|
||||
|
||||
## Non-Goals
|
||||
|
||||
The following are explicitly out of scope:
|
||||
|
||||
- New user-facing features or UX redesigns
|
||||
- Changes to workspace configuration format (`workspaces.json`)
|
||||
- Changes to the C++ engine components (Launcher, WindowArranger, SnapshotTool)
|
||||
- Changes to the Module Interface
|
||||
- Telemetry changes
|
||||
|
||||
> The objective is functional parity, not feature expansion.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- WorkspacesEditor WPF application (6 XAML files, 31 C# files)
|
||||
- WorkspacesCsharpLibrary WPF imaging code (`BaseApplication.cs` icon handling)
|
||||
- Resource dictionaries and styling
|
||||
- ViewModels and data binding
|
||||
- Accessibility and theme support
|
||||
- Installer and signing updates
|
||||
- WorkspacesEditorUITest updates
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Workspaces core C++ functionality
|
||||
- Launcher UI (already migrated to WinUI 3)
|
||||
- Named pipe IPC protocol
|
||||
- Window placement algorithms
|
||||
- Configuration file format changes
|
||||
|
||||
### Dependencies (Must Be Resolved First)
|
||||
|
||||
> **Blocker:** `WorkspacesCsharpLibrary` contains WPF imaging code (`System.Windows.Media.Imaging.BitmapImage`) used by both the Editor and `Workspaces.ModuleServices`. This library must be updated to remove WPF dependencies before the Editor migration can proceed.
|
||||
|
||||
---
|
||||
|
||||
## Key Challenges
|
||||
|
||||
This migration is significantly more complex than the Launcher UI:
|
||||
|
||||
| Challenge | Details | Approach |
|
||||
|-----------|---------|----------|
|
||||
| Multiple windows/pages | MainWindow, WorkspacesEditorPage, SnapshotWindow, OverlayWindow | Migrate each window independently, starting from leaf pages |
|
||||
| Frame-based navigation | WPF `Frame` + `Page` pattern | WinUI `NavigationView` or direct content switching |
|
||||
| WPF Triggers in multiple locations | `Style.Triggers`, `DataTriggers` on IsEnabled, IsMouseOver | Convert each to `VisualStateManager` states |
|
||||
| Expander with complex DataTemplates | Workspace app list uses `Expander` with nested templates | WinUI `Expander` is a direct equivalent; port DataTemplate content |
|
||||
| COM interop (shortcut creation) | `IWshRuntimeLibrary` for Windows shortcuts | COM interop works identically in WinUI; no migration needed |
|
||||
| BitmapImage in shared library | `WorkspacesCsharpLibrary.BaseApplication` uses WPF imaging | Replace with `Microsoft.UI.Xaml.Media.Imaging.BitmapImage` or `Windows.Graphics.Imaging` |
|
||||
| Icon extraction (GDI+ pipeline) | `System.Drawing.Icon` → `Bitmap` → `BitmapImage` chain | Replace with `Windows.Graphics.Imaging.SoftwareBitmap` pipeline |
|
||||
|
||||
---
|
||||
|
||||
## Risks to Investigate Before Writing Code
|
||||
|
||||
These areas require spikes before committing to implementation:
|
||||
|
||||
### 1. SnapshotWindow & OverlayWindow (HIGH RISK)
|
||||
|
||||
The capture experience relies on:
|
||||
- Transparent windows
|
||||
- Topmost behavior
|
||||
- Screen coordinates and hit testing
|
||||
- Desktop overlay rendering
|
||||
|
||||
WinUI has known gaps in windowing and overlay scenarios that often require `AppWindow`/HWND interop. This is where functional regressions are most likely to surface.
|
||||
|
||||
**Action:** Spike a minimal transparent topmost WinUI window with click-through behavior before estimating Milestone 4.
|
||||
|
||||
### 2. Resource Migration (MEDIUM RISK)
|
||||
|
||||
The `.resx` → `.resw` migration is straightforward per-file but touches nearly every XAML file. Before estimating effort, inventory:
|
||||
- Total number of localized strings
|
||||
- Converters that reference resource strings
|
||||
- Bindings that depend on `{x:Static}` resource syntax
|
||||
|
||||
**Action:** Run a count of `x:Static props:Resources.` references across all Editor XAML files.
|
||||
|
||||
### 3. UITest Migration (MEDIUM RISK)
|
||||
|
||||
UI test migration effort is often underestimated:
|
||||
|
||||
> UI migration = 40% of effort, Test fixes = 60% of effort
|
||||
|
||||
`WorkspacesEditorUITest` may depend on:
|
||||
- WPF-specific element identifiers
|
||||
- Accessibility IDs that change with WinUI
|
||||
- Automation patterns that differ between frameworks
|
||||
|
||||
**Action:** Inspect `WorkspacesEditorUITest` early to understand element identifiers, accessibility IDs, and automation patterns before assuming they port cleanly.
|
||||
|
||||
---
|
||||
|
||||
## PR Structure
|
||||
|
||||
**Single PR with 5 milestones** (same pattern as the Launcher UI migration):
|
||||
|
||||
### Milestone 1: Remove WPF Imaging Dependencies
|
||||
|
||||
**Goal:** Decouple `WorkspacesCsharpLibrary` from WPF-specific imaging APIs.
|
||||
|
||||
- [ ] Remove `System.Windows.Media.Imaging` dependency from `BaseApplication.cs`
|
||||
- [ ] Replace WPF `BitmapImage` property with WinUI-compatible alternative
|
||||
- [ ] Update icon extraction pipeline (GDI+ → SoftwareBitmap or platform-agnostic)
|
||||
- [ ] Verify `Workspaces.ModuleServices` still builds and functions
|
||||
- [ ] Run existing tests to confirm no regressions
|
||||
|
||||
**Success criteria:** No `System.Windows.*` imaging dependencies remain. Existing tests pass. Editor still builds.
|
||||
|
||||
**Why first?** This is the primary blocker — the Editor and ModuleServices both depend on this library.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 2: WinUI Editor Foundation
|
||||
|
||||
**Goal:** Create the new WinUI editor project and bootstrapping infrastructure.
|
||||
|
||||
- [ ] Create new WinUI `.csproj` (`WorkspacesEditor.WinUI`)
|
||||
- [ ] Custom entry point with `DISABLE_XAML_GENERATED_MAIN`
|
||||
- [ ] GPO check and singleton mutex (match Launcher UI pattern)
|
||||
- [ ] `DispatcherQueue` setup
|
||||
- [ ] Create empty MainWindow shell
|
||||
- [ ] Verify project builds and window displays
|
||||
|
||||
**Success criteria:** Empty editor launches successfully. Existing functionality untouched.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 3: Main Editor Page Migration
|
||||
|
||||
**Goal:** Move the primary workspace management experience to WinUI.
|
||||
|
||||
This is likely the largest milestone.
|
||||
|
||||
- [ ] Port the main editor page layout (workspace list, search, sort, create button)
|
||||
- [ ] Migrate DataTemplates (workspace items with app lists)
|
||||
- [ ] Convert `Expander` controls (WPF Expander → WinUI Expander)
|
||||
- [ ] Port `Style.Triggers` → `VisualStateManager`
|
||||
- [ ] Wire ViewModel data binding (`ObservableCollection`, `INotifyPropertyChanged`)
|
||||
- [ ] Migrate `.resx` strings → `.resw` with `x:Uid` pattern
|
||||
|
||||
**Success criteria:** Workspace list renders. Search works. Sort works. Workspace selection works.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 4: Snapshot + Overlay Migration
|
||||
|
||||
**Goal:** Move the workspace capture experience to WinUI.
|
||||
|
||||
This is where most functional regressions will likely surface due to WinUI windowing limitations.
|
||||
|
||||
- [ ] Port SnapshotWindow (capture overlay)
|
||||
- [ ] Port OverlayWindow (desktop overlay during capture)
|
||||
- [ ] Wire navigation flow: Editor → Snapshot → return to Editor
|
||||
- [ ] Handle transparent/topmost window behavior via `AppWindow`/HWND interop
|
||||
|
||||
**Success criteria:** New workspace creation works end-to-end. Capture flow works. Return-to-editor flow works.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 5: Final Integration & WPF Removal
|
||||
|
||||
**Goal:** Complete migration and remove legacy implementation.
|
||||
|
||||
- [ ] Remove old WPF `WorkspacesEditor` project
|
||||
- [ ] Update installer references (WiX, signing)
|
||||
- [ ] Update solution file (`PowerToys.slnx`)
|
||||
- [ ] Update verification script paths
|
||||
- [ ] Update `WorkspacesEditorUITest` to reference new project
|
||||
- [ ] Accessibility validation (keyboard nav, Narrator, High Contrast)
|
||||
- [ ] Theme validation (Light/Dark/HC)
|
||||
- [ ] Final test pass
|
||||
|
||||
**Success criteria:** All existing scenarios pass. WPF editor removed. WinUI editor becomes production implementation.
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
### Functional Testing
|
||||
|
||||
- [ ] Create new workspace (capture, name, save)
|
||||
- [ ] Edit workspace (rename, remove apps, modify positions)
|
||||
- [ ] Launch workspace from Editor
|
||||
- [ ] Delete workspace
|
||||
- [ ] Search workspaces by name/app
|
||||
- [ ] Sort workspaces (name, created, last launched)
|
||||
- [ ] Create desktop shortcut for workspace
|
||||
- [ ] Launch & Edit flow (re-capture)
|
||||
|
||||
### Accessibility Testing
|
||||
|
||||
- [ ] Keyboard-only navigation through all Editor controls
|
||||
- [ ] Tab order logical and consistent
|
||||
- [ ] Narrator announces all interactive elements
|
||||
- [ ] Focus management after dialogs and page transitions
|
||||
- [ ] High Contrast mode renders correctly
|
||||
|
||||
### Visual Testing
|
||||
|
||||
- [ ] 100%, 150%, 200% DPI scaling
|
||||
- [ ] Light Theme
|
||||
- [ ] Dark Theme
|
||||
- [ ] Multiple monitor environments
|
||||
- [ ] Window resizing behavior
|
||||
|
||||
---
|
||||
|
||||
## Expected Outcome
|
||||
|
||||
The Workspaces module will be completely free of WPF dependencies. Both the Editor and Launcher UI will run on WinUI 3, providing a consistent Fluent UI experience. The patterns established in the Launcher UI migration (project structure, IPC handling, resource management, accessibility approach) will be directly reusable, reducing the learning curve for this larger effort.
|
||||
|
||||
**Estimated effort:** 30–40 hours, single PR with 5 milestones.
|
||||
@@ -22,7 +22,7 @@
|
||||
const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe";
|
||||
const std::wstring workspacesWindowArrangerPath = L"PowerToys.WorkspacesWindowArranger.exe";
|
||||
const std::wstring workspacesSnapshotToolPath = L"PowerToys.WorkspacesSnapshotTool.exe";
|
||||
const std::wstring workspacesEditorPath = L"PowerToys.WorkspacesEditor.exe";
|
||||
const std::wstring workspacesEditorPath = L"WinUI3Apps\\PowerToys.WorkspacesEditor.exe";
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -327,7 +327,7 @@ private:
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
sei.lpFile = L"PowerToys.WorkspacesEditor.exe";
|
||||
sei.lpFile = workspacesEditorPath.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = executable_args.data();
|
||||
if (ShellExecuteExW(&sei))
|
||||
|
||||
@@ -50,7 +50,7 @@ std::vector<std::wstring> processes =
|
||||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
L"WinUI3Apps\\PowerToys.WorkspacesEditor.exe",
|
||||
L"PowerToys.ZoomIt.exe",
|
||||
L"Microsoft.CmdPal.UI.exe",
|
||||
};
|
||||
|
||||
@@ -443,8 +443,8 @@ function Test-CoreFiles {
|
||||
'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',
|
||||
|
||||
Reference in New Issue
Block a user