Merge remote-tracking branch 'upstream/main' into dev/migrie/merge-upstream-again-again

This commit is contained in:
Mike Griese
2025-03-18 20:40:46 -05:00
137 changed files with 9149 additions and 1883 deletions

View File

@@ -3,13 +3,13 @@
"isRoot": true,
"tools": {
"dotnet-consolidate": {
"version": "2.0.0",
"version": "4.2.0",
"commands": [
"dotnet-consolidate"
]
},
"xamlstyler.console": {
"version": "3.2404.2",
"version": "3.2501.8",
"commands": [
"xstyler"
]

View File

@@ -233,6 +233,9 @@ SWAPBUTTON
SYSTEMDOCKED
TABLETPC
# Units
nmi
# MATH
artanh

View File

@@ -513,6 +513,7 @@ flyouts
FMask
fmtid
FOF
WANTNUKEWARNING
FOFX
FOLDERID
folderpath
@@ -1474,6 +1475,7 @@ SHCNE
SHCNF
SHCONTF
Shcore
shellapi
SHELLDETAILS
SHELLDLL
shellex
@@ -1880,6 +1882,7 @@ windowssearch
windowssettings
WINDOWSTYLES
WINDOWSTYLESICON
winerror
WINEVENT
winget
wingetcreate

View File

@@ -146,11 +146,6 @@ jobs:
Write-Host "##vso[task.setvariable variable=MSBuildCacheParameters]$MSBuildCacheParameters"
displayName: Prepare MSBuildCache variables
- template: steps-ensure-dotnet-version.yml
parameters:
sdk: true
version: '6.0'
- template: steps-ensure-dotnet-version.yml
parameters:
sdk: true

View File

@@ -104,4 +104,4 @@
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
</ItemGroup>
</Project>
</Project>

View File

@@ -9,6 +9,7 @@ This software incorporates material from third parties.
- Installer/Runner
- Measure tool
- Peek
- Registry Preview
## Utility: Color Picker
@@ -788,6 +789,34 @@ SOFTWARE.
## Utility: Peek
### Monaco Editor
**Source**: https://github.com/Microsoft/monaco-editor
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
The MIT License (MIT)
Copyright (c) 2016 - present Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### The Quite OK Image Format reference decoder
**Source**: https://github.com/phoboslab/qoi
@@ -1331,6 +1360,35 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
```
## Utility: Registry Preview
### Monaco Editor
**Source**: https://github.com/Microsoft/monaco-editor
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
The MIT License (MIT)
Copyright (c) 2016 - present Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## NuGet Packages used by PowerToys

View File

@@ -236,6 +236,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowDataDiagnosticsValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusReplaceVariablesValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredNewPlusReplaceVariablesValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredRunAtStartupValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredRunAtStartupValue());

View File

@@ -65,6 +65,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue();
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
};
}

View File

@@ -69,6 +69,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredNewPlusHideTemplateFilenameExtensionValue();
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
}
}
}

View File

@@ -11,17 +11,40 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public class By
{
private readonly OpenQA.Selenium.By by;
private readonly OpenQA.Selenium.By? by;
private readonly bool isAccessibilityId;
private readonly string? accessibilityId;
private By(OpenQA.Selenium.By by)
{
isAccessibilityId = false;
this.by = by;
}
private By(string accessibilityId)
{
isAccessibilityId = true;
this.accessibilityId = accessibilityId;
}
public override string ToString()
{
// override ToString to return detailed debugging content provided by OpenQA.Selenium.By
return this.by.ToString();
return this.GetAccessibilityId();
}
public bool GetIsAccessibilityId() => this.isAccessibilityId;
public string GetAccessibilityId()
{
if (this.isAccessibilityId)
{
return this.accessibilityId!;
}
else
{
return this.by!.ToString();
}
}
/// <summary>
@@ -31,6 +54,13 @@ namespace Microsoft.PowerToys.UITest
/// <returns>A By object.</returns>
public static By Name(string name) => new By(OpenQA.Selenium.By.Name(name));
/// <summary>
/// Creates a By object using the className attribute.
/// </summary>
/// <param name="className">The className attribute to search for.</param>
/// <returns>A By object.</returns>
public static By ClassName(string className) => new By(OpenQA.Selenium.By.ClassName(className));
/// <summary>
/// Creates a By object using the ID attribute.
/// </summary>
@@ -38,6 +68,13 @@ namespace Microsoft.PowerToys.UITest
/// <returns>A By object.</returns>
public static By Id(string id) => new By(OpenQA.Selenium.By.Id(id));
/// <summary>
/// Creates a By object using the ID attribute.
/// </summary>
/// <param name="accessibilityId">The ID attribute to search for.</param>
/// <returns>A By object.</returns>
public static By AccessibilityId(string accessibilityId) => new By(accessibilityId);
/// <summary>
/// Creates a By object using the XPath expression.
/// </summary>
@@ -70,6 +107,6 @@ namespace Microsoft.PowerToys.UITest
/// Converts the By object to an OpenQA.Selenium.By object.
/// </summary>
/// <returns>An OpenQA.Selenium.By object.</returns>
internal OpenQA.Selenium.By ToSeleniumBy() => by;
internal OpenQA.Selenium.By ToSeleniumBy() => by!;
}
}

View File

@@ -3,8 +3,13 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
using ABI.Windows.Foundation;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
@@ -63,6 +68,14 @@ namespace Microsoft.PowerToys.UITest
get { return this.windowsElement?.Selected ?? false; }
}
/// <summary>
/// Gets the Rect of the UI element.
/// </summary>
public Rectangle? Rect
{
get { return this.windowsElement?.Rect; }
}
/// <summary>
/// Gets the AutomationID of the UI element.
/// </summary>
@@ -138,6 +151,54 @@ namespace Microsoft.PowerToys.UITest
});
}
/// <summary>
/// Drag element move offset.
/// </summary>
/// <param name="offsetX">The offsetX to move.</param>
/// <param name="offsetY">The offsetY to move.</param>
public void Drag(int offsetX, int offsetY)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
actions.Build().Perform();
});
}
/// <summary>
/// Drag element move to other element.
/// </summary>
/// <param name="element">Move to this element.</param>
public void Drag(Element element)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).ClickAndHold();
Assert.IsNotNull(element.windowsElement, "element is null");
int dx = (element.windowsElement.Rect.X - windowElement.Rect.X) / 10;
int dy = (element.windowsElement.Rect.Y - windowElement.Rect.Y) / 10;
for (int i = 0; i < 10; i++)
{
actions.MoveByOffset(dx, dy);
}
actions.Release();
actions.Build().Perform();
});
}
/// <summary>
/// Send Key of the element.
/// </summary>
/// <param name="key">The Key to Send.</param>
public void SendKeys(string key)
{
PerformAction((actions, windowElement) =>
{
windowElement.SendKeys(key);
});
}
/// <summary>
/// Gets the attribute value of the UI element.
/// </summary>
@@ -222,8 +283,16 @@ namespace Microsoft.PowerToys.UITest
var foundElements = FindHelper.FindAll<T, AppiumWebElement>(
() =>
{
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
return elements;
if (by.GetIsAccessibilityId())
{
var elements = this.windowsElement.FindElementsByAccessibilityId(by.GetAccessibilityId());
return elements;
}
else
{
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
return elements;
}
},
this.driver,
timeoutMS);

View File

@@ -17,6 +17,19 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
internal static class FindHelper
{
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<IReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
var items = findElementsFunc();
var res = items.Select(item =>
{
var element = item as WindowsElement;
return NewElement<T>(element, driver, timeoutMS);
}).Where(item => item.IsMatchingTarget()).ToList();
return new ReadOnlyCollection<T>(res);
}
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{

View File

@@ -8,6 +8,7 @@ using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
namespace Microsoft.PowerToys.UITest
{
@@ -98,8 +99,16 @@ namespace Microsoft.PowerToys.UITest
var foundElements = FindHelper.FindAll<T, WindowsElement>(
() =>
{
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
return elements;
if (by.GetIsAccessibilityId())
{
var elements = this.WindowsDriver.FindElementsByAccessibilityId(by.GetAccessibilityId());
return elements;
}
else
{
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
return elements;
}
},
this.WindowsDriver,
timeoutMS);
@@ -145,6 +154,39 @@ namespace Microsoft.PowerToys.UITest
return this.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Keyboard Action key.
/// </summary>
/// <param name="key1">The Keys1 to click.</param>
/// <param name="key2">The Keys2 to click.</param>
/// <param name="key3">The Keys3 to click.</param>
/// <param name="key4">The Keys4 to click.</param>
public void KeyboardAction(string key1, string key2 = "", string key3 = "", string key4 = "")
{
PerformAction((actions, windowElement) =>
{
if (string.IsNullOrEmpty(key2))
{
actions.SendKeys(key1);
}
else if (string.IsNullOrEmpty(key3))
{
actions.SendKeys(key1).SendKeys(key2);
}
else if (string.IsNullOrEmpty(key4))
{
actions.SendKeys(key1).SendKeys(key2).SendKeys(key3);
}
else
{
actions.SendKeys(key1).SendKeys(key2).SendKeys(key3).SendKeys(key4);
}
actions.Release();
actions.Build().Perform();
});
}
/// <summary>
/// Attaches to an existing PowerToys module.
/// </summary>
@@ -189,5 +231,28 @@ namespace Microsoft.PowerToys.UITest
return this;
}
/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
/// <param name="action">The action to perform on the element.</param>
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 500 ms</param>
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 500 ms</param>
protected void PerformAction(Action<Actions, WindowsDriver<WindowsElement>> action, int msPreAction = 500, int msPostAction = 500)
{
if (msPreAction > 0)
{
Task.Delay(msPreAction).Wait();
}
var windowsDriver = this.WindowsDriver;
Actions actions = new Actions(this.WindowsDriver);
action(actions, windowsDriver);
if (msPostAction > 0)
{
Task.Delay(msPostAction).Wait();
}
}
}
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -19,15 +20,19 @@ namespace Microsoft.PowerToys.UITest
// Default session path is PowerToys settings dashboard
private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
private string? locationPath;
private WindowsDriver<WindowsElement> Root { get; set; }
private WindowsDriver<WindowsElement>? Driver { get; set; }
private Process? appDriver;
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
public SessionHelper(PowerToysModule scope)
{
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
this.locationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var winAppDriverProcessInfo = new ProcessStartInfo
{
@@ -49,17 +54,12 @@ namespace Microsoft.PowerToys.UITest
/// Initializes the test environment.
/// </summary>
/// <param name="scope">The PowerToys module to start.</param>
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
public SessionHelper Init()
{
string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
this.StartExe(path + this.sessionPath);
this.StartExe(locationPath + this.sessionPath);
Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
// Set default timeout to 5 seconds
this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
return this;
}
@@ -68,9 +68,11 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public void Cleanup()
{
ExitScopeExe();
try
{
appDriver?.Kill();
appDriver?.WaitForExit(); // Optional: Wait for the process to exit
}
catch (Exception ex)
{
@@ -87,7 +89,53 @@ namespace Microsoft.PowerToys.UITest
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", appPath);
Console.WriteLine($"appPath: {appPath}");
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
// Set default timeout to 5 seconds
this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Exit a exe.
/// </summary>
/// <param name="path">The path to the application executable.</param>
public void ExitExe(string path)
{
// Exit Exe
string exeName = Path.GetFileNameWithoutExtension(path);
// PowerToys.FancyZonesEditor
Process[] processes = Process.GetProcessesByName(exeName);
foreach (Process process in processes)
{
try
{
process.Kill();
process.WaitForExit(); // Optional: Wait for the process to exit
}
catch (Exception ex)
{
Assert.Fail($"Failed to terminate process {process.ProcessName} (ID: {process.Id}): {ex.Message}");
}
}
}
/// <summary>
/// Exit now exe.
/// </summary>
public void ExitScopeExe()
{
ExitExe(sessionPath);
}
/// <summary>
/// Restarts now exe and takes control of it.
/// </summary>
public void RestartScopeExe()
{
ExitExe(sessionPath);
StartExe(locationPath + sessionPath);
}
public WindowsDriver<WindowsElement> GetRoot() => this.Root;

View File

@@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
@@ -31,11 +32,6 @@ namespace Microsoft.PowerToys.UITest
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
}
~UITestBase()
{
this.sessionHelper.Cleanup();
}
/// <summary>
/// Initializes the test.
/// </summary>
@@ -53,6 +49,15 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// UnInitializes the test.
/// </summary>
[TestCleanup]
public void TestClean()
{
this.sessionHelper.Cleanup();
}
/// <summary>
/// Finds an element by selector.
/// Shortcut for this.Session.Find<T>(by, timeoutMS)
@@ -153,5 +158,24 @@ namespace Microsoft.PowerToys.UITest
{
return this.Session.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Restart scope exe.
/// </summary>
public void RestartScopeExe()
{
this.sessionHelper.RestartScopeExe();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
return;
}
/// <summary>
/// Restart scope exe.
/// </summary>
public void ExitScopeExe()
{
this.sessionHelper.ExitScopeExe();
return;
}
}
}

View File

@@ -89,6 +89,7 @@ namespace powertoys_gpo
const std::wstring POLICY_MWB_DISABLE_USER_DEFINED_IP_MAPPING_RULES = L"MwbDisableUserDefinedIpMappingRules";
const std::wstring POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES = L"MwbPolicyDefinedIpMappingRules";
const std::wstring POLICY_NEW_PLUS_HIDE_TEMPLATE_FILENAME_EXTENSION = L"NewPlusHideTemplateFilenameExtension";
const std::wstring POLICY_NEW_PLUS_REPLACE_VARIABLES = L"NewPlusReplaceVariablesInTemplateFilenames";
// Methods used for reading the registry
#pragma region ReadRegistryMethods
@@ -620,5 +621,11 @@ namespace powertoys_gpo
{
return getConfiguredValue(POLICY_NEW_PLUS_HIDE_TEMPLATE_FILENAME_EXTENSION);
}
inline gpo_rule_configured_t getConfiguredNewPlusReplaceVariablesValue()
{
return getConfiguredValue(POLICY_NEW_PLUS_REPLACE_VARIABLES);
}
#pragma endregion IndividualModuleSettingPolicies
}

View File

@@ -682,5 +682,15 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="NewPlusReplaceVariablesInTemplateFilenames" class="Both" displayName="$(string.NewPlusReplaceVariablesInTemplateFilenames)" explainText="$(string.NewPlusReplaceVariablesInTemplateFilenamesDescription)" key="Software\Policies\PowerToys" valueName="NewPlusReplaceVariablesInTemplateFilenames">
<parentCategory ref="NewPlus" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_89_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
</policies>
</policyDefinitions>

View File

@@ -227,6 +227,14 @@ If you enable this policy, the setting is enabled and the extension is hidden.
If you disable this policy, the setting is disabled and the extension is shown.
If you don't configure this policy, the user takes control over the setting and can enable or disable it.
</string>
<string id="NewPlusReplaceVariablesInTemplateFilenamesDescription">This policy configures if supported variables will get replaced in template filenames.
If you enable this policy, the setting is enabled and supported variables in filenames will get replaced.
If you disable this policy, the setting is disabled and variables in filenames will not get replaced.
If you don't configure this policy, the user will be able to control the setting and can enable or disable it.
</string>
<string id="ConfigureAllUtilityGlobalEnabledState">Configure global utility enabled state</string>
@@ -291,7 +299,7 @@ If you don't configure this policy, the user takes control over the setting and
<string id="MwbPolicyDefinedIpMappingRules">Predefined IP Address mapping rules</string>
<string id="NewPlusHideTemplateFilenameExtension">Hide template filename extension</string>
<string id="AllowDiagnosticData">Allow sending diagnostic data</string>
<string id="ConfigureRunAtStartup">Configure the run at startup setting</string>
<string id="ConfigureRunAtStartup">Configure the run at startup setting</string>
</stringTable>
<presentationTable>

View File

@@ -7,6 +7,7 @@ using System.IO.Abstractions;
using System.Threading;
using Common.UI;
using HostsEditor.Telemetry;
using HostsUILib.Helpers;
using HostsUILib.Settings;
using HostsUILib.ViewModels;
@@ -38,6 +39,8 @@ namespace Hosts
/// </summary>
public App()
{
PowerToysTelemetry.Log.WriteEvent(new HostEditorStartEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{

View File

@@ -2,10 +2,13 @@
// 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 Hosts.Helpers;
using HostsEditor.Telemetry;
using HostsUILib.Helpers;
using HostsUILib.Views;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
@@ -44,6 +47,8 @@ namespace Hosts
Activated += MainWindow_Activated;
MainPage = Host.GetService<HostsMainPage>();
PowerToysTelemetry.Log.WriteEvent(new HostEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)

View File

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

View File

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

View File

@@ -153,7 +153,7 @@ namespace MouseWithoutBorders
string filePath = stringData;
_ = Common.ImpersonateLoggedOnUserAndDoSomething(() =>
_ = Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
if (File.Exists(filePath) || Directory.Exists(filePath))
{
@@ -579,7 +579,7 @@ namespace MouseWithoutBorders
{
if (postAct.Equals("desktop", StringComparison.OrdinalIgnoreCase))
{
_ = ImpersonateLoggedOnUserAndDoSomething(() =>
_ = Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
savingFolder = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\MouseWithoutBorders\\";
@@ -696,7 +696,7 @@ namespace MouseWithoutBorders
Path.GetFileName(fileName),
remoteMachine);
_ = ImpersonateLoggedOnUserAndDoSomething(() =>
_ = Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
ProcessStartInfo startInfo = new();
startInfo.UseShellExecute = true;

View File

@@ -1,277 +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.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
// <summary>
// Keyboard/Mouse hook callback implementation.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
using MouseWithoutBorders.Form;
using Thread = MouseWithoutBorders.Core.Thread;
namespace MouseWithoutBorders
{
internal partial class Common
{
private static readonly DATA KeybdPackage = new();
private static readonly DATA MousePackage = new();
#pragma warning disable SA1307 // Accessible fields should begin with upper-case names
internal static ulong inputEventCount;
internal static ulong invalidPackageCount;
#pragma warning restore SA1307
internal static int MOVE_MOUSE_RELATIVE = 100000;
internal static int XY_BY_PIXEL = 300000;
static Common()
{
}
internal static ulong InvalidPackageCount
{
get => Common.invalidPackageCount;
set => Common.invalidPackageCount = value;
}
internal static ulong InputEventCount
{
get => Common.inputEventCount;
set => Common.inputEventCount = value;
}
internal static ulong RealInputEventCount
{
get;
set;
}
private static Point actualLastPos;
private static int myLastX;
private static int myLastY;
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Dotnet port with style preservation")]
internal static void MouseEvent(MOUSEDATA e, int dx, int dy)
{
try
{
PaintCount = 0;
bool switchByMouseEnabled = IsSwitchingByMouseEnabled();
if (switchByMouseEnabled && Sk != null && (DesMachineID == MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == WM_MOUSEMOVE)
{
Point p = MachineStuff.MoveToMyNeighbourIfNeeded(e.X, e.Y, MachineStuff.desMachineID);
if (!p.IsEmpty)
{
HasSwitchedMachineSinceLastCopy = true;
Logger.LogDebug(string.Format(
CultureInfo.CurrentCulture,
"***** Host Machine: newDesMachineIdEx set = [{0}]. Mouse is now at ({1},{2})",
MachineStuff.newDesMachineIdEx,
e.X,
e.Y));
myLastX = e.X;
myLastY = e.Y;
PrepareToSwitchToMachine(MachineStuff.newDesMachineIdEx, p);
}
}
if (MachineStuff.desMachineID != MachineID && MachineStuff.SwitchLocation.Count <= 0)
{
MousePackage.Des = MachineStuff.desMachineID;
MousePackage.Type = PackageType.Mouse;
MousePackage.Md.dwFlags = e.dwFlags;
MousePackage.Md.WheelDelta = e.WheelDelta;
// Relative move
if (Setting.Values.MoveMouseRelatively && Math.Abs(dx) >= MOVE_MOUSE_RELATIVE && Math.Abs(dy) >= MOVE_MOUSE_RELATIVE)
{
MousePackage.Md.X = dx;
MousePackage.Md.Y = dy;
}
else
{
MousePackage.Md.X = (e.X - MachineStuff.primaryScreenBounds.Left) * 65535 / screenWidth;
MousePackage.Md.Y = (e.Y - MachineStuff.primaryScreenBounds.Top) * 65535 / screenHeight;
}
SkSend(MousePackage, null, false);
if (MousePackage.Md.dwFlags is WM_LBUTTONUP or WM_RBUTTONUP)
{
Thread.Sleep(10);
}
NativeMethods.GetCursorPos(ref actualLastPos);
if (actualLastPos != Common.LastPos)
{
Logger.LogDebug($"Mouse cursor has moved unexpectedly: Expected: {Common.LastPos}, actual: {actualLastPos}.");
Common.LastPos = actualLastPos;
}
}
#if SHOW_ON_WINLOGON_EX
if (RunOnLogonDesktop && e.dwFlags == WM_RBUTTONUP &&
desMachineID == machineID &&
e.x > 2 && e.x < 100 && e.y > 2 && e.y < 20)
{
DoSomethingInUIThread(delegate()
{
MainForm.HideMenuWhenRunOnLogonDesktop();
MainForm.MainMenu.Hide();
MainForm.MainMenu.Show(e.x - 5, e.y - 3);
});
}
#endif
}
catch (Exception ex)
{
Logger.Log(ex);
}
}
internal static bool IsSwitchingByMouseEnabled()
{
return (EasyMouseOption)Setting.Values.EasyMouse == EasyMouseOption.Enable || InputHook.EasyMouseKeyDown;
}
internal static void PrepareToSwitchToMachine(ID newDesMachineID, Point desMachineXY)
{
Logger.LogDebug($"PrepareToSwitchToMachine: newDesMachineID = {newDesMachineID}, desMachineXY = {desMachineXY}");
if (((GetTick() - MachineStuff.lastJump < 100) && (GetTick() - MachineStuff.lastJump > 0)) || MachineStuff.desMachineID == ID.ALL)
{
Logger.LogDebug("PrepareToSwitchToMachine: lastJump");
return;
}
MachineStuff.lastJump = GetTick();
string newDesMachineName = MachineStuff.NameFromID(newDesMachineID);
if (!IsConnectedTo(newDesMachineID))
{// Connection lost, cancel switching
Logger.LogDebug("No active connection found for " + newDesMachineName);
// ShowToolTip("No active connection found for [" + newDesMachineName + "]!", 500);
}
else
{
MachineStuff.newDesMachineID = newDesMachineID;
MachineStuff.SwitchLocation.X = desMachineXY.X;
MachineStuff.SwitchLocation.Y = desMachineXY.Y;
MachineStuff.SwitchLocation.ResetCount();
_ = EvSwitch.Set();
// PostMessage(mainForm.Handle, WM_SWITCH, IntPtr.Zero, IntPtr.Zero);
if (newDesMachineID != DragDrop.DragMachine)
{
if (!DragDrop.IsDragging && !DragDrop.IsDropping)
{
if (DragDrop.MouseDown && !RunOnLogonDesktop && !RunOnScrSaverDesktop)
{
DragDrop.DragDropStep02();
}
}
else if (DragDrop.DragMachine != (ID)1)
{
DragDrop.ChangeDropMachine();
}
}
else
{
DragDrop.DragDropStep11();
}
// Change des machine
if (MachineStuff.desMachineID != newDesMachineID)
{
Logger.LogDebug("MouseEvent: Switching to new machine:" + newDesMachineName);
// Ask current machine to hide the Mouse cursor
if (newDesMachineID != ID.ALL && MachineStuff.desMachineID != MachineID)
{
SendPackage(MachineStuff.desMachineID, PackageType.HideMouse);
}
DesMachineID = newDesMachineID;
if (MachineStuff.desMachineID == MachineID)
{
if (GetTick() - clipboardCopiedTime < BIG_CLIPBOARD_DATA_TIMEOUT)
{
clipboardCopiedTime = 0;
Common.GetRemoteClipboard("PrepareToSwitchToMachine");
}
}
else
{
// Ask the new active machine to get clipboard data (if the data is too big)
SendPackage(MachineStuff.desMachineID, PackageType.MachineSwitched);
}
_ = Interlocked.Increment(ref switchCount);
}
}
}
internal static void SaveSwitchCount()
{
if (SwitchCount > 0)
{
_ = Task.Run(() =>
{
Setting.Values.SwitchCount += SwitchCount;
_ = Interlocked.Exchange(ref switchCount, 0);
});
}
}
internal static void KeybdEvent(KEYBDDATA e)
{
try
{
PaintCount = 0;
if (MachineStuff.desMachineID != MachineStuff.newDesMachineID)
{
Logger.LogDebug("KeybdEvent: Switching to new machine...");
DesMachineID = MachineStuff.newDesMachineID;
}
if (MachineStuff.desMachineID != MachineID)
{
KeybdPackage.Des = MachineStuff.desMachineID;
KeybdPackage.Type = PackageType.Keyboard;
KeybdPackage.Kd = e;
KeybdPackage.DateTime = GetTick();
SkSend(KeybdPackage, null, false);
if (KeybdPackage.Kd.dwFlags is WM_KEYUP or WM_SYSKEYUP)
{
Thread.Sleep(10);
}
}
}
catch (Exception ex)
{
Logger.Log(ex);
}
}
}
}

View File

@@ -1,526 +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.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Windows.Forms;
// <summary>
// Some other helper methods.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
using Microsoft.Win32;
using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
using static System.Windows.Forms.Control;
namespace MouseWithoutBorders
{
internal partial class Common
{
internal const string HELPER_FORM_TEXT = "Mouse without Borders Helper";
internal const string HelperProcessName = "PowerToys.MouseWithoutBordersHelper";
private static bool signalHelperToExit;
private static bool signalWatchDogToExit;
internal static long WndProcCounter;
private static void WatchDogThread()
{
long oldCounter = WndProcCounter;
do
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
if (signalWatchDogToExit)
{
break;
}
}
while (BlockingUI)
{
Thread.Sleep(1000);
}
if (WndProcCounter == oldCounter)
{
Process p = Process.GetCurrentProcess();
string procInfo = $"{p.PrivateMemorySize64 / 1024 / 1024}MB, {p.TotalProcessorTime}, {Environment.ProcessorCount}.";
string threadStacks = $"{procInfo} {Thread.DumpThreadsStack()}";
Logger.TelemetryLogTrace(threadStacks, SeverityLevel.Error);
break;
}
oldCounter = WndProcCounter;
}
while (true);
}
private static void HelperThread()
{
// SuppressFlow fixes an issue on service mode, where the helper process can't get enough permissions to be started again.
// More details can be found on: https://github.com/microsoft/PowerToys/pull/36892
using var asyncFlowControl = System.Threading.ExecutionContext.SuppressFlow();
try
{
while (true)
{
_ = EvSwitch.WaitOne(); // Switching to another machine?
if (signalHelperToExit)
{
break;
}
if (MachineStuff.NewDesMachineID != Common.MachineID && MachineStuff.NewDesMachineID != ID.ALL)
{
HideMouseCursor(false);
Common.MainFormDotEx(true);
}
else
{
if (MachineStuff.SwitchLocation.Count > 0)
{
MachineStuff.SwitchLocation.Count--;
// When we want to move mouse by pixels, we add 300k to x and y (search for XY_BY_PIXEL for other related code).
Logger.LogDebug($"+++++ Moving mouse to {MachineStuff.SwitchLocation.X}, {MachineStuff.SwitchLocation.Y}");
// MaxXY = 65535 so 100k is safe.
if (MachineStuff.SwitchLocation.X > XY_BY_PIXEL - 100000 || MachineStuff.SwitchLocation.Y > XY_BY_PIXEL - 100000)
{
InputSimulation.MoveMouse(MachineStuff.SwitchLocation.X - XY_BY_PIXEL, MachineStuff.SwitchLocation.Y - XY_BY_PIXEL);
}
else
{
InputSimulation.MoveMouseEx(MachineStuff.SwitchLocation.X, MachineStuff.SwitchLocation.Y);
}
Common.MainFormDot();
}
}
if (MachineStuff.NewDesMachineID == Common.MachineID)
{
ReleaseAllKeys();
}
}
}
catch (Exception e)
{
Logger.Log(e);
}
signalHelperToExit = false;
Logger.LogDebug("^^^Helper Thread exiting...^^^");
}
internal static void MainFormDotEx(bool bCheckTS)
{
Logger.LogDebug("***** MainFormDotEx:");
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop)
{
int left = MachineStuff.PrimaryScreenBounds.Left + ((MachineStuff.PrimaryScreenBounds.Right - MachineStuff.PrimaryScreenBounds.Left) / 2) - 1;
int top = Setting.Values.HideMouse ? 3 : MachineStuff.PrimaryScreenBounds.Top + ((MachineStuff.PrimaryScreenBounds.Bottom - MachineStuff.PrimaryScreenBounds.Top) / 2);
Common.MainFormVisible = true;
if (Setting.Values.HideMouse && Setting.Values.StealFocusWhenSwitchingMachine && Common.SendMessageToHelper(0x407, new IntPtr(left), new IntPtr(top), true) == 0)
{
try
{
/* When user just switches to the Logon desktop, user is actually on the "Windows Default Lock Screen" (LockApp).
* If a click is sent to this during switch, it actually triggers a desktop switch on the local machine causing a reconnection affecting the machine switch.
* We can detect and skip in this case.
* */
IntPtr foreGroundWindow = NativeMethods.GetForegroundWindow();
string foreGroundWindowText = GetText(foreGroundWindow);
bool mouseClick = true;
if (foreGroundWindowText.Equals("Windows Default Lock Screen", StringComparison.OrdinalIgnoreCase))
{
mouseClick = false;
}
// Window title may be localized, check process name:
if (mouseClick)
{
_ = NativeMethods.GetWindowThreadProcessId(foreGroundWindow, out uint pid);
if (pid > 0)
{
string foreGroundWindowProcess = Process.GetProcessById((int)pid)?.ProcessName;
if (foreGroundWindowProcess.Equals("LockApp", StringComparison.OrdinalIgnoreCase))
{
mouseClick = false;
}
}
}
if (mouseClick)
{
InputSimulation.MouseClickDotForm(left + 1, top + 1);
}
}
catch (Exception e)
{
Logger.Log(e);
}
}
}
CustomCursor.ShowFakeMouseCursor(int.MinValue, int.MinValue);
}
internal static void MainForm3Pixels()
{
Logger.LogDebug("***** MainFormDotLarge:");
DoSomethingInUIThread(
() =>
{
MainForm.Left = MachineStuff.PrimaryScreenBounds.Left + ((MachineStuff.PrimaryScreenBounds.Right - MachineStuff.PrimaryScreenBounds.Left) / 2) - 2;
MainForm.Top = Setting.Values.HideMouse ? 3 : MachineStuff.PrimaryScreenBounds.Top + ((MachineStuff.PrimaryScreenBounds.Bottom - MachineStuff.PrimaryScreenBounds.Top) / 2) - 1;
MainForm.Width = 3;
MainForm.Height = 3;
MainForm.Opacity = 0.11D;
MainForm.TopMost = true;
if (Setting.Values.HideMouse)
{
MainForm.BackColor = Color.Black;
MainForm.Show();
Common.MainFormVisible = true;
}
else
{
MainForm.BackColor = Color.White;
MainForm.Hide();
Common.MainFormVisible = false;
}
},
true);
CustomCursor.ShowFakeMouseCursor(int.MinValue, int.MinValue);
}
internal static void MainFormDot()
{
DoSomethingInUIThread(
() =>
{
_ = Common.SendMessageToHelper(0x408, IntPtr.Zero, IntPtr.Zero, false);
MainForm.Left = MachineStuff.PrimaryScreenBounds.Left + ((MachineStuff.PrimaryScreenBounds.Right - MachineStuff.PrimaryScreenBounds.Left) / 2) - 1;
MainForm.Top = Setting.Values.HideMouse ? 3 : MachineStuff.PrimaryScreenBounds.Top + ((MachineStuff.PrimaryScreenBounds.Bottom - MachineStuff.PrimaryScreenBounds.Top) / 2);
MainForm.Width = 1;
MainForm.Height = 1;
MainForm.Opacity = 0.15;
MainForm.Hide();
Common.MainFormVisible = false;
},
true);
CustomCursor.ShowFakeMouseCursor(int.MinValue, int.MinValue);
}
internal static void ToggleIcon()
{
try
{
if (toggleIconsIndex < TOGGLE_ICONS_SIZE)
{
Common.DoSomethingInUIThread(() => Common.MainForm.ChangeIcon(toggleIcons[toggleIconsIndex++]));
}
else
{
toggleIconsIndex = 0;
toggleIcons = null;
}
}
catch (Exception e)
{
Logger.Log(e);
}
}
internal static void RunDDHelper(bool cleanUp = false)
{
if (Common.RunOnLogonDesktop || Common.RunOnScrSaverDesktop)
{
return;
}
if (cleanUp)
{
try
{
Process[] ps = Process.GetProcessesByName(HelperProcessName);
foreach (Process p in ps)
{
p.KillProcess();
}
}
catch (Exception e)
{
Logger.Log(e);
_ = Common.SendMessageToHelper(SharedConst.QUIT_CMD, IntPtr.Zero, IntPtr.Zero);
}
return;
}
if (!Common.IsMyDesktopActive())
{
return;
}
if (!Common.IpcChannelCreated)
{
Logger.TelemetryLogTrace($"{nameof(Common.IpcChannelCreated)} = {Common.IpcChannelCreated}. {Logger.GetStackTrace(new StackTrace())}", SeverityLevel.Warning);
return;
}
if (!MainForm.IsDisposed)
{
MainForm.NotifyIcon.Visible = false;
MainForm.NotifyIcon.Visible = Setting.Values.ShowOriginalUI;
}
IntPtr h = (IntPtr)NativeMethods.FindWindow(null, Common.HELPER_FORM_TEXT);
if (h.ToInt32() <= 0)
{
_ = Common.CreateProcessInInputDesktopSession(
$"\"{Path.GetDirectoryName(Application.ExecutablePath)}\\{HelperProcessName}.exe\"",
string.Empty,
Common.GetInputDesktop(),
0);
HasSwitchedMachineSinceLastCopy = true;
// Common.CreateLowIntegrityProcess("\"" + Path.GetDirectoryName(Application.ExecutablePath) + "\\MouseWithoutBordersHelper.exe\"", string.Empty, 0, false, 0);
var processes = Process.GetProcessesByName(HelperProcessName);
if (processes?.Length == 0)
{
Logger.Log("Unable to start helper process.");
Common.ShowToolTip("Error starting Mouse Without Borders Helper, clipboard sharing will not work!", 5000, ToolTipIcon.Error);
}
else
{
Logger.Log("Helper process started.");
}
}
else
{
var processes = Process.GetProcessesByName(HelperProcessName);
if (processes?.Length > 0)
{
Logger.Log("Helper process found running.");
}
else
{
Logger.Log("Invalid helper process found running.");
Common.ShowToolTip("Error finding Mouse Without Borders Helper, clipboard sharing will not work!", 5000, ToolTipIcon.Error);
}
}
}
internal static int SendMessageToHelper(int msg, IntPtr wparam, IntPtr lparam, bool wait = true, bool log = true)
{
int h = NativeMethods.FindWindow(null, Common.HELPER_FORM_TEXT);
int rv = -1;
if (h > 0)
{
rv = wait
? (int)NativeMethods.SendMessage((IntPtr)h, msg, wparam, lparam)
: NativeMethods.PostMessage((IntPtr)h, msg, wparam, lparam) ? 1 : 0;
}
if (log)
{
Logger.LogDebug($"SendMessageToHelper: HelperWindow={h}, Return={rv}, msg={msg}, w={wparam.ToInt32()}, l={lparam.ToInt32()}, Post={!wait}");
}
return rv;
}
internal static bool IsWindows8AndUp()
{
return (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor >= 2)
|| Environment.OSVersion.Version.Major > 6;
}
internal static string GetMiniLog(IEnumerable<ControlCollection> optionControls)
{
string log = string.Empty;
log += "=============================================================================================================================\r\n";
log += $"{Application.ProductName} version {Application.ProductVersion}\r\n";
log += $"{Setting.Values.Username}/{GetDebugInfo(MyKey)}\r\n";
log += $"{MachineName}/{MachineID}/{DesMachineID}\r\n";
log += $"Id: {Setting.Values.DeviceId}\r\n";
log += $"Matrix: {string.Join(",", MachineStuff.MachineMatrix)}\r\n";
log += $"McPool: {Setting.Values.MachinePoolString}\r\n";
log += "\r\nOPTIONS:\r\n";
foreach (ControlCollection controlCollection in optionControls)
{
foreach (object c in controlCollection)
{
if (c is CheckBox checkBox)
{
log += $"({(checkBox.Checked ? 1 : 0)}) {checkBox.Text}\r\n";
continue;
}
if (c is RadioButton radioButton)
{
log += $"({(radioButton.Checked ? 1 : 0)}) {radioButton.Name}.[{radioButton.Text}]\r\n";
continue;
}
if (c is ComboBox comboBox)
{
log += $"{comboBox.Name} = {comboBox.Text}\r\n";
continue;
}
}
}
log += "\r\n";
SocketStuff sk = Sk;
if (sk?.TcpSockets != null)
{
foreach (TcpSk tcp in sk.TcpSockets)
{
log += $"{Common.MachineName}{(tcp.IsClient ? "=>" : "<=")}{tcp.MachineName}({tcp.MachineId}):{tcp.Status}\r\n";
}
}
log += string.Format(CultureInfo.CurrentCulture, "Helper:{0}\r\n", SendMessageToHelper(0x400, IntPtr.Zero, IntPtr.Zero));
log += Setting.Values.LastPersonalizeLogonScr + "\r\n";
log += "Name2IP =\r\n" + Setting.Values.Name2IP + "\r\n";
log += "Last 10 trace messages:\r\n";
log += string.Join(Environment.NewLine, Logger.LogCounter.Select(item => $"({item.Value}): {item.Key}").Take(10));
log += "\r\n=============================================================================================================================";
return log;
}
internal static bool GetUserName()
{
if (string.IsNullOrEmpty(Setting.Values.Username) && !Common.RunOnLogonDesktop)
{
if (Program.User.Contains("system", StringComparison.CurrentCultureIgnoreCase))
{
_ = Common.ImpersonateLoggedOnUserAndDoSomething(() =>
{
// See: https://stackoverflow.com/questions/19487541/how-to-get-windows-user-name-from-sessionid
static string GetUsernameBySessionId(int sessionId)
{
string username = "SYSTEM";
if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTSInfoClass.WTSUserName, out nint buffer, out int strLen) && strLen > 1)
{
username = Marshal.PtrToStringAnsi(buffer);
NativeMethods.WTSFreeMemory(buffer);
if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTSInfoClass.WTSDomainName, out buffer, out strLen) && strLen > 1)
{
username = @$"{Marshal.PtrToStringAnsi(buffer)}\{username}";
NativeMethods.WTSFreeMemory(buffer);
}
}
return username;
}
// The most direct way to fetch the username is WindowsIdentity.GetCurrent(true).Name
// but GetUserName can run within an ExecutionContext.SuppressFlow block, which creates issues
// with WindowsIdentity.GetCurrent.
// See: https://stackoverflow.com/questions/76998988/exception-when-using-executioncontext-suppressflow-in-net-7
// So we use WTSQuerySessionInformation as a workaround.
Setting.Values.Username = GetUsernameBySessionId(Process.GetCurrentProcess().SessionId);
});
}
else
{
Setting.Values.Username = Program.User;
}
Logger.LogDebug("[Username] = " + Setting.Values.Username);
}
return !string.IsNullOrEmpty(Setting.Values.Username);
}
internal static void ShowOneWayModeMessage()
{
ToggleShowTopMostMessage(
@"
Due to Security Controls, a remote device cannot control a SAW device.
Please use the keyboard and Mouse from the SAW device.
(Press Esc to hide this message)
",
string.Empty,
10);
}
internal static void ApplyCADSetting()
{
try
{
if (Setting.Values.DisableCAD)
{
RegistryKey k = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System");
if (k != null)
{
k.SetValue("DisableCAD", 1, RegistryValueKind.DWord);
k.Close();
}
}
else
{
RegistryKey k = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System");
if (k != null)
{
k.SetValue("DisableCAD", 0, RegistryValueKind.DWord);
k.Close();
}
}
}
catch (Exception e)
{
Logger.Log(e);
}
}
}
}

View File

@@ -93,7 +93,7 @@ namespace MouseWithoutBorders
internal static void Init()
{
_ = Common.GetUserName();
_ = Helper.GetUserName();
Common.GeneratedKey = true;
try
@@ -148,7 +148,7 @@ namespace MouseWithoutBorders
private static void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
Common.WndProcCounter++;
Helper.WndProcCounter++;
if (e.Mode is PowerModes.Resume or PowerModes.Suspend)
{
@@ -167,21 +167,21 @@ namespace MouseWithoutBorders
watchDogThread.Start();
*/
helper = new Thread(new ThreadStart(HelperThread), "Helper Thread");
helper = new Thread(new ThreadStart(Helper.HelperThread), "Helper Thread");
helper.SetApartmentState(ApartmentState.STA);
helper.Start();
}
private static void AskHelperThreadsToExit(int waitTime)
{
signalHelperToExit = true;
signalWatchDogToExit = true;
Helper.signalHelperToExit = true;
Helper.signalWatchDogToExit = true;
_ = EvSwitch.Set();
int c = 0;
if (helper != null && c < waitTime)
{
while (signalHelperToExit)
while (Helper.signalHelperToExit)
{
Thread.Sleep(1);
}
@@ -251,7 +251,7 @@ namespace MouseWithoutBorders
private static void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
Logger.LogDebug("NetworkAvailabilityEventArgs.IsAvailable: " + e.IsAvailable.ToString(CultureInfo.InvariantCulture));
Common.WndProcCounter++;
Helper.WndProcCounter++;
ScheduleReopenSocketsDueToNetworkChanges(!e.IsAvailable);
}

View File

@@ -1,314 +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.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Principal;
// <summary>
// Impersonation.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
namespace MouseWithoutBorders
{
internal partial class Common
{
internal static bool RunElevated()
{
return WindowsIdentity.GetCurrent().Owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid);
}
internal static bool ImpersonateLoggedOnUserAndDoSomething(Action targetFunc)
{
if (Common.RunWithNoAdminRight)
{
targetFunc();
return true;
}
else
{
// SuppressFlow fixes an issue on service mode, where WTSQueryUserToken runs successfully once and then fails
// on subsequent calls. The reason appears to be an unknown issue with reverting the impersonation,
// meaning that subsequent impersonation attempts run as the logged-on user and fail.
// This is a workaround.
using var asyncFlowControl = System.Threading.ExecutionContext.SuppressFlow();
uint dwSessionId;
IntPtr hUserToken = IntPtr.Zero, hUserTokenDup = IntPtr.Zero;
try
{
dwSessionId = (uint)Process.GetCurrentProcess().SessionId;
uint rv = NativeMethods.WTSQueryUserToken(dwSessionId, ref hUserToken);
var lastError = rv == 0 ? Marshal.GetLastWin32Error() : 0;
Logger.LogDebug($"{nameof(NativeMethods.WTSQueryUserToken)} returned {rv.ToString(CultureInfo.CurrentCulture)}");
if (rv == 0)
{
Logger.Log($"{nameof(NativeMethods.WTSQueryUserToken)} failed with: {lastError}.");
return false;
}
if (!NativeMethods.DuplicateToken(hUserToken, (int)NativeMethods.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, ref hUserTokenDup))
{
Logger.TelemetryLogTrace($"{nameof(NativeMethods.DuplicateToken)} Failed! {Logger.GetStackTrace(new StackTrace())}", SeverityLevel.Warning);
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return false;
}
if (NativeMethods.ImpersonateLoggedOnUser(hUserTokenDup))
{
targetFunc();
_ = NativeMethods.RevertToSelf();
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return true;
}
else
{
Logger.Log("ImpersonateLoggedOnUser Failed!");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return false;
}
}
catch (Exception e)
{
Logger.Log(e);
return false;
}
}
}
internal static int CreateProcessInInputDesktopSession(string commandLine, string arg, string desktop, short wShowWindow, bool lowIntegrity = false)
// As user who runs explorer.exe
{
if (!Program.User.Contains("system", StringComparison.InvariantCultureIgnoreCase))
{
ProcessStartInfo s = new(commandLine, arg);
s.WindowStyle = wShowWindow != 0 ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden;
Process p = Process.Start(s);
return p == null ? 0 : p.Id;
}
string commandLineWithArg = commandLine + " " + arg;
int lastError;
int dwSessionId;
IntPtr hUserToken = IntPtr.Zero, hUserTokenDup = IntPtr.Zero;
Logger.LogDebug("CreateProcessInInputDesktopSession called, launching " + commandLineWithArg + " on " + desktop);
try
{
dwSessionId = Process.GetCurrentProcess().SessionId;
// Get the user token used by DuplicateTokenEx
lastError = (int)NativeMethods.WTSQueryUserToken((uint)dwSessionId, ref hUserToken);
NativeMethods.STARTUPINFO si = default;
si.cb = Marshal.SizeOf(si);
si.lpDesktop = "winsta0\\" + desktop;
si.wShowWindow = wShowWindow;
NativeMethods.SECURITY_ATTRIBUTES sa = default;
sa.Length = Marshal.SizeOf(sa);
if (!NativeMethods.DuplicateTokenEx(hUserToken, NativeMethods.MAXIMUM_ALLOWED, ref sa, (int)NativeMethods.SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)NativeMethods.TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
{
lastError = Marshal.GetLastWin32Error();
Logger.Log(string.Format(CultureInfo.CurrentCulture, "DuplicateTokenEx error: {0} Token does not have the privilege.", lastError));
_ = NativeMethods.CloseHandle(hUserToken);
return 0;
}
if (lowIntegrity)
{
NativeMethods.TOKEN_MANDATORY_LABEL tIL;
// Low
string sIntegritySid = "S-1-16-4096";
bool rv = NativeMethods.ConvertStringSidToSid(sIntegritySid, out IntPtr pIntegritySid);
if (!rv)
{
Logger.Log("ConvertStringSidToSid failed");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return 0;
}
tIL.Label.Attributes = NativeMethods.SE_GROUP_INTEGRITY;
tIL.Label.Sid = pIntegritySid;
rv = NativeMethods.SetTokenInformation(hUserTokenDup, NativeMethods.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, ref tIL, (uint)Marshal.SizeOf(tIL) + (uint)IntPtr.Size);
if (!rv)
{
Logger.Log("SetTokenInformation failed");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return 0;
}
}
uint dwCreationFlags = NativeMethods.NORMAL_PRIORITY_CLASS | NativeMethods.CREATE_NEW_CONSOLE;
IntPtr pEnv = IntPtr.Zero;
if (NativeMethods.CreateEnvironmentBlock(ref pEnv, hUserTokenDup, true))
{
dwCreationFlags |= NativeMethods.CREATE_UNICODE_ENVIRONMENT;
}
else
{
pEnv = IntPtr.Zero;
}
_ = NativeMethods.CreateProcessAsUser(
hUserTokenDup, // client's access token
null, // file to execute
commandLineWithArg, // command line
ref sa, // pointer to process SECURITY_ATTRIBUTES
ref sa, // pointer to thread SECURITY_ATTRIBUTES
false, // handles are not inheritable
(int)dwCreationFlags, // creation flags
pEnv, // pointer to new environment block
null, // name of current directory
ref si, // pointer to STARTUPINFO structure
out NativeMethods.PROCESS_INFORMATION pi); // receives information about new process
// GetLastError should be 0
int iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
Logger.LogDebug("CreateProcessAsUser returned " + iResultOfCreateProcessAsUser.ToString(CultureInfo.CurrentCulture));
// Close handles task
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return (iResultOfCreateProcessAsUser == 0) ? (int)pi.dwProcessId : 0;
}
catch (Exception e)
{
Logger.Log(e);
return 0;
}
}
#if CUSTOMIZE_LOGON_SCREEN
internal static bool CreateLowIntegrityProcess(string commandLine, string args, int wait, bool killIfTimedOut, long limitedMem, short wShowWindow = 0)
{
int processId = CreateProcessInInputDesktopSession(commandLine, args, "default", wShowWindow, true);
if (processId <= 0)
{
return false;
}
if (wait > 0)
{
if (limitedMem > 0)
{
int sec = 0;
while (true)
{
Process p;
try
{
if ((p = Process.GetProcessById(processId)) == null)
{
Logger.Log("Process exited!");
break;
}
}
catch (ArgumentException)
{
Logger.Log("GetProcessById.ArgumentException");
break;
}
if ((!p.HasExited && p.PrivateMemorySize64 > limitedMem) || (++sec > (wait / 1000)))
{
Logger.Log(string.Format(CultureInfo.CurrentCulture, "Process log (mem): {0}, {1}", sec, p.PrivateMemorySize64));
return false;
}
Thread.Sleep(1000);
}
}
else
{
Process p;
if ((p = Process.GetProcessById(processId)) == null)
{
Logger.Log("Process exited!");
}
else if (NativeMethods.WaitForSingleObject(p.Handle, wait) != NativeMethods.WAIT_OBJECT_0 && killIfTimedOut)
{
Logger.Log("Process log (time).");
TerminateProcessTree(p.Handle, (uint)processId, -1);
return false;
}
}
}
return true;
}
internal static void TerminateProcessTree(IntPtr hProcess, uint processID, int exitCode)
{
if (processID > 0 && hProcess.ToInt32() > 0)
{
Process[] processes = Process.GetProcesses();
int dwSessionId = Process.GetCurrentProcess().SessionId;
foreach (Process p in processes)
{
if (p.SessionId == dwSessionId)
{
NativeMethods.PROCESS_BASIC_INFORMATION processBasicInformation = default;
try
{
if (NativeMethods.NtQueryInformationProcess(p.Handle, 0, ref processBasicInformation, (uint)Marshal.SizeOf(processBasicInformation), out uint bytesWritten) >= 0)
{// NT_SUCCESS(...)
if (processBasicInformation.InheritedFromUniqueProcessId == processID)
{
TerminateProcessTree(p.Handle, processBasicInformation.UniqueProcessId, exitCode);
}
}
}
catch (InvalidOperationException e)
{
Logger.Log(e);
continue;
}
catch (Win32Exception e)
{
Logger.Log(e);
continue;
}
}
}
_ = NativeMethods.TerminateProcess(hProcess, (IntPtr)exitCode);
}
}
#endif
}
}

View File

@@ -1,161 +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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.ServiceProcess;
using System.Threading.Tasks;
using System.Windows.Forms;
// <summary>
// Service control code.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
[module: SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions", Scope = "member", Target = "MouseWithoutBorders.Common.#StartMouseWithoutBordersService()", Justification = "Dotnet port with style preservation")]
namespace MouseWithoutBorders
{
internal partial class Common
{
private static bool shownErrMessage;
private static DateTime lastStartServiceTime = DateTime.UtcNow;
internal static void StartMouseWithoutBordersService(string desktopToRunMouseWithoutBordersOn = null, string startTag1 = "byapp", string startTag2 = null)
{
// NOTE(@yuyoyuppe): the new flow assumes we run both mwb processes directly from the svc.
if (Common.RunWithNoAdminRight || true)
{
return;
}
Logger.Log($"{nameof(StartMouseWithoutBordersService)}: {Logger.GetStackTrace(new StackTrace())}.");
Task task = Task.Run(() =>
{
Process[] ps = Process.GetProcessesByName("MouseWithoutBordersSvc");
if (ps.Length != 0)
{
if (DateTime.UtcNow - lastStartServiceTime < TimeSpan.FromSeconds(5))
{
Logger.Log($"{nameof(StartMouseWithoutBordersService)}: Aborted.");
return;
}
foreach (Process pp in ps)
{
Logger.Log(string.Format(CultureInfo.InvariantCulture, "Killing process MouseWithoutBordersSvc {0}.", pp.Id));
pp.KillProcess();
}
}
lastStartServiceTime = DateTime.UtcNow;
ServiceController service = new("MouseWithoutBordersSvc");
try
{
Logger.Log("Starting " + service.ServiceName);
}
catch (Exception)
{
if (!shownErrMessage)
{
shownErrMessage = true;
_ = MessageBox.Show(
Application.ProductName + " is not installed yet, please run Setup.exe first!",
Application.ProductName,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return;
}
try
{
int c = 0;
while (service.Status != ServiceControllerStatus.Stopped && c++ < 5)
{
Thread.Sleep(1000);
service = new ServiceController("MouseWithoutBordersSvc");
}
if (string.IsNullOrWhiteSpace(desktopToRunMouseWithoutBordersOn))
{
startTag2 ??= Process.GetCurrentProcess().SessionId.ToString(CultureInfo.InvariantCulture);
service.Start(new string[] { startTag1, startTag2 });
}
else
{
service.Start(new string[] { desktopToRunMouseWithoutBordersOn });
}
}
catch (Exception e)
{
Logger.Log(e);
// ERROR_SERVICE_ALREADY_RUNNING
if (!(shownErrMessage || ((e?.InnerException as Win32Exception)?.NativeErrorCode == 1056)))
{
shownErrMessage = true;
_ = MessageBox.Show(
"Cannot start service " + service.ServiceName + ": " + e.Message,
Common.BinaryName,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return;
}
});
// Wait for the task while not blocking the UI thread.
do
{
MMSleep(1);
if (task.IsCanceled || task.IsCompleted || task.IsFaulted)
{
break;
}
}
while (true);
}
internal static void StartServiceAndSendLogoffSignal()
{
try
{
Process[] p = Process.GetProcessesByName("winlogon");
Process me = Process.GetCurrentProcess();
string myWinlogon = p?.FirstOrDefault(item => item.SessionId == me.SessionId)?.Id.ToString(CultureInfo.InvariantCulture) ?? null;
if (string.IsNullOrWhiteSpace(myWinlogon))
{
StartMouseWithoutBordersService(null, "logoff");
}
else
{
StartMouseWithoutBordersService(null, "logoff", myWinlogon);
}
}
catch (Exception e)
{
Logger.Log($"{nameof(StartServiceAndSendLogoffSignal)}: {e.Message}");
}
}
}
}

View File

@@ -269,7 +269,7 @@ namespace MouseWithoutBorders
if (!Common.RunWithNoAdminRight)
{
Logger.LogDebug("*** Starting on active Desktop: " + desktopToRunMouseWithoutBordersOn);
StartMouseWithoutBordersService(desktopToRunMouseWithoutBordersOn);
Service.StartMouseWithoutBordersService(desktopToRunMouseWithoutBordersOn);
}
}
@@ -279,7 +279,7 @@ namespace MouseWithoutBorders
{
if (!IsMyDesktopActive() || Common.CurrentProcess.SessionId != NativeMethods.WTSGetActiveConsoleSessionId())
{
Common.RunDDHelper(true);
Helper.RunDDHelper(true);
int waitCount = 20;
while (NativeMethods.WTSGetActiveConsoleSessionId() == 0xFFFFFFFF && waitCount > 0)

View File

@@ -101,8 +101,10 @@ namespace MouseWithoutBorders
private static bool runOnLogonDesktop;
private static bool runOnScrSaverDesktop;
private static int[] toggleIcons;
private static int toggleIconsIndex;
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
internal static int[] toggleIcons;
internal static int toggleIconsIndex;
#pragma warning restore SA1307
internal const int TOGGLE_ICONS_SIZE = 4;
internal const int ICON_ONE = 0;
internal const int ICON_ALL = 1;
@@ -114,7 +116,9 @@ namespace MouseWithoutBorders
internal const int NETWORK_STREAM_BUF_SIZE = 1024 * 1024;
internal static readonly EventWaitHandle EvSwitch = new(false, EventResetMode.AutoReset);
private static Point lastPos;
private static int switchCount;
#pragma warning disable SA1307 // Accessible fields should begin with upper-case names
internal static int switchCount;
#pragma warning restore SA1307
private static long lastReconnectByHotKeyTime;
private static int tcpPort;
private static bool secondOpenSocketTry;
@@ -543,7 +547,7 @@ namespace MouseWithoutBorders
internal static void SendAwakeBeat()
{
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && Common.IsMyDesktopActive() &&
Setting.Values.BlockScreenSaver && lastRealInputEventCount != Common.RealInputEventCount)
Setting.Values.BlockScreenSaver && lastRealInputEventCount != Event.RealInputEventCount)
{
SendPackage(ID.ALL, PackageType.Awake);
}
@@ -552,13 +556,13 @@ namespace MouseWithoutBorders
SendHeartBeat();
}
lastInputEventCount = Common.InputEventCount;
lastRealInputEventCount = Common.RealInputEventCount;
lastInputEventCount = Event.InputEventCount;
lastRealInputEventCount = Event.RealInputEventCount;
}
internal static void HumanBeingDetected()
{
if (lastInputEventCount == Common.InputEventCount)
if (lastInputEventCount == Event.InputEventCount)
{
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && Common.IsMyDesktopActive())
{
@@ -566,7 +570,7 @@ namespace MouseWithoutBorders
}
}
lastInputEventCount = Common.InputEventCount;
lastInputEventCount = Event.InputEventCount;
}
private static void PokeMyself()
@@ -581,7 +585,7 @@ namespace MouseWithoutBorders
InputSimulation.MoveMouseRelative(-x, -y);
Thread.Sleep(50);
if (lastInputEventCount != Common.InputEventCount)
if (lastInputEventCount != Event.InputEventCount)
{
break;
}
@@ -590,8 +594,8 @@ namespace MouseWithoutBorders
internal static void InitLastInputEventCount()
{
lastInputEventCount = Common.InputEventCount;
lastRealInputEventCount = Common.RealInputEventCount;
lastInputEventCount = Event.InputEventCount;
lastRealInputEventCount = Event.RealInputEventCount;
}
internal static void SendHello()
@@ -665,7 +669,7 @@ namespace MouseWithoutBorders
{
Common.DoSomethingInUIThread(() =>
{
if (!DragDrop.MouseDown && Common.SendMessageToHelper(0x401, IntPtr.Zero, IntPtr.Zero) > 0)
if (!DragDrop.MouseDown && Helper.SendMessageToHelper(0x401, IntPtr.Zero, IntPtr.Zero) > 0)
{
Common.MMSleep(0.2);
InputSimulation.SendKey(new KEYBDDATA() { wVk = (int)VK.SNAPSHOT });
@@ -674,14 +678,14 @@ namespace MouseWithoutBorders
Logger.LogDebug("PrepareScreenCapture: SNAPSHOT simulated.");
_ = NativeMethods.MoveWindow(
(IntPtr)NativeMethods.FindWindow(null, Common.HELPER_FORM_TEXT),
(IntPtr)NativeMethods.FindWindow(null, Helper.HELPER_FORM_TEXT),
MachineStuff.DesktopBounds.Left,
MachineStuff.DesktopBounds.Top,
MachineStuff.DesktopBounds.Right - MachineStuff.DesktopBounds.Left,
MachineStuff.DesktopBounds.Bottom - MachineStuff.DesktopBounds.Top,
false);
_ = Common.SendMessageToHelper(0x406, IntPtr.Zero, IntPtr.Zero, false);
_ = Helper.SendMessageToHelper(0x406, IntPtr.Zero, IntPtr.Zero, false);
}
else
{
@@ -698,7 +702,7 @@ namespace MouseWithoutBorders
// {
// Process.Start("explorer", "\"" + file + "\"");
// });
_ = CreateProcessInInputDesktopSession(
_ = Launch.CreateProcessInInputDesktopSession(
"\"" + Environment.ExpandEnvironmentVariables(@"%SystemRoot%\System32\Mspaint.exe") +
"\"",
"\"" + file + "\"",
@@ -956,8 +960,8 @@ namespace MouseWithoutBorders
{
// SwitchToMachine(MachineName.Trim());
MachineStuff.NewDesMachineID = DesMachineID = MachineID;
MachineStuff.SwitchLocation.X = XY_BY_PIXEL + myLastX;
MachineStuff.SwitchLocation.Y = XY_BY_PIXEL + myLastY;
MachineStuff.SwitchLocation.X = Event.XY_BY_PIXEL + Event.myLastX;
MachineStuff.SwitchLocation.Y = Event.XY_BY_PIXEL + Event.myLastY;
MachineStuff.SwitchLocation.ResetCount();
EvSwitch.Set();
}
@@ -1314,7 +1318,7 @@ namespace MouseWithoutBorders
}
else
{
_ = ImpersonateLoggedOnUserAndDoSomething(() =>
_ = Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
st = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\" + Common.BinaryName;
if (!Directory.Exists(st))

View File

@@ -206,7 +206,7 @@ namespace MouseWithoutBorders.Class
{
int rv = 1, dx = 0, dy = 0;
bool local = false;
Common.InputEventCount++;
Event.InputEventCount++;
try
{
@@ -220,14 +220,14 @@ namespace MouseWithoutBorders.Class
}
else
{
Common.RealInputEventCount++;
Event.RealInputEventCount++;
if (MachineStuff.NewDesMachineID == Common.MachineID || MachineStuff.NewDesMachineID == ID.ALL)
{
local = true;
if (Common.MainFormVisible && !DragDrop.IsDropping)
{
Common.MainFormDot();
Helper.MainFormDot();
}
}
@@ -269,10 +269,10 @@ namespace MouseWithoutBorders.Class
{
MachineStuff.SwitchLocation.Count--;
if (MachineStuff.SwitchLocation.X > Common.XY_BY_PIXEL - 100000 || MachineStuff.SwitchLocation.Y > Common.XY_BY_PIXEL - 100000)
if (MachineStuff.SwitchLocation.X > Event.XY_BY_PIXEL - 100000 || MachineStuff.SwitchLocation.Y > Event.XY_BY_PIXEL - 100000)
{
hookCallbackMouseData.X = MachineStuff.SwitchLocation.X - Common.XY_BY_PIXEL;
hookCallbackMouseData.Y = MachineStuff.SwitchLocation.Y - Common.XY_BY_PIXEL;
hookCallbackMouseData.X = MachineStuff.SwitchLocation.X - Event.XY_BY_PIXEL;
hookCallbackMouseData.Y = MachineStuff.SwitchLocation.Y - Event.XY_BY_PIXEL;
}
else
{
@@ -308,8 +308,8 @@ namespace MouseWithoutBorders.Class
hookCallbackMouseData.Y = MachineStuff.PrimaryScreenBounds.Bottom + 1;
}
dx += dx < 0 ? -Common.MOVE_MOUSE_RELATIVE : Common.MOVE_MOUSE_RELATIVE;
dy += dy < 0 ? -Common.MOVE_MOUSE_RELATIVE : Common.MOVE_MOUSE_RELATIVE;
dx += dx < 0 ? -Event.MOVE_MOUSE_RELATIVE : Event.MOVE_MOUSE_RELATIVE;
dy += dy < 0 ? -Event.MOVE_MOUSE_RELATIVE : Event.MOVE_MOUSE_RELATIVE;
}
}
@@ -336,13 +336,13 @@ namespace MouseWithoutBorders.Class
private int KeyboardHookProc(int nCode, int wParam, IntPtr lParam)
{
Common.InputEventCount++;
Event.InputEventCount++;
if (!RealData)
{
return NativeMethods.CallNextHookEx(hKeyboardHook, nCode, wParam, lParam);
}
Common.RealInputEventCount++;
Event.RealInputEventCount++;
keyboardHookStruct = LParamToKeyboardHookStruct(lParam);
hookCallbackKeybdData.dwFlags = keyboardHookStruct.Flags;

View File

@@ -223,7 +223,7 @@ namespace MouseWithoutBorders.Class
if (Common.MainFormVisible && !DragDrop.IsDropping)
{
Common.MainFormDot();
Helper.MainFormDot();
}
return rv;

View File

@@ -36,6 +36,7 @@ using Newtonsoft.Json;
using StreamJsonRpc;
using Logger = MouseWithoutBorders.Core.Logger;
using SettingsHelper = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper;
using Thread = MouseWithoutBorders.Core.Thread;
[module: SuppressMessage("Microsoft.MSInternal", "CA904:DeclareTypesInMicrosoftOrSystemNamespace", Scope = "namespace", Target = "MouseWithoutBorders", Justification = "Dotnet port with style preservation")]
@@ -128,7 +129,7 @@ namespace MouseWithoutBorders.Class
{
if (args.Length > 2)
{
Helper.UserLocalAppDataPath = args[2].Trim();
SettingsHelper.UserLocalAppDataPath = args[2].Trim();
}
}
@@ -235,7 +236,7 @@ namespace MouseWithoutBorders.Class
Application.SetCompatibleTextRenderingDefault(false);
Common.Init();
Common.WndProcCounter++;
Core.Helper.WndProcCounter++;
var formScreen = new FrmScreen();
@@ -430,7 +431,7 @@ namespace MouseWithoutBorders.Class
Logger.Log(e);
}
Common.StartMouseWithoutBordersService();
Service.StartMouseWithoutBordersService();
}
internal static string User { get; set; }

View File

@@ -33,6 +33,7 @@ using MouseWithoutBorders.Core;
using Settings.UI.Library.Attributes;
using Lock = System.Threading.Lock;
using SettingsHelper = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper;
[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "MouseWithoutBorders.Properties.Setting.Values.#LoadIntSetting(System.String,System.Int32)", Justification = "Dotnet port with style preservation")]
[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "MouseWithoutBorders.Properties.Setting.Values.#SaveSetting(System.String,System.Object)", Justification = "Dotnet port with style preservation")]
@@ -193,7 +194,7 @@ namespace MouseWithoutBorders.Class
{
_settingsUtils = new SettingsUtils();
_watcher = Helper.GetFileWatcher("MouseWithoutBorders", "settings.json", () =>
_watcher = SettingsHelper.GetFileWatcher("MouseWithoutBorders", "settings.json", () =>
{
try
{

View File

@@ -1684,7 +1684,7 @@ namespace MouseWithoutBorders.Class
{
string fileName = null;
if (!Common.ImpersonateLoggedOnUserAndDoSomething(() =>
if (!Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
if (!File.Exists(Common.LastDragDropFile))
{
@@ -1873,7 +1873,7 @@ namespace MouseWithoutBorders.Class
}
else
{
_ = Common.ImpersonateLoggedOnUserAndDoSomething(() => { r = SendFileEx(s, ecStream, fileName); });
_ = Launch.ImpersonateLoggedOnUserAndDoSomething(() => { r = SendFileEx(s, ecStream, fileName); });
}
return r;
@@ -2111,7 +2111,7 @@ namespace MouseWithoutBorders.Class
}
}
_ = Common.CreateLowIntegrityProcess(
_ = Launch.CreateLowIntegrityProcess(
"\"" + Path.GetDirectoryName(Application.ExecutablePath) + "\\MouseWithoutBordersHelper.exe\"",
"InternalError" + " \"" + msg + "\"",
0,

View File

@@ -132,7 +132,7 @@ internal static class DragDrop
{
if (!IsDropping)
{
IntPtr h = (IntPtr)NativeMethods.FindWindow(null, Common.HELPER_FORM_TEXT);
IntPtr h = (IntPtr)NativeMethods.FindWindow(null, Helper.HELPER_FORM_TEXT);
if (h.ToInt32() > 0)
{
_ = Interlocked.Exchange(ref dragDropStep05ExCalledByIpc, 0);
@@ -189,7 +189,7 @@ internal static class DragDrop
if (!IsDropping)
{
_ = Common.ImpersonateLoggedOnUserAndDoSomething(() =>
_ = Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
if (!string.IsNullOrEmpty(dragFileName) && (File.Exists(dragFileName) || Directory.Exists(dragFileName)))
{

View File

@@ -0,0 +1,274 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using MouseWithoutBorders.Class;
// <summary>
// Keyboard/Mouse hook callback implementation.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
namespace MouseWithoutBorders.Core;
internal static class Event
{
private static readonly DATA KeybdPackage = new();
private static readonly DATA MousePackage = new();
#pragma warning disable SA1307 // Accessible fields should begin with upper-case names
internal static ulong inputEventCount;
internal static ulong invalidPackageCount;
#pragma warning restore SA1307
internal static int MOVE_MOUSE_RELATIVE = 100000;
internal static int XY_BY_PIXEL = 300000;
static Event()
{
}
internal static ulong InvalidPackageCount
{
get => Event.invalidPackageCount;
set => Event.invalidPackageCount = value;
}
internal static ulong InputEventCount
{
get => Event.inputEventCount;
set => Event.inputEventCount = value;
}
internal static ulong RealInputEventCount
{
get;
set;
}
private static Point actualLastPos;
#pragma warning disable SA1307 // Accessible fields should begin with upper-case names
internal static int myLastX;
internal static int myLastY;
#pragma warning restore SA1307
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Dotnet port with style preservation")]
internal static void MouseEvent(MOUSEDATA e, int dx, int dy)
{
try
{
Common.PaintCount = 0;
bool switchByMouseEnabled = IsSwitchingByMouseEnabled();
if (switchByMouseEnabled && Common.Sk != null && (Common.DesMachineID == Common.MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == Common.WM_MOUSEMOVE)
{
Point p = MachineStuff.MoveToMyNeighbourIfNeeded(e.X, e.Y, MachineStuff.desMachineID);
if (!p.IsEmpty)
{
Common.HasSwitchedMachineSinceLastCopy = true;
Logger.LogDebug(string.Format(
CultureInfo.CurrentCulture,
"***** Host Machine: newDesMachineIdEx set = [{0}]. Mouse is now at ({1},{2})",
MachineStuff.newDesMachineIdEx,
e.X,
e.Y));
myLastX = e.X;
myLastY = e.Y;
PrepareToSwitchToMachine(MachineStuff.newDesMachineIdEx, p);
}
}
if (MachineStuff.desMachineID != Common.MachineID && MachineStuff.SwitchLocation.Count <= 0)
{
MousePackage.Des = MachineStuff.desMachineID;
MousePackage.Type = PackageType.Mouse;
MousePackage.Md.dwFlags = e.dwFlags;
MousePackage.Md.WheelDelta = e.WheelDelta;
// Relative move
if (Setting.Values.MoveMouseRelatively && Math.Abs(dx) >= MOVE_MOUSE_RELATIVE && Math.Abs(dy) >= MOVE_MOUSE_RELATIVE)
{
MousePackage.Md.X = dx;
MousePackage.Md.Y = dy;
}
else
{
MousePackage.Md.X = (e.X - MachineStuff.primaryScreenBounds.Left) * 65535 / Common.screenWidth;
MousePackage.Md.Y = (e.Y - MachineStuff.primaryScreenBounds.Top) * 65535 / Common.screenHeight;
}
Common.SkSend(MousePackage, null, false);
if (MousePackage.Md.dwFlags is Common.WM_LBUTTONUP or Common.WM_RBUTTONUP)
{
Thread.Sleep(10);
}
NativeMethods.GetCursorPos(ref actualLastPos);
if (actualLastPos != Common.LastPos)
{
Logger.LogDebug($"Mouse cursor has moved unexpectedly: Expected: {Common.LastPos}, actual: {actualLastPos}.");
Common.LastPos = actualLastPos;
}
}
#if SHOW_ON_WINLOGON_EX
if (RunOnLogonDesktop && e.dwFlags == WM_RBUTTONUP &&
desMachineID == machineID &&
e.x > 2 && e.x < 100 && e.y > 2 && e.y < 20)
{
DoSomethingInUIThread(delegate()
{
MainForm.HideMenuWhenRunOnLogonDesktop();
MainForm.MainMenu.Hide();
MainForm.MainMenu.Show(e.x - 5, e.y - 3);
});
}
#endif
}
catch (Exception ex)
{
Logger.Log(ex);
}
}
internal static bool IsSwitchingByMouseEnabled()
{
return (EasyMouseOption)Setting.Values.EasyMouse == EasyMouseOption.Enable || InputHook.EasyMouseKeyDown;
}
internal static void PrepareToSwitchToMachine(ID newDesMachineID, Point desMachineXY)
{
Logger.LogDebug($"PrepareToSwitchToMachine: newDesMachineID = {newDesMachineID}, desMachineXY = {desMachineXY}");
if (((Common.GetTick() - MachineStuff.lastJump < 100) && (Common.GetTick() - MachineStuff.lastJump > 0)) || MachineStuff.desMachineID == ID.ALL)
{
Logger.LogDebug("PrepareToSwitchToMachine: lastJump");
return;
}
MachineStuff.lastJump = Common.GetTick();
string newDesMachineName = MachineStuff.NameFromID(newDesMachineID);
if (!Common.IsConnectedTo(newDesMachineID))
{// Connection lost, cancel switching
Logger.LogDebug("No active connection found for " + newDesMachineName);
// ShowToolTip("No active connection found for [" + newDesMachineName + "]!", 500);
}
else
{
MachineStuff.newDesMachineID = newDesMachineID;
MachineStuff.SwitchLocation.X = desMachineXY.X;
MachineStuff.SwitchLocation.Y = desMachineXY.Y;
MachineStuff.SwitchLocation.ResetCount();
_ = Common.EvSwitch.Set();
// PostMessage(mainForm.Handle, WM_SWITCH, IntPtr.Zero, IntPtr.Zero);
if (newDesMachineID != DragDrop.DragMachine)
{
if (!DragDrop.IsDragging && !DragDrop.IsDropping)
{
if (DragDrop.MouseDown && !Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop)
{
DragDrop.DragDropStep02();
}
}
else if (DragDrop.DragMachine != (ID)1)
{
DragDrop.ChangeDropMachine();
}
}
else
{
DragDrop.DragDropStep11();
}
// Change des machine
if (MachineStuff.desMachineID != newDesMachineID)
{
Logger.LogDebug("MouseEvent: Switching to new machine:" + newDesMachineName);
// Ask current machine to hide the Mouse cursor
if (newDesMachineID != ID.ALL && MachineStuff.desMachineID != Common.MachineID)
{
Common.SendPackage(MachineStuff.desMachineID, PackageType.HideMouse);
}
Common.DesMachineID = newDesMachineID;
if (MachineStuff.desMachineID == Common.MachineID)
{
if (Common.GetTick() - Common.clipboardCopiedTime < Common.BIG_CLIPBOARD_DATA_TIMEOUT)
{
Common.clipboardCopiedTime = 0;
Common.GetRemoteClipboard("PrepareToSwitchToMachine");
}
}
else
{
// Ask the new active machine to get clipboard data (if the data is too big)
Common.SendPackage(MachineStuff.desMachineID, PackageType.MachineSwitched);
}
_ = Interlocked.Increment(ref Common.switchCount);
}
}
}
internal static void SaveSwitchCount()
{
if (Common.SwitchCount > 0)
{
_ = Task.Run(() =>
{
Setting.Values.SwitchCount += Common.SwitchCount;
_ = Interlocked.Exchange(ref Common.switchCount, 0);
});
}
}
internal static void KeybdEvent(KEYBDDATA e)
{
try
{
Common.PaintCount = 0;
if (MachineStuff.desMachineID != MachineStuff.newDesMachineID)
{
Logger.LogDebug("KeybdEvent: Switching to new machine...");
Common.DesMachineID = MachineStuff.newDesMachineID;
}
if (MachineStuff.desMachineID != Common.MachineID)
{
KeybdPackage.Des = MachineStuff.desMachineID;
KeybdPackage.Type = PackageType.Keyboard;
KeybdPackage.Kd = e;
KeybdPackage.DateTime = Common.GetTick();
Common.SkSend(KeybdPackage, null, false);
if (KeybdPackage.Kd.dwFlags is Common.WM_KEYUP or Common.WM_SYSKEYUP)
{
Thread.Sleep(10);
}
}
}
catch (Exception ex)
{
Logger.Log(ex);
}
}
}

View File

@@ -0,0 +1,526 @@
// 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.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Windows.Forms;
using Microsoft.Win32;
using MouseWithoutBorders.Class;
using static System.Windows.Forms.Control;
// <summary>
// Some other helper methods.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
namespace MouseWithoutBorders.Core;
internal static class Helper
{
internal const string HELPER_FORM_TEXT = "Mouse without Borders Helper";
internal const string HelperProcessName = "PowerToys.MouseWithoutBordersHelper";
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
internal static bool signalHelperToExit;
internal static bool signalWatchDogToExit;
#pragma warning restore SA1307
internal static long WndProcCounter;
private static void WatchDogThread()
{
long oldCounter = WndProcCounter;
do
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
if (signalWatchDogToExit)
{
break;
}
}
while (Common.BlockingUI)
{
Thread.Sleep(1000);
}
if (WndProcCounter == oldCounter)
{
Process p = Process.GetCurrentProcess();
string procInfo = $"{p.PrivateMemorySize64 / 1024 / 1024}MB, {p.TotalProcessorTime}, {Environment.ProcessorCount}.";
string threadStacks = $"{procInfo} {Thread.DumpThreadsStack()}";
Logger.TelemetryLogTrace(threadStacks, SeverityLevel.Error);
break;
}
oldCounter = WndProcCounter;
}
while (true);
}
internal static void HelperThread()
{
// SuppressFlow fixes an issue on service mode, where the helper process can't get enough permissions to be started again.
// More details can be found on: https://github.com/microsoft/PowerToys/pull/36892
using var asyncFlowControl = System.Threading.ExecutionContext.SuppressFlow();
try
{
while (true)
{
_ = Common.EvSwitch.WaitOne(); // Switching to another machine?
if (signalHelperToExit)
{
break;
}
if (MachineStuff.NewDesMachineID != Common.MachineID && MachineStuff.NewDesMachineID != ID.ALL)
{
Common.HideMouseCursor(false);
Helper.MainFormDotEx(true);
}
else
{
if (MachineStuff.SwitchLocation.Count > 0)
{
MachineStuff.SwitchLocation.Count--;
// When we want to move mouse by pixels, we add 300k to x and y (search for XY_BY_PIXEL for other related code).
Logger.LogDebug($"+++++ Moving mouse to {MachineStuff.SwitchLocation.X}, {MachineStuff.SwitchLocation.Y}");
// MaxXY = 65535 so 100k is safe.
if (MachineStuff.SwitchLocation.X > Event.XY_BY_PIXEL - 100000 || MachineStuff.SwitchLocation.Y > Event.XY_BY_PIXEL - 100000)
{
InputSimulation.MoveMouse(MachineStuff.SwitchLocation.X - Event.XY_BY_PIXEL, MachineStuff.SwitchLocation.Y - Event.XY_BY_PIXEL);
}
else
{
InputSimulation.MoveMouseEx(MachineStuff.SwitchLocation.X, MachineStuff.SwitchLocation.Y);
}
Helper.MainFormDot();
}
}
if (MachineStuff.NewDesMachineID == Common.MachineID)
{
Common.ReleaseAllKeys();
}
}
}
catch (Exception e)
{
Logger.Log(e);
}
signalHelperToExit = false;
Logger.LogDebug("^^^Helper Thread exiting...^^^");
}
internal static void MainFormDotEx(bool bCheckTS)
{
Logger.LogDebug("***** MainFormDotEx:");
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop)
{
int left = MachineStuff.PrimaryScreenBounds.Left + ((MachineStuff.PrimaryScreenBounds.Right - MachineStuff.PrimaryScreenBounds.Left) / 2) - 1;
int top = Setting.Values.HideMouse ? 3 : MachineStuff.PrimaryScreenBounds.Top + ((MachineStuff.PrimaryScreenBounds.Bottom - MachineStuff.PrimaryScreenBounds.Top) / 2);
Common.MainFormVisible = true;
if (Setting.Values.HideMouse && Setting.Values.StealFocusWhenSwitchingMachine && Helper.SendMessageToHelper(0x407, new IntPtr(left), new IntPtr(top), true) == 0)
{
try
{
/* When user just switches to the Logon desktop, user is actually on the "Windows Default Lock Screen" (LockApp).
* If a click is sent to this during switch, it actually triggers a desktop switch on the local machine causing a reconnection affecting the machine switch.
* We can detect and skip in this case.
* */
IntPtr foreGroundWindow = NativeMethods.GetForegroundWindow();
string foreGroundWindowText = Common.GetText(foreGroundWindow);
bool mouseClick = true;
if (foreGroundWindowText.Equals("Windows Default Lock Screen", StringComparison.OrdinalIgnoreCase))
{
mouseClick = false;
}
// Window title may be localized, check process name:
if (mouseClick)
{
_ = NativeMethods.GetWindowThreadProcessId(foreGroundWindow, out uint pid);
if (pid > 0)
{
string foreGroundWindowProcess = Process.GetProcessById((int)pid)?.ProcessName;
if (foreGroundWindowProcess.Equals("LockApp", StringComparison.OrdinalIgnoreCase))
{
mouseClick = false;
}
}
}
if (mouseClick)
{
InputSimulation.MouseClickDotForm(left + 1, top + 1);
}
}
catch (Exception e)
{
Logger.Log(e);
}
}
}
CustomCursor.ShowFakeMouseCursor(int.MinValue, int.MinValue);
}
internal static void MainForm3Pixels()
{
Logger.LogDebug("***** MainFormDotLarge:");
Common.DoSomethingInUIThread(
() =>
{
Common.MainForm.Left = MachineStuff.PrimaryScreenBounds.Left + ((MachineStuff.PrimaryScreenBounds.Right - MachineStuff.PrimaryScreenBounds.Left) / 2) - 2;
Common.MainForm.Top = Setting.Values.HideMouse ? 3 : MachineStuff.PrimaryScreenBounds.Top + ((MachineStuff.PrimaryScreenBounds.Bottom - MachineStuff.PrimaryScreenBounds.Top) / 2) - 1;
Common.MainForm.Width = 3;
Common.MainForm.Height = 3;
Common.MainForm.Opacity = 0.11D;
Common.MainForm.TopMost = true;
if (Setting.Values.HideMouse)
{
Common.MainForm.BackColor = Color.Black;
Common.MainForm.Show();
Common.MainFormVisible = true;
}
else
{
Common.MainForm.BackColor = Color.White;
Common.MainForm.Hide();
Common.MainFormVisible = false;
}
},
true);
CustomCursor.ShowFakeMouseCursor(int.MinValue, int.MinValue);
}
internal static void MainFormDot()
{
Common.DoSomethingInUIThread(
() =>
{
_ = Helper.SendMessageToHelper(0x408, IntPtr.Zero, IntPtr.Zero, false);
Common.MainForm.Left = MachineStuff.PrimaryScreenBounds.Left + ((MachineStuff.PrimaryScreenBounds.Right - MachineStuff.PrimaryScreenBounds.Left) / 2) - 1;
Common.MainForm.Top = Setting.Values.HideMouse ? 3 : MachineStuff.PrimaryScreenBounds.Top + ((MachineStuff.PrimaryScreenBounds.Bottom - MachineStuff.PrimaryScreenBounds.Top) / 2);
Common.MainForm.Width = 1;
Common.MainForm.Height = 1;
Common.MainForm.Opacity = 0.15;
Common.MainForm.Hide();
Common.MainFormVisible = false;
},
true);
CustomCursor.ShowFakeMouseCursor(int.MinValue, int.MinValue);
}
internal static void ToggleIcon()
{
try
{
if (Common.toggleIconsIndex < Common.TOGGLE_ICONS_SIZE)
{
Common.DoSomethingInUIThread(() => Common.MainForm.ChangeIcon(Common.toggleIcons[Common.toggleIconsIndex++]));
}
else
{
Common.toggleIconsIndex = 0;
Common.toggleIcons = null;
}
}
catch (Exception e)
{
Logger.Log(e);
}
}
internal static void RunDDHelper(bool cleanUp = false)
{
if (Common.RunOnLogonDesktop || Common.RunOnScrSaverDesktop)
{
return;
}
if (cleanUp)
{
try
{
Process[] ps = Process.GetProcessesByName(HelperProcessName);
foreach (Process p in ps)
{
p.KillProcess();
}
}
catch (Exception e)
{
Logger.Log(e);
_ = Helper.SendMessageToHelper(SharedConst.QUIT_CMD, IntPtr.Zero, IntPtr.Zero);
}
return;
}
if (!Common.IsMyDesktopActive())
{
return;
}
if (!Common.IpcChannelCreated)
{
Logger.TelemetryLogTrace($"{nameof(Common.IpcChannelCreated)} = {Common.IpcChannelCreated}. {Logger.GetStackTrace(new StackTrace())}", SeverityLevel.Warning);
return;
}
if (!Common.MainForm.IsDisposed)
{
Common.MainForm.NotifyIcon.Visible = false;
Common.MainForm.NotifyIcon.Visible = Setting.Values.ShowOriginalUI;
}
IntPtr h = (IntPtr)NativeMethods.FindWindow(null, Helper.HELPER_FORM_TEXT);
if (h.ToInt32() <= 0)
{
_ = Launch.CreateProcessInInputDesktopSession(
$"\"{Path.GetDirectoryName(Application.ExecutablePath)}\\{HelperProcessName}.exe\"",
string.Empty,
Common.GetInputDesktop(),
0);
Common.HasSwitchedMachineSinceLastCopy = true;
// Common.CreateLowIntegrityProcess("\"" + Path.GetDirectoryName(Application.ExecutablePath) + "\\MouseWithoutBordersHelper.exe\"", string.Empty, 0, false, 0);
var processes = Process.GetProcessesByName(HelperProcessName);
if (processes?.Length == 0)
{
Logger.Log("Unable to start helper process.");
Common.ShowToolTip("Error starting Mouse Without Borders Helper, clipboard sharing will not work!", 5000, ToolTipIcon.Error);
}
else
{
Logger.Log("Helper process started.");
}
}
else
{
var processes = Process.GetProcessesByName(HelperProcessName);
if (processes?.Length > 0)
{
Logger.Log("Helper process found running.");
}
else
{
Logger.Log("Invalid helper process found running.");
Common.ShowToolTip("Error finding Mouse Without Borders Helper, clipboard sharing will not work!", 5000, ToolTipIcon.Error);
}
}
}
internal static int SendMessageToHelper(int msg, IntPtr wparam, IntPtr lparam, bool wait = true, bool log = true)
{
int h = NativeMethods.FindWindow(null, Helper.HELPER_FORM_TEXT);
int rv = -1;
if (h > 0)
{
rv = wait
? (int)NativeMethods.SendMessage((IntPtr)h, msg, wparam, lparam)
: NativeMethods.PostMessage((IntPtr)h, msg, wparam, lparam) ? 1 : 0;
}
if (log)
{
Logger.LogDebug($"SendMessageToHelper: HelperWindow={h}, Return={rv}, msg={msg}, w={wparam.ToInt32()}, l={lparam.ToInt32()}, Post={!wait}");
}
return rv;
}
internal static bool IsWindows8AndUp()
{
return (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor >= 2)
|| Environment.OSVersion.Version.Major > 6;
}
internal static string GetMiniLog(IEnumerable<ControlCollection> optionControls)
{
string log = string.Empty;
log += "=============================================================================================================================\r\n";
log += $"{Application.ProductName} version {Application.ProductVersion}\r\n";
log += $"{Setting.Values.Username}/{Common.GetDebugInfo(Common.MyKey)}\r\n";
log += $"{Common.MachineName}/{Common.MachineID}/{Common.DesMachineID}\r\n";
log += $"Id: {Setting.Values.DeviceId}\r\n";
log += $"Matrix: {string.Join(",", MachineStuff.MachineMatrix)}\r\n";
log += $"McPool: {Setting.Values.MachinePoolString}\r\n";
log += "\r\nOPTIONS:\r\n";
foreach (ControlCollection controlCollection in optionControls)
{
foreach (object c in controlCollection)
{
if (c is CheckBox checkBox)
{
log += $"({(checkBox.Checked ? 1 : 0)}) {checkBox.Text}\r\n";
continue;
}
if (c is RadioButton radioButton)
{
log += $"({(radioButton.Checked ? 1 : 0)}) {radioButton.Name}.[{radioButton.Text}]\r\n";
continue;
}
if (c is ComboBox comboBox)
{
log += $"{comboBox.Name} = {comboBox.Text}\r\n";
continue;
}
}
}
log += "\r\n";
SocketStuff sk = Common.Sk;
if (sk?.TcpSockets != null)
{
foreach (TcpSk tcp in sk.TcpSockets)
{
log += $"{Common.MachineName}{(tcp.IsClient ? "=>" : "<=")}{tcp.MachineName}({tcp.MachineId}):{tcp.Status}\r\n";
}
}
log += string.Format(CultureInfo.CurrentCulture, "Helper:{0}\r\n", SendMessageToHelper(0x400, IntPtr.Zero, IntPtr.Zero));
log += Setting.Values.LastPersonalizeLogonScr + "\r\n";
log += "Name2IP =\r\n" + Setting.Values.Name2IP + "\r\n";
log += "Last 10 trace messages:\r\n";
log += string.Join(Environment.NewLine, Logger.LogCounter.Select(item => $"({item.Value}): {item.Key}").Take(10));
log += "\r\n=============================================================================================================================";
return log;
}
internal static bool GetUserName()
{
if (string.IsNullOrEmpty(Setting.Values.Username) && !Common.RunOnLogonDesktop)
{
if (Program.User.Contains("system", StringComparison.CurrentCultureIgnoreCase))
{
_ = Launch.ImpersonateLoggedOnUserAndDoSomething(() =>
{
// See: https://stackoverflow.com/questions/19487541/how-to-get-windows-user-name-from-sessionid
static string GetUsernameBySessionId(int sessionId)
{
string username = "SYSTEM";
if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTSInfoClass.WTSUserName, out nint buffer, out int strLen) && strLen > 1)
{
username = Marshal.PtrToStringAnsi(buffer);
NativeMethods.WTSFreeMemory(buffer);
if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTSInfoClass.WTSDomainName, out buffer, out strLen) && strLen > 1)
{
username = @$"{Marshal.PtrToStringAnsi(buffer)}\{username}";
NativeMethods.WTSFreeMemory(buffer);
}
}
return username;
}
// The most direct way to fetch the username is WindowsIdentity.GetCurrent(true).Name
// but GetUserName can run within an ExecutionContext.SuppressFlow block, which creates issues
// with WindowsIdentity.GetCurrent.
// See: https://stackoverflow.com/questions/76998988/exception-when-using-executioncontext-suppressflow-in-net-7
// So we use WTSQuerySessionInformation as a workaround.
Setting.Values.Username = GetUsernameBySessionId(Process.GetCurrentProcess().SessionId);
});
}
else
{
Setting.Values.Username = Program.User;
}
Logger.LogDebug("[Username] = " + Setting.Values.Username);
}
return !string.IsNullOrEmpty(Setting.Values.Username);
}
internal static void ShowOneWayModeMessage()
{
Common.ToggleShowTopMostMessage(
@"
Due to Security Controls, a remote device cannot control a SAW device.
Please use the keyboard and Mouse from the SAW device.
(Press Esc to hide this message)
",
string.Empty,
10);
}
internal static void ApplyCADSetting()
{
try
{
if (Setting.Values.DisableCAD)
{
RegistryKey k = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System");
if (k != null)
{
k.SetValue("DisableCAD", 1, RegistryValueKind.DWord);
k.Close();
}
}
else
{
RegistryKey k = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System");
if (k != null)
{
k.SetValue("DisableCAD", 0, RegistryValueKind.DWord);
k.Close();
}
}
}
catch (Exception e)
{
Logger.Log(e);
}
}
}

View File

@@ -0,0 +1,312 @@
// 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.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Principal;
using MouseWithoutBorders.Class;
// <summary>
// Impersonation.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
namespace MouseWithoutBorders.Core;
internal static class Launch
{
private static bool RunElevated()
{
return WindowsIdentity.GetCurrent().Owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid);
}
internal static bool ImpersonateLoggedOnUserAndDoSomething(Action targetFunc)
{
if (Common.RunWithNoAdminRight)
{
targetFunc();
return true;
}
else
{
// SuppressFlow fixes an issue on service mode, where WTSQueryUserToken runs successfully once and then fails
// on subsequent calls. The reason appears to be an unknown issue with reverting the impersonation,
// meaning that subsequent impersonation attempts run as the logged-on user and fail.
// This is a workaround.
using var asyncFlowControl = System.Threading.ExecutionContext.SuppressFlow();
uint dwSessionId;
IntPtr hUserToken = IntPtr.Zero, hUserTokenDup = IntPtr.Zero;
try
{
dwSessionId = (uint)Process.GetCurrentProcess().SessionId;
uint rv = NativeMethods.WTSQueryUserToken(dwSessionId, ref hUserToken);
var lastError = rv == 0 ? Marshal.GetLastWin32Error() : 0;
Logger.LogDebug($"{nameof(NativeMethods.WTSQueryUserToken)} returned {rv.ToString(CultureInfo.CurrentCulture)}");
if (rv == 0)
{
Logger.Log($"{nameof(NativeMethods.WTSQueryUserToken)} failed with: {lastError}.");
return false;
}
if (!NativeMethods.DuplicateToken(hUserToken, (int)NativeMethods.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, ref hUserTokenDup))
{
Logger.TelemetryLogTrace($"{nameof(NativeMethods.DuplicateToken)} Failed! {Logger.GetStackTrace(new StackTrace())}", SeverityLevel.Warning);
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return false;
}
if (NativeMethods.ImpersonateLoggedOnUser(hUserTokenDup))
{
targetFunc();
_ = NativeMethods.RevertToSelf();
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return true;
}
else
{
Logger.Log("ImpersonateLoggedOnUser Failed!");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return false;
}
}
catch (Exception e)
{
Logger.Log(e);
return false;
}
}
}
internal static int CreateProcessInInputDesktopSession(string commandLine, string arg, string desktop, short wShowWindow, bool lowIntegrity = false)
// As user who runs explorer.exe
{
if (!Program.User.Contains("system", StringComparison.InvariantCultureIgnoreCase))
{
ProcessStartInfo s = new(commandLine, arg);
s.WindowStyle = wShowWindow != 0 ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden;
Process p = Process.Start(s);
return p == null ? 0 : p.Id;
}
string commandLineWithArg = commandLine + " " + arg;
int lastError;
int dwSessionId;
IntPtr hUserToken = IntPtr.Zero, hUserTokenDup = IntPtr.Zero;
Logger.LogDebug("CreateProcessInInputDesktopSession called, launching " + commandLineWithArg + " on " + desktop);
try
{
dwSessionId = Process.GetCurrentProcess().SessionId;
// Get the user token used by DuplicateTokenEx
lastError = (int)NativeMethods.WTSQueryUserToken((uint)dwSessionId, ref hUserToken);
NativeMethods.STARTUPINFO si = default;
si.cb = Marshal.SizeOf(si);
si.lpDesktop = "winsta0\\" + desktop;
si.wShowWindow = wShowWindow;
NativeMethods.SECURITY_ATTRIBUTES sa = default;
sa.Length = Marshal.SizeOf(sa);
if (!NativeMethods.DuplicateTokenEx(hUserToken, NativeMethods.MAXIMUM_ALLOWED, ref sa, (int)NativeMethods.SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)NativeMethods.TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
{
lastError = Marshal.GetLastWin32Error();
Logger.Log(string.Format(CultureInfo.CurrentCulture, "DuplicateTokenEx error: {0} Token does not have the privilege.", lastError));
_ = NativeMethods.CloseHandle(hUserToken);
return 0;
}
if (lowIntegrity)
{
NativeMethods.TOKEN_MANDATORY_LABEL tIL;
// Low
string sIntegritySid = "S-1-16-4096";
bool rv = NativeMethods.ConvertStringSidToSid(sIntegritySid, out IntPtr pIntegritySid);
if (!rv)
{
Logger.Log("ConvertStringSidToSid failed");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return 0;
}
tIL.Label.Attributes = NativeMethods.SE_GROUP_INTEGRITY;
tIL.Label.Sid = pIntegritySid;
rv = NativeMethods.SetTokenInformation(hUserTokenDup, NativeMethods.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, ref tIL, (uint)Marshal.SizeOf(tIL) + (uint)IntPtr.Size);
if (!rv)
{
Logger.Log("SetTokenInformation failed");
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return 0;
}
}
uint dwCreationFlags = NativeMethods.NORMAL_PRIORITY_CLASS | NativeMethods.CREATE_NEW_CONSOLE;
IntPtr pEnv = IntPtr.Zero;
if (NativeMethods.CreateEnvironmentBlock(ref pEnv, hUserTokenDup, true))
{
dwCreationFlags |= NativeMethods.CREATE_UNICODE_ENVIRONMENT;
}
else
{
pEnv = IntPtr.Zero;
}
_ = NativeMethods.CreateProcessAsUser(
hUserTokenDup, // client's access token
null, // file to execute
commandLineWithArg, // command line
ref sa, // pointer to process SECURITY_ATTRIBUTES
ref sa, // pointer to thread SECURITY_ATTRIBUTES
false, // handles are not inheritable
(int)dwCreationFlags, // creation flags
pEnv, // pointer to new environment block
null, // name of current directory
ref si, // pointer to STARTUPINFO structure
out NativeMethods.PROCESS_INFORMATION pi); // receives information about new process
// GetLastError should be 0
int iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
Logger.LogDebug("CreateProcessAsUser returned " + iResultOfCreateProcessAsUser.ToString(CultureInfo.CurrentCulture));
// Close handles task
_ = NativeMethods.CloseHandle(hUserToken);
_ = NativeMethods.CloseHandle(hUserTokenDup);
return (iResultOfCreateProcessAsUser == 0) ? (int)pi.dwProcessId : 0;
}
catch (Exception e)
{
Logger.Log(e);
return 0;
}
}
#if CUSTOMIZE_LOGON_SCREEN
internal static bool CreateLowIntegrityProcess(string commandLine, string args, int wait, bool killIfTimedOut, long limitedMem, short wShowWindow = 0)
{
int processId = CreateProcessInInputDesktopSession(commandLine, args, "default", wShowWindow, true);
if (processId <= 0)
{
return false;
}
if (wait > 0)
{
if (limitedMem > 0)
{
int sec = 0;
while (true)
{
Process p;
try
{
if ((p = Process.GetProcessById(processId)) == null)
{
Logger.Log("Process exited!");
break;
}
}
catch (ArgumentException)
{
Logger.Log("GetProcessById.ArgumentException");
break;
}
if ((!p.HasExited && p.PrivateMemorySize64 > limitedMem) || (++sec > (wait / 1000)))
{
Logger.Log(string.Format(CultureInfo.CurrentCulture, "Process log (mem): {0}, {1}", sec, p.PrivateMemorySize64));
return false;
}
Thread.Sleep(1000);
}
}
else
{
Process p;
if ((p = Process.GetProcessById(processId)) == null)
{
Logger.Log("Process exited!");
}
else if (NativeMethods.WaitForSingleObject(p.Handle, wait) != NativeMethods.WAIT_OBJECT_0 && killIfTimedOut)
{
Logger.Log("Process log (time).");
TerminateProcessTree(p.Handle, (uint)processId, -1);
return false;
}
}
}
return true;
}
private static void TerminateProcessTree(IntPtr hProcess, uint processID, int exitCode)
{
if (processID > 0 && hProcess.ToInt32() > 0)
{
Process[] processes = Process.GetProcesses();
int dwSessionId = Process.GetCurrentProcess().SessionId;
foreach (Process p in processes)
{
if (p.SessionId == dwSessionId)
{
NativeMethods.PROCESS_BASIC_INFORMATION processBasicInformation = default;
try
{
if (NativeMethods.NtQueryInformationProcess(p.Handle, 0, ref processBasicInformation, (uint)Marshal.SizeOf(processBasicInformation), out uint bytesWritten) >= 0)
{// NT_SUCCESS(...)
if (processBasicInformation.InheritedFromUniqueProcessId == processID)
{
TerminateProcessTree(p.Handle, processBasicInformation.UniqueProcessId, exitCode);
}
}
}
catch (InvalidOperationException e)
{
Logger.Log(e);
continue;
}
catch (Win32Exception e)
{
Logger.Log(e);
continue;
}
}
}
_ = NativeMethods.TerminateProcess(hProcess, (IntPtr)exitCode);
}
}
#endif
}

View File

@@ -138,7 +138,7 @@ internal static class Logger
Common.PackageSent.ClipboardDragDrop,
Common.PackageSent.ClipboardDragDropEnd,
Common.PackageSent.ExplorerDragDrop,
Common.inputEventCount,
Event.inputEventCount,
Common.PackageSent.Nil);
Log(log);
lastPackageSent = Common.PackageSent; // Copy data
@@ -161,7 +161,7 @@ internal static class Logger
Common.PackageReceived.ClipboardDragDrop,
Common.PackageReceived.ClipboardDragDropEnd,
Common.PackageReceived.ExplorerDragDrop,
Common.invalidPackageCount,
Event.invalidPackageCount,
Common.PackageReceived.Nil,
Receiver.processedPackageCount,
Receiver.skippedPackageCount);
@@ -197,15 +197,9 @@ internal static class Logger
myThreads.Add(t);
}
_ = Logger.PrivateDump(sb, AllLogs, "[Program logs]\r\n===============\r\n", 0, level, false);
_ = Logger.PrivateDump(sb, new Common(), "[Other Logs]\r\n===============\r\n", 0, level, false);
Logger.DumpType(sb, typeof(Logger), 0, level);
sb.AppendLine("[DragDrop]\r\n===============");
Logger.DumpType(sb, typeof(DragDrop), 0, level);
sb.AppendLine("[MachineStuff]\r\n===============");
Logger.DumpType(sb, typeof(MachineStuff), 0, level);
sb.AppendLine("[Receiver]\r\n===============");
Logger.DumpType(sb, typeof(Receiver), 0, level);
Logger.DumpProgramLogs(sb, level);
Logger.DumpOtherLogs(sb, level);
Logger.DumpStaticTypes(sb, level);
log = string.Format(
CultureInfo.CurrentCulture,
@@ -241,6 +235,36 @@ internal static class Logger
}
}
internal static void DumpProgramLogs(StringBuilder sb, int level)
{
_ = Logger.PrivateDump(sb, AllLogs, "[Program logs]\r\n===============\r\n", 0, level, false);
}
internal static void DumpOtherLogs(StringBuilder sb, int level)
{
_ = Logger.PrivateDump(sb, new Common(), "[Other Logs]\r\n===============\r\n", 0, level, false);
}
internal static void DumpStaticTypes(StringBuilder sb, int level)
{
sb.AppendLine($"[{nameof(DragDrop)}]\r\n===============");
Logger.DumpType(sb, typeof(DragDrop), 0, level);
sb.AppendLine($"[{nameof(Event)}]\r\n===============");
Logger.DumpType(sb, typeof(Event), 0, level);
sb.AppendLine($"[{nameof(Helper)}]\r\n===============");
Logger.DumpType(sb, typeof(Helper), 0, level);
sb.AppendLine($"[{nameof(Launch)}]\r\n===============");
Logger.DumpType(sb, typeof(Launch), 0, level);
sb.AppendLine($"[{nameof(Logger)}]\r\n===============");
Logger.DumpType(sb, typeof(Logger), 0, level);
sb.AppendLine($"[{nameof(MachineStuff)}]\r\n===============");
Logger.DumpType(sb, typeof(MachineStuff), 0, level);
sb.AppendLine($"[{nameof(Receiver)}]\r\n===============");
Logger.DumpType(sb, typeof(Receiver), 0, level);
sb.AppendLine($"[{nameof(Service)}]\r\n===============");
Logger.DumpType(sb, typeof(Service), 0, level);
}
internal static bool PrivateDump(StringBuilder sb, object obj, string objName, int level, int maxLevel, bool stop)
{
Type t;

View File

@@ -1018,8 +1018,8 @@ internal static class MachineStuff
}
NewDesMachineID = Common.DesMachineID = id;
SwitchLocation.X = Common.XY_BY_PIXEL + primaryScreenBounds.Left + ((primaryScreenBounds.Right - primaryScreenBounds.Left) / 2);
SwitchLocation.Y = Common.XY_BY_PIXEL + primaryScreenBounds.Top + ((primaryScreenBounds.Bottom - primaryScreenBounds.Top) / 2);
SwitchLocation.X = Event.XY_BY_PIXEL + primaryScreenBounds.Left + ((primaryScreenBounds.Right - primaryScreenBounds.Left) / 2);
SwitchLocation.Y = Event.XY_BY_PIXEL + primaryScreenBounds.Top + ((primaryScreenBounds.Bottom - primaryScreenBounds.Top) / 2);
SwitchLocation.ResetCount();
Common.UpdateMultipleModeIconAndMenu();
Common.HideMouseCursor(false);

View File

@@ -41,12 +41,12 @@ internal static class Receiver
{
if (package.Type == PackageType.Invalid)
{
if ((Common.InvalidPackageCount % 100) == 0)
if ((Event.InvalidPackageCount % 100) == 0)
{
Common.ShowToolTip("Invalid packages received!", 1000, ToolTipIcon.Warning, false);
}
Common.InvalidPackageCount++;
Event.InvalidPackageCount++;
Logger.Log("Invalid packages received!");
return false;
}
@@ -104,7 +104,7 @@ internal static class Receiver
{
if ((package.Kd.dwFlags & (int)Common.LLKHF.UP) == (int)Common.LLKHF.UP)
{
Common.ShowOneWayModeMessage();
Helper.ShowOneWayModeMessage();
}
return;
@@ -133,7 +133,7 @@ internal static class Receiver
{
if (package.Md.dwFlags is Common.WM_LBUTTONDOWN or Common.WM_RBUTTONDOWN)
{
Common.ShowOneWayModeMessage();
Helper.ShowOneWayModeMessage();
}
}
else if (package.Md.dwFlags is Common.WM_LBUTTONUP or Common.WM_RBUTTONUP)
@@ -144,13 +144,13 @@ internal static class Receiver
return;
}
if (Math.Abs(package.Md.X) >= Common.MOVE_MOUSE_RELATIVE && Math.Abs(package.Md.Y) >= Common.MOVE_MOUSE_RELATIVE)
if (Math.Abs(package.Md.X) >= Event.MOVE_MOUSE_RELATIVE && Math.Abs(package.Md.Y) >= Event.MOVE_MOUSE_RELATIVE)
{
if (package.Md.dwFlags == Common.WM_MOUSEMOVE)
{
InputSimulation.MoveMouseRelative(
package.Md.X < 0 ? package.Md.X + Common.MOVE_MOUSE_RELATIVE : package.Md.X - Common.MOVE_MOUSE_RELATIVE,
package.Md.Y < 0 ? package.Md.Y + Common.MOVE_MOUSE_RELATIVE : package.Md.Y - Common.MOVE_MOUSE_RELATIVE);
package.Md.X < 0 ? package.Md.X + Event.MOVE_MOUSE_RELATIVE : package.Md.X - Event.MOVE_MOUSE_RELATIVE,
package.Md.Y < 0 ? package.Md.Y + Event.MOVE_MOUSE_RELATIVE : package.Md.Y - Event.MOVE_MOUSE_RELATIVE);
_ = NativeMethods.GetCursorPos(ref lastXY);
Point p = MachineStuff.MoveToMyNeighbourIfNeeded(lastXY.X, lastXY.Y, Common.MachineID);
@@ -195,9 +195,9 @@ internal static class Receiver
case PackageType.NextMachine:
Logger.LogDebug("PackageType.NextMachine received!");
if (Common.IsSwitchingByMouseEnabled())
if (Event.IsSwitchingByMouseEnabled())
{
Common.PrepareToSwitchToMachine((ID)package.Md.WheelDelta, new Point(package.Md.X, package.Md.Y));
Event.PrepareToSwitchToMachine((ID)package.Md.WheelDelta, new Point(package.Md.X, package.Md.Y));
}
break;
@@ -383,7 +383,7 @@ internal static class Receiver
case PackageType.HideMouse:
Common.HasSwitchedMachineSinceLastCopy = true;
Common.HideMouseCursor(true);
Common.MainFormDotEx(false);
Helper.MainFormDotEx(false);
Common.ReleaseAllKeys();
break;

View File

@@ -0,0 +1,159 @@
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.ServiceProcess;
using System.Threading.Tasks;
using System.Windows.Forms;
using MouseWithoutBorders.Class;
[module: SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions", Scope = "member", Target = "MouseWithoutBorders.Common.#StartMouseWithoutBordersService()", Justification = "Dotnet port with style preservation")]
// <summary>
// Service control code.
// </summary>
// <history>
// 2008 created by Truong Do (ductdo).
// 2009-... modified by Truong Do (TruongDo).
// 2023- Included in PowerToys.
// </history>
namespace MouseWithoutBorders.Core;
internal static class Service
{
private static bool shownErrMessage;
private static DateTime lastStartServiceTime = DateTime.UtcNow;
internal static void StartMouseWithoutBordersService(string desktopToRunMouseWithoutBordersOn = null, string startTag1 = "byapp", string startTag2 = null)
{
// NOTE(@yuyoyuppe): the new flow assumes we run both mwb processes directly from the svc.
if (Common.RunWithNoAdminRight || true)
{
return;
}
Logger.Log($"{nameof(StartMouseWithoutBordersService)}: {Logger.GetStackTrace(new StackTrace())}.");
Task task = Task.Run(() =>
{
Process[] ps = Process.GetProcessesByName("MouseWithoutBordersSvc");
if (ps.Length != 0)
{
if (DateTime.UtcNow - lastStartServiceTime < TimeSpan.FromSeconds(5))
{
Logger.Log($"{nameof(StartMouseWithoutBordersService)}: Aborted.");
return;
}
foreach (Process pp in ps)
{
Logger.Log(string.Format(CultureInfo.InvariantCulture, "Killing process MouseWithoutBordersSvc {0}.", pp.Id));
pp.KillProcess();
}
}
lastStartServiceTime = DateTime.UtcNow;
ServiceController service = new("MouseWithoutBordersSvc");
try
{
Logger.Log("Starting " + service.ServiceName);
}
catch (Exception)
{
if (!shownErrMessage)
{
shownErrMessage = true;
_ = MessageBox.Show(
Application.ProductName + " is not installed yet, please run Setup.exe first!",
Application.ProductName,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return;
}
try
{
int c = 0;
while (service.Status != ServiceControllerStatus.Stopped && c++ < 5)
{
Thread.Sleep(1000);
service = new ServiceController("MouseWithoutBordersSvc");
}
if (string.IsNullOrWhiteSpace(desktopToRunMouseWithoutBordersOn))
{
startTag2 ??= Process.GetCurrentProcess().SessionId.ToString(CultureInfo.InvariantCulture);
service.Start(new string[] { startTag1, startTag2 });
}
else
{
service.Start(new string[] { desktopToRunMouseWithoutBordersOn });
}
}
catch (Exception e)
{
Logger.Log(e);
// ERROR_SERVICE_ALREADY_RUNNING
if (!(shownErrMessage || ((e?.InnerException as Win32Exception)?.NativeErrorCode == 1056)))
{
shownErrMessage = true;
_ = MessageBox.Show(
"Cannot start service " + service.ServiceName + ": " + e.Message,
Common.BinaryName,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return;
}
});
// Wait for the task while not blocking the UI thread.
do
{
Common.MMSleep(1);
if (task.IsCanceled || task.IsCompleted || task.IsFaulted)
{
break;
}
}
while (true);
}
internal static void StartServiceAndSendLogoffSignal()
{
try
{
Process[] p = Process.GetProcessesByName("winlogon");
Process me = Process.GetCurrentProcess();
string myWinlogon = p?.FirstOrDefault(item => item.SessionId == me.SessionId)?.Id.ToString(CultureInfo.InvariantCulture) ?? null;
if (string.IsNullOrWhiteSpace(myWinlogon))
{
StartMouseWithoutBordersService(null, "logoff");
}
else
{
StartMouseWithoutBordersService(null, "logoff", myWinlogon);
}
}
catch (Exception e)
{
Logger.Log($"{nameof(StartServiceAndSendLogoffSignal)}: {e.Message}");
}
}
}

View File

@@ -63,8 +63,8 @@ namespace MouseWithoutBorders
try
{
Common.Hook = new InputHook();
Common.Hook.MouseEvent += new InputHook.MouseEvHandler(Common.MouseEvent);
Common.Hook.KeyboardEvent += new InputHook.KeybdEvHandler(Common.KeybdEvent);
Common.Hook.MouseEvent += new InputHook.MouseEvHandler(Event.MouseEvent);
Common.Hook.KeyboardEvent += new InputHook.KeybdEvHandler(Event.KeybdEvent);
Logger.Log("(((((Keyboard/Mouse hooks installed/reinstalled!)))))");
/* The hook is called in the context of the thread that installed it.

View File

@@ -132,7 +132,7 @@ namespace MouseWithoutBorders
internal void UpdateKeyTextBox()
{
_ = Common.GetUserName();
_ = Helper.GetUserName();
textBoxEnc.Text = Common.MyKey;
}
@@ -787,7 +787,7 @@ namespace MouseWithoutBorders
{
if (!Common.RunWithNoAdminRight)
{
Common.ApplyCADSetting();
Helper.ApplyCADSetting();
ShowUpdateMessage();
}
}
@@ -1163,7 +1163,7 @@ namespace MouseWithoutBorders
private void LinkLabelMiniLog_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
string miniLog = Common.GetMiniLog(new[] { groupBoxOtherOptions.Controls, groupBoxShortcuts.Controls });
string miniLog = Helper.GetMiniLog(new[] { groupBoxOtherOptions.Controls, groupBoxShortcuts.Controls });
Clipboard.SetText(miniLog);
Common.ShowToolTip("Log has been placed in the clipboard.", 30000, ToolTipIcon.Info, false);

View File

@@ -78,7 +78,7 @@ namespace MouseWithoutBorders
NotifyIcon.BalloonTipTitle = Application.ProductName;
menuGenDumpFile.Visible = true;
Common.WndProcCounter++;
Helper.WndProcCounter++;
try
{
@@ -101,7 +101,7 @@ namespace MouseWithoutBorders
}
else
{
Common.StartServiceAndSendLogoffSignal();
Service.StartServiceAndSendLogoffSignal();
Quit(true, true);
}
}
@@ -119,7 +119,7 @@ namespace MouseWithoutBorders
NotifyIcon.Dispose();
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop)
{
Common.RunDDHelper(true);
Helper.RunDDHelper(true);
}
// Common.UnhookClipboard();
@@ -133,7 +133,7 @@ namespace MouseWithoutBorders
Setting.Values.SwitchCount += Common.SwitchCount;
Process me = Process.GetCurrentProcess();
Common.WndProcCounter++;
Helper.WndProcCounter++;
try
{
@@ -142,13 +142,13 @@ namespace MouseWithoutBorders
Common.Cleanup();
}
Common.WndProcCounter++;
Helper.WndProcCounter++;
if (!Common.RunOnScrSaverDesktop)
{
Common.ReleaseAllKeys();
}
Common.RunDDHelper(true);
Helper.RunDDHelper(true);
}
catch (Exception e)
{
@@ -285,7 +285,7 @@ namespace MouseWithoutBorders
helperTimer.Interval = 100;
helperTimer.Tick += new EventHandler(HelperTimer_Tick);
helperTimer.Start();
Common.WndProcCounter++;
Helper.WndProcCounter++;
if (Environment.OSVersion.Version.Major > 6 ||
(Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor >= 1))
@@ -307,7 +307,7 @@ namespace MouseWithoutBorders
private void HelperTimer_Tick(object sender, EventArgs e)
{
Common.WndProcCounter++;
Helper.WndProcCounter++;
if (busy)
{
@@ -338,7 +338,7 @@ namespace MouseWithoutBorders
if (Common.MainFormVisible)
{
Common.MainFormDot();
Helper.MainFormDot();
}
InputSimulation.ResetSystemKeyFlags();
@@ -357,7 +357,7 @@ namespace MouseWithoutBorders
{
if (!Common.SecondOpenSocketTry)
{
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && !Common.GetUserName())
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && !Helper.GetUserName())
{
// While Windows 8 is hybrid-shutting down, user name would be empty (as returned from the .Net API), we should not do anything in this case.
Logger.LogDebug("No active user.");
@@ -407,7 +407,7 @@ namespace MouseWithoutBorders
// Common.ReHookClipboard();
}
Common.RunDDHelper();
Helper.RunDDHelper();
}
count = 0;
@@ -485,7 +485,7 @@ namespace MouseWithoutBorders
// One more time after 1/3 minutes (Sometimes XP has explorer started late)
if (count == 600 || count == 1800)
{
Common.RunDDHelper();
Helper.RunDDHelper();
}
if (count == 600)
@@ -545,7 +545,7 @@ namespace MouseWithoutBorders
if (Common.ToggleIcons != null)
{
Common.ToggleIcon();
Helper.ToggleIcon();
}
if (count % 20 == 0)
@@ -578,13 +578,13 @@ namespace MouseWithoutBorders
}
else if ((count % 36005) == 0)
{// One hour
Common.SaveSwitchCount();
Event.SaveSwitchCount();
int rv = 0;
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && Common.IsMyDesktopActive() && (rv = Common.SendMessageToHelper(0x400, IntPtr.Zero, IntPtr.Zero)) <= 0)
if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && Common.IsMyDesktopActive() && (rv = Helper.SendMessageToHelper(0x400, IntPtr.Zero, IntPtr.Zero)) <= 0)
{
Logger.TelemetryLogTrace($"{Common.HELPER_FORM_TEXT} not found: {rv}", SeverityLevel.Warning);
Logger.TelemetryLogTrace($"{Helper.HELPER_FORM_TEXT} not found: {rv}", SeverityLevel.Warning);
}
}
}
@@ -776,7 +776,7 @@ namespace MouseWithoutBorders
break;
case NativeMethods.WM_HIDE_DRAG_DROP:
Common.MainFormDot();
Helper.MainFormDot();
/*
this.Width = 1;
@@ -795,14 +795,14 @@ namespace MouseWithoutBorders
break;
case NativeMethods.WM_HIDE_DD_HELPER:
Common.MainForm3Pixels();
Helper.MainForm3Pixels();
Common.MMSleep(0.2);
if (m.WParam.ToInt32() == 1)
{
InputSimulation.MouseUp(); // A file is being dragged
}
IntPtr h = (IntPtr)NativeMethods.FindWindow(null, Common.HELPER_FORM_TEXT);
IntPtr h = (IntPtr)NativeMethods.FindWindow(null, Helper.HELPER_FORM_TEXT);
if (h.ToInt32() > 0)
{
@@ -831,7 +831,7 @@ namespace MouseWithoutBorders
case WM_QUERYENDSESSION:
Logger.LogDebug("WM_QUERYENDSESSION...");
Common.StartServiceAndSendLogoffSignal();
Service.StartServiceAndSendLogoffSignal();
break;
case WM_ENDSESSION:

View File

@@ -99,6 +99,111 @@ LegalKeyDictionary = Concurrent.ConcurrentDictionary`2[System.String,System.Byte
--_budget = ????????????
--_growLockArray = True
--_comparerIsDefaultForClasses = False
initDone = False
REOPEN_WHEN_WSAECONNRESET = -10054
REOPEN_WHEN_HOTKEY = -10055
PleaseReopenSocket = 0
ReopenSocketDueToReadError = False
<LastResumeSuspendTime>k__BackingField = ????????????
--_dateData = ????????????
--MinValue = 01/01/0001 00:00:00
--MaxValue = 31/12/9999 23:59:59
--UnixEpoch = 01/01/1970 00:00:00
lastReleaseAllKeysCall = 0
PackageSent = MouseWithoutBorders.PackageMonitor
--Keyboard = 0
--Mouse = 0
--Heartbeat = 0
--ByeBye = 0
--Hello = 0
--Matrix = 0
--ClipboardText = 0
--ClipboardImage = 0
--Clipboard = 0
--ClipboardDragDrop = 0
--ClipboardDragDropEnd = 0
--ClipboardAsk = 0
--ExplorerDragDrop = 0
--Nil = 0
PackageReceived = MouseWithoutBorders.PackageMonitor
--Keyboard = 0
--Mouse = 0
--Heartbeat = 0
--ByeBye = 0
--Hello = 0
--Matrix = 0
--ClipboardText = 0
--ClipboardImage = 0
--Clipboard = 0
--ClipboardDragDrop = 0
--ClipboardDragDropEnd = 0
--ClipboardAsk = 0
--ExplorerDragDrop = 0
--Nil = 0
PackageID = 0
SensitivePoints = Generic.List`1[Point]
--_items = Point[]
----System.Drawing.Point[] = Point[]: N/A
--_size = 0
--_version = 0
--s_emptyArray = Point[]
----System.Drawing.Point[] = Point[]: N/A
p = {X=0,Y=0}
--x = 0
--y = 0
--Empty = {X=0,Y=0}
<IpcChannelCreated>k__BackingField = False
BIG_CLIPBOARD_DATA_TIMEOUT = 30000
MAX_CLIPBOARD_DATA_SIZE_CAN_BE_SENT_INSTANTLY_TCP = 1048576
MAX_CLIPBOARD_FILE_SIZE_CAN_BE_SENT = 104857600
TEXT_HEADER_SIZE = 12
DATA_SIZE = 48
TEXT_TYPE_SEP = {4CFF57F7-BEDD-43d5-AE8F-27A61E886F2F}
TOGGLE_ICONS_SIZE = 4
ICON_ONE = 0
ICON_ALL = 1
ICON_SMALL_CLIPBOARD = 2
ICON_BIG_CLIPBOARD = 3
ICON_ERROR = 4
JUST_GOT_BACK_FROM_SCREEN_SAVER = 9999
NETWORK_STREAM_BUF_SIZE = 1048576
SymAlBlockSize = 16
PW_LENGTH = 16
PACKAGE_SIZE = 32
PACKAGE_SIZE_EX = 64
WP_PACKAGE_SIZE = 6
KEYEVENTF_KEYDOWN = 1
KEYEVENTF_KEYUP = 2
WH_MOUSE = 7
WH_KEYBOARD = 2
WH_MOUSE_LL = 14
WH_KEYBOARD_LL = 13
WM_MOUSEMOVE = 512
WM_LBUTTONDOWN = 513
WM_RBUTTONDOWN = 516
WM_MBUTTONDOWN = 519
WM_XBUTTONDOWN = 523
WM_LBUTTONUP = 514
WM_RBUTTONUP = 517
WM_MBUTTONUP = 520
WM_XBUTTONUP = 524
WM_LBUTTONDBLCLK = 515
WM_RBUTTONDBLCLK = 518
WM_MBUTTONDBLCLK = 521
WM_MOUSEWHEEL = 522
WM_KEYDOWN = 256
WM_KEYUP = 257
WM_SYSKEYDOWN = 260
WM_SYSKEYUP = 261
[DragDrop]
===============
isDragging = False
dragDropStep05ExCalledByIpc = 0
isDropping = False
dragMachine = NONE
<MouseDown>k__BackingField = False
[Event]
===============
KeybdPackage = MouseWithoutBorders.DATA
--Type = 0
--Id = 0
@@ -144,113 +249,15 @@ actualLastPos = {X=0,Y=0}
--Empty = {X=0,Y=0}
myLastX = 0
myLastY = 0
[Helper]
===============
signalHelperToExit = False
signalWatchDogToExit = False
WndProcCounter = 0
initDone = False
REOPEN_WHEN_WSAECONNRESET = -10054
REOPEN_WHEN_HOTKEY = -10055
PleaseReopenSocket = 0
ReopenSocketDueToReadError = False
<LastResumeSuspendTime>k__BackingField = ????????????
--_dateData = ????????????
--MinValue = 01/01/0001 00:00:00
--MaxValue = 31/12/9999 23:59:59
--UnixEpoch = 01/01/1970 00:00:00
lastReleaseAllKeysCall = 0
PackageSent = MouseWithoutBorders.PackageMonitor
--Keyboard = 0
--Mouse = 0
--Heartbeat = 0
--ByeBye = 0
--Hello = 0
--Matrix = 0
--ClipboardText = 0
--ClipboardImage = 0
--Clipboard = 0
--ClipboardDragDrop = 0
--ClipboardDragDropEnd = 0
--ClipboardAsk = 0
--ExplorerDragDrop = 0
--Nil = 0
PackageReceived = MouseWithoutBorders.PackageMonitor
--Keyboard = 0
--Mouse = 0
--Heartbeat = 0
--ByeBye = 0
--Hello = 0
--Matrix = 0
--ClipboardText = 0
--ClipboardImage = 0
--Clipboard = 0
--ClipboardDragDrop = 0
--ClipboardDragDropEnd = 0
--ClipboardAsk = 0
--ExplorerDragDrop = 0
--Nil = 0
PackageID = 0
shownErrMessage = False
lastStartServiceTime = ????????????
--_dateData = ????????????
--MinValue = 01/01/0001 00:00:00
--MaxValue = 31/12/9999 23:59:59
--UnixEpoch = 01/01/1970 00:00:00
SensitivePoints = Generic.List`1[Point]
--_items = Point[]
----System.Drawing.Point[] = Point[]: N/A
--_size = 0
--_version = 0
--s_emptyArray = Point[]
----System.Drawing.Point[] = Point[]: N/A
p = {X=0,Y=0}
--x = 0
--y = 0
--Empty = {X=0,Y=0}
<IpcChannelCreated>k__BackingField = False
BIG_CLIPBOARD_DATA_TIMEOUT = 30000
MAX_CLIPBOARD_DATA_SIZE_CAN_BE_SENT_INSTANTLY_TCP = 1048576
MAX_CLIPBOARD_FILE_SIZE_CAN_BE_SENT = 104857600
TEXT_HEADER_SIZE = 12
DATA_SIZE = 48
TEXT_TYPE_SEP = {4CFF57F7-BEDD-43d5-AE8F-27A61E886F2F}
TOGGLE_ICONS_SIZE = 4
ICON_ONE = 0
ICON_ALL = 1
ICON_SMALL_CLIPBOARD = 2
ICON_BIG_CLIPBOARD = 3
ICON_ERROR = 4
JUST_GOT_BACK_FROM_SCREEN_SAVER = 9999
NETWORK_STREAM_BUF_SIZE = 1048576
SymAlBlockSize = 16
PW_LENGTH = 16
HELPER_FORM_TEXT = Mouse without Borders Helper
HelperProcessName = PowerToys.MouseWithoutBordersHelper
PACKAGE_SIZE = 32
PACKAGE_SIZE_EX = 64
WP_PACKAGE_SIZE = 6
KEYEVENTF_KEYDOWN = 1
KEYEVENTF_KEYUP = 2
WH_MOUSE = 7
WH_KEYBOARD = 2
WH_MOUSE_LL = 14
WH_KEYBOARD_LL = 13
WM_MOUSEMOVE = 512
WM_LBUTTONDOWN = 513
WM_RBUTTONDOWN = 516
WM_MBUTTONDOWN = 519
WM_XBUTTONDOWN = 523
WM_LBUTTONUP = 514
WM_RBUTTONUP = 517
WM_MBUTTONUP = 520
WM_XBUTTONUP = 524
WM_LBUTTONDBLCLK = 515
WM_RBUTTONDBLCLK = 518
WM_MBUTTONDBLCLK = 521
WM_MOUSEWHEEL = 522
WM_KEYDOWN = 256
WM_KEYUP = 257
WM_SYSKEYDOWN = 260
WM_SYSKEYUP = 261
[Launch]
===============
[Logger]
===============
AllLogsLock = Lock
@@ -319,13 +326,6 @@ MAX_LOG = 10000
MaxLogExceptionPerHour = 1000
HeaderSENT = Be{0},Ke{1},Mo{2},He{3},Mx{4},Tx{5},Im{6},By{7},Cl{8},Dr{9},De{10},Ed{11},Ie{12},Ni{13}
HeaderRECEIVED = Be{0},Ke{1},Mo{2},He{3},Mx{4},Tx{5},Im{6},By{7},Cl{8},Dr{9},De{10},Ed{11},In{12},Ni{13},Pc{14}/{15}
[DragDrop]
===============
isDragging = False
dragDropStep05ExCalledByIpc = 0
isDropping = False
dragMachine = NONE
<MouseDown>k__BackingField = False
[MachineStuff]
===============
McMatrixLock = Lock
@@ -424,3 +424,11 @@ lastXY = {X=0,Y=0}
--x = 0
--y = 0
--Empty = {X=0,Y=0}
[Service]
===============
shownErrMessage = False
lastStartServiceTime = ????????????
--_dateData = ????????????
--MinValue = 01/01/0001 00:00:00
--MaxValue = 31/12/9999 23:59:59
--UnixEpoch = 01/01/1970 00:00:00

View File

@@ -114,18 +114,11 @@ public static class LoggerTests
using var streamReader = new StreamReader(stream);
var expected = streamReader.ReadToEnd();
// copied from DumpObjects in Common.Log.cs
// copied from DumpObjects in Logger.cs
var sb = new StringBuilder(1000000);
_ = Logger.PrivateDump(sb, Logger.AllLogs, "[Program logs]\r\n===============\r\n", 0, settingsDumpObjectsLevel, false);
_ = Logger.PrivateDump(sb, new Common(), "[Other Logs]\r\n===============\r\n", 0, settingsDumpObjectsLevel, false);
sb.AppendLine("[Logger]\r\n===============");
Logger.DumpType(sb, typeof(Logger), 0, settingsDumpObjectsLevel);
sb.AppendLine("[DragDrop]\r\n===============");
Logger.DumpType(sb, typeof(DragDrop), 0, settingsDumpObjectsLevel);
sb.AppendLine("[MachineStuff]\r\n===============");
Logger.DumpType(sb, typeof(MachineStuff), 0, settingsDumpObjectsLevel);
sb.AppendLine("[Receiver]\r\n===============");
Logger.DumpType(sb, typeof(Receiver), 0, settingsDumpObjectsLevel);
Logger.DumpProgramLogs(sb, settingsDumpObjectsLevel);
Logger.DumpOtherLogs(sb, settingsDumpObjectsLevel);
Logger.DumpStaticTypes(sb, settingsDumpObjectsLevel);
var actual = sb.ToString();
expected = NormalizeLog(expected);

View File

@@ -83,6 +83,8 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="..\NewShellExtensionContextMenu\constants.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_filesystem.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_variables.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\new_utilities.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\settings.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\shell_context_sub_menu.h" />
@@ -97,6 +99,7 @@
<ClInclude Include="shell_context_menu_win10.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\powertoys_module.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\settings.cpp" />

View File

@@ -57,6 +57,12 @@
<ClInclude Include="..\NewShellExtensionContextMenu\settings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_filesystem.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\NewShellExtensionContextMenu\helpers_variables.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -92,6 +98,9 @@
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files\new.rc">

View File

@@ -32,7 +32,7 @@ IFACEMETHODIMP shell_context_menu_win10::Initialize(PCIDLIST_ABSOLUTE, IDataObje
IFACEMETHODIMP shell_context_menu_win10::QueryContextMenu(HMENU menu_handle, UINT menu_index, UINT menu_first_cmd_id, UINT, UINT menu_flags)
{
if (!NewSettingsInstance().GetEnabled()
|| package::IsWin11OrGreater()
|| package::IsWin11OrGreater()
)
{
return E_FAIL;
@@ -47,7 +47,7 @@ IFACEMETHODIMP shell_context_menu_win10::QueryContextMenu(HMENU menu_handle, UIN
{
// Create the initial context popup menu containing the list of templates and open templates action
int menu_id = menu_first_cmd_id;
MENUITEMINFO newplus_main_context_menu_item;
MENUITEMINFO newplus_main_context_menu_item = { 0 };
HMENU sub_menu_of_templates = CreatePopupMenu();
int sub_menu_index = 0;
@@ -142,7 +142,7 @@ void shell_context_menu_win10::add_open_templates_to_context_menu(HMENU sub_menu
wchar_t menu_name_open[256] = { 0 };
wcscpy_s(menu_name_open, ARRAYSIZE(menu_name_open), localized_context_menu_item_open_templates.c_str());
const auto open_folder_item = Make<template_folder_context_menu_item>(template_folder_root);
MENUITEMINFO newplus_menu_item_open_templates;
MENUITEMINFO newplus_menu_item_open_templates = { 0 };
newplus_menu_item_open_templates.cbSize = sizeof(MENUITEMINFO);
newplus_menu_item_open_templates.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
newplus_menu_item_open_templates.wID = menu_id;
@@ -174,7 +174,7 @@ void shell_context_menu_win10::add_open_templates_to_context_menu(HMENU sub_menu
void shell_context_menu_win10::add_separator_to_context_menu(HMENU sub_menu_of_templates, int sub_menu_index)
{
MENUITEMINFO menu_item_separator;
MENUITEMINFO menu_item_separator = { 0 };
menu_item_separator.cbSize = sizeof(MENUITEMINFO);
menu_item_separator.fMask = MIIM_FTYPE;
menu_item_separator.fType = MFT_SEPARATOR;
@@ -184,8 +184,11 @@ void shell_context_menu_win10::add_separator_to_context_menu(HMENU sub_menu_of_t
void shell_context_menu_win10::add_template_item_to_context_menu(HMENU sub_menu_of_templates, int sub_menu_index, newplus::template_item* const template_item, int menu_id, int index)
{
wchar_t menu_name[256] = { 0 };
wcscpy_s(menu_name, ARRAYSIZE(menu_name), template_item->get_menu_title(!utilities::get_newplus_setting_hide_extension(), !utilities::get_newplus_setting_hide_starting_digits()).c_str());
MENUITEMINFO newplus_menu_item_template;
wcscpy_s(menu_name, ARRAYSIZE(menu_name), template_item->get_menu_title(
!utilities::get_newplus_setting_hide_extension(),
!utilities::get_newplus_setting_hide_starting_digits(),
utilities::get_newplus_setting_resolve_variables()).c_str());
MENUITEMINFO newplus_menu_item_template = { 0 };
newplus_menu_item_template.cbSize = sizeof(MENUITEMINFO);
newplus_menu_item_template.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_DATA;
newplus_menu_item_template.wID = menu_id;

View File

@@ -114,6 +114,8 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="dll_main.h" />
<ClInclude Include="helpers_filesystem.h" />
<ClInclude Include="helpers_variables.h" />
<ClInclude Include="shell_context_menu.h" />
<ClInclude Include="shell_context_sub_menu.h" />
<ClInclude Include="shell_context_sub_menu_item.h" />
@@ -128,6 +130,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
<ClInclude Include="template_item.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
<ClCompile Include="new_utilities.cpp" />
<ClCompile Include="shell_context_menu.cpp" />
<ClCompile Include="shell_context_sub_menu.cpp" />

View File

@@ -34,6 +34,9 @@
<ClCompile Include="new_utilities.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="template_folder.h">
@@ -75,6 +78,12 @@
<ClInclude Include="resource.base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="helpers_filesystem.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="helpers_variables.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -14,6 +14,8 @@ namespace newplus::constants::non_localizable
constexpr WCHAR settings_json_key_hide_starting_digits[] = L"HideStartingDigits";
constexpr WCHAR settings_json_key_replace_variables[] = L"ReplaceVariables";
constexpr WCHAR settings_json_key_template_location[] = L"TemplateLocation";
constexpr WCHAR context_menu_package_name[] = L"NewPlusContextMenu";
@@ -30,5 +32,5 @@ namespace newplus::constants::non_localizable
constexpr WCHAR open_templates_icon_dark_resource_relative_path[] = L"\\Assets\\NewPlus\\Open_templates_dark.ico";
constexpr WCHAR desktop_ini_filename[] = L"desktop.ini";
constexpr WCHAR parent_folder_name_variable[] = L"$PARENT_FOLDER_NAME";
}

View File

@@ -0,0 +1,59 @@
#pragma once
#include "helpers_variables.h"
namespace newplus::helpers::filesystem
{
namespace constants::non_localizable
{
constexpr WCHAR desktop_ini_filename[] = L"desktop.ini";
}
inline bool is_hidden(const std::filesystem::path path)
{
const std::filesystem::path::string_type name = path.filename();
if (name == constants::non_localizable::desktop_ini_filename)
{
return true;
}
return false;
}
inline bool is_directory(const std::filesystem::path path)
{
const auto entry = std::filesystem::directory_entry(path);
return entry.is_directory();
}
inline std::wstring make_valid_filename(const std::wstring& string, const wchar_t replace_with = L' ')
{
// replace all non-filename-valid chars with replace_with wchar
std::wstring valid_filename = string;
std::replace_if(valid_filename.begin(), valid_filename.end(), [](wchar_t c) { return c == L'/' || c == L'\\' || c == L':' || c == L'*' || c == L'?' || c == L'"' || c == L'<' || c == L'>' || c == L'|'; }, replace_with);
return valid_filename;
}
inline std::wstring make_unique_path_name(const std::wstring& initial_path)
{
std::filesystem::path folder_path(initial_path);
std::filesystem::path path_based_on(initial_path);
int counter = 1;
while (std::filesystem::exists(folder_path))
{
std::wstring new_filename = path_based_on.stem().wstring() + L" (" + std::to_wstring(counter) + L")";
if (path_based_on.has_extension())
{
new_filename += path_based_on.extension().wstring();
}
folder_path = path_based_on.parent_path() / new_filename;
counter++;
}
return folder_path.wstring();
}
}

View File

@@ -0,0 +1,169 @@
#pragma once
#include <regex>
#include "..\..\powerrename\lib\Helpers.h"
#include "helpers_filesystem.h"
#pragma comment(lib, "Pathcch.lib")
namespace newplus::helpers::variables
{
inline std::wstring resolve_an_environment_variable(const std::wstring& string)
{
std::wstring return_string = string;
wchar_t* env_variable = nullptr;
_wdupenv_s(&env_variable, nullptr, return_string.c_str());
if (env_variable != nullptr)
{
return_string = env_variable;
free(env_variable);
}
return return_string;
}
inline std::wstring resolve_date_time_variables(const std::wstring& string)
{
SYSTEMTIME local_now = { 0 };
GetLocalTime(&local_now);
wchar_t resolved_filename[MAX_PATH] = { 0 };
GetDatedFileName(resolved_filename, ARRAYSIZE(resolved_filename), string.c_str(), local_now);
return resolved_filename;
}
inline std::wstring replace_all_occurrences(const std::wstring& string, const std::wstring& search_for, const std::wstring& replacement)
{
std::wstring return_string = string;
size_t pos = 0;
while ((pos = return_string.find(search_for, pos)) != std::wstring::npos)
{
return_string.replace(pos, search_for.length(), replacement);
pos += replacement.length();
}
return return_string;
}
inline std::wstring resolve_environment_variables(const std::wstring& string)
{
// Do case-insensitive string replacement of environment variables being consistent with normal %eNV_VaR% behavior
std::wstring return_string = string;
const std::wregex reg_expression(L"%([^%]+)%");
std::wsmatch match;
size_t start = 0;
while (std::regex_search(return_string.cbegin() + start, return_string.cend(), match, reg_expression))
{
std::wstring env_var_name = match[1].str();
std::wstring env_var_value = resolve_an_environment_variable(env_var_name);
if (!env_var_value.empty())
{
size_t match_position = match.position(0) + start;
return_string.replace(match_position, match.length(0), env_var_value);
start = match_position + env_var_value.length();
}
else
{
start += match.position(0) + match.length(0);
}
}
return return_string;
}
inline std::wstring resolve_parent_folder(const std::wstring& string, const std::wstring& parent_folder_name)
{
// Do case-sensitive string replacement, for consistency on variables designated with $
std::wstring result = replace_all_occurrences(string, constants::non_localizable::parent_folder_name_variable, parent_folder_name);
return result;
}
inline std::filesystem::path resolve_variables_in_filename(const std::wstring& filename, const std::wstring& parent_folder_name)
{
std::wstring result;
result = resolve_date_time_variables(filename);
result = resolve_environment_variables(result);
if (!parent_folder_name.empty())
{
result = resolve_parent_folder(result, parent_folder_name);
}
result = newplus::helpers::filesystem::make_valid_filename(result);
return result;
}
inline std::filesystem::path resolve_variables_in_path(const std::filesystem::path& path)
{
// Need to resolve the whole path top-down (root to leaf), because of the support for $PARENT_FOLDER_NAME
std::filesystem::path result;
std::wstring previous_section;
std::wstring current_section;
auto path_section = path.begin();
int level = 0;
while (path_section != path.end())
{
previous_section = current_section;
current_section = path_section->wstring();
if (level <= 1)
{
// Up to and including L"x:\\"
result /= current_section;
}
else
{
// Past L"x:\\", e.g. L"x:\\level1" and beyond
result /= resolve_variables_in_filename(current_section, previous_section);
}
path_section++;
level++;
}
return result;
}
inline void resolve_variables_in_filename_and_rename_files(const std::filesystem::path& path, const bool do_rename = true)
{
// Depth first recursion, so that we start renaming the leaves, and avoid having to rescan
for (const auto& entry : std::filesystem::directory_iterator(path))
{
if (std::filesystem::is_directory(entry.status()))
{
resolve_variables_in_filename_and_rename_files(entry.path(), do_rename);
}
}
// Perform the actual rename
for (const auto& current : std::filesystem::directory_iterator(path))
{
if (!newplus::helpers::filesystem::is_hidden(current))
{
const std::filesystem::path resolved_path = resolve_variables_in_path(current.path());
// Only rename if the filename is actually different
const std::wstring non_resolved_leaf = current.path().filename();
const std::wstring resolved_leaf = resolved_path.filename();
if (StrCmpIW(non_resolved_leaf.c_str(), resolved_leaf.c_str()) != 0)
{
const std::wstring org_name = current.path();
const std::wstring new_name = current.path().parent_path() / resolved_leaf;
const std::wstring really_new_name = helpers::filesystem::make_unique_path_name(new_name);
// To aid with testing, only conditionally rename
if (do_rename)
{
std::filesystem::rename(org_name, really_new_name);
}
}
}
}
}
}

View File

@@ -9,6 +9,7 @@
#include "settings.h"
#include "template_item.h"
#include "trace.h"
#include "helpers_variables.h"
#pragma comment(lib, "Shlwapi.lib")
@@ -72,23 +73,6 @@ namespace newplus::utilities
return hIcon;
}
inline bool is_hidden(const std::filesystem::path path)
{
const std::filesystem::path::string_type name = path.filename();
if (name == constants::non_localizable::desktop_ini_filename)
{
return true;
}
return false;
}
inline bool is_directory(const std::filesystem::path path)
{
const auto entry = std::filesystem::directory_entry(path);
return entry.is_directory();
}
inline bool wstring_same_when_comparing_ignore_case(std::wstring stringA, std::wstring stringB)
{
transform(stringA.begin(), stringA.end(), stringA.begin(), towupper);
@@ -97,20 +81,6 @@ namespace newplus::utilities
return (stringA == stringB);
}
inline void process_pending_window_messages(HWND window_handle = NULL)
{
if (window_handle == NULL)
{
window_handle = GetActiveWindow();
}
MSG current_message;
while (PeekMessage(&current_message, window_handle, NULL, NULL, PM_REMOVE))
{
DispatchMessage(&current_message);
}
}
inline std::wstring get_new_template_folder_location()
{
return NewSettingsInstance().GetTemplateLocation();
@@ -126,6 +96,11 @@ namespace newplus::utilities
return NewSettingsInstance().GetHideStartingDigits();
}
inline bool get_newplus_setting_resolve_variables()
{
return NewSettingsInstance().GetReplaceVariables();
}
inline void create_folder_if_not_exist(const std::filesystem::path path)
{
std::filesystem::create_directory(path);
@@ -259,6 +234,7 @@ namespace newplus::utilities
{
ComPtr<IWebBrowserApp> web_browser_app;
VARIANT v;
VariantInit(&v);
V_VT(&v) = VT_I4;
V_I4(&v) = i;
hr = shell_windows->Item(v, &shell_window);
@@ -382,15 +358,30 @@ namespace newplus::utilities
std::filesystem::path source_fullpath = template_entry->path;
std::filesystem::path target_fullpath = std::wstring(target_path_name);
// Only append name to target if source is not a directory
if (!utilities::is_directory(source_fullpath))
// Get target name without starting digits as appropriate
const std::wstring target_name = template_entry->get_target_filename(!utilities::get_newplus_setting_hide_starting_digits());
// Get initial resolved name
target_fullpath /= target_name;
// Expand variables in name of the target path
if (utilities::get_newplus_setting_resolve_variables())
{
target_fullpath.append(template_entry->get_target_filename(!utilities::get_newplus_setting_hide_starting_digits()));
target_fullpath = helpers::variables::resolve_variables_in_path(target_fullpath);
}
// Copy file and determine final filename
// See if our target already exist, and if so then generate a unique name
target_fullpath = helpers::filesystem::make_unique_path_name(target_fullpath);
// Finally copy file/folder/subfolders
std::filesystem::path target_final_fullpath = template_entry->copy_object_to(GetActiveWindow(), target_fullpath);
// Resolve variables and rename files in newly copied folders and subfolders and files
if (utilities::get_newplus_setting_resolve_variables() && helpers::filesystem::is_directory(target_final_fullpath))
{
helpers::variables::resolve_variables_in_filename_and_rename_files(target_final_fullpath);
}
// Touch all files and set last modified to "now"
update_last_write_time(target_final_fullpath);

View File

@@ -43,6 +43,7 @@ void NewSettings::Save()
values.add_property(newplus::constants::non_localizable::settings_json_key_hide_file_extension, new_settings.hide_file_extension);
values.add_property(newplus::constants::non_localizable::settings_json_key_hide_starting_digits, new_settings.hide_starting_digits);
values.add_property(newplus::constants::non_localizable::settings_json_key_replace_variables, new_settings.replace_variables);
values.add_property(newplus::constants::non_localizable::settings_json_key_template_location, new_settings.template_location);
values.save_to_settings_file();
@@ -70,6 +71,9 @@ void NewSettings::InitializeWithDefaultSettings()
// Currently a similar defaulting logic is also in InitializeWithDefaultSettings in NewViewModel.cs
SetHideFileExtension(true);
// By default Replace Variables is turned off
SetReplaceVariables(false);
SetTemplateLocation(GetTemplateLocationDefaultPath());
}
@@ -139,6 +143,12 @@ void NewSettings::ParseJson()
new_settings.hide_starting_digits = hideStartingDigitsValue.value();
}
auto resolveVariables = settings.get_bool_value(newplus::constants::non_localizable::settings_json_key_replace_variables);
if (resolveVariables.has_value())
{
new_settings.replace_variables = resolveVariables.value();
}
GetSystemTimeAsFileTime(&new_settings_last_loaded_timestamp);
}
@@ -163,11 +173,8 @@ bool NewSettings::GetEnabled()
bool NewSettings::GetHideFileExtension() const
{
auto gpoSetting = powertoys_gpo::getConfiguredNewPlusHideTemplateFilenameExtensionValue();
if (gpoSetting == powertoys_gpo::gpo_rule_configured_enabled)
{
return true;
}
const auto gpoSetting = powertoys_gpo::getConfiguredNewPlusHideTemplateFilenameExtensionValue();
if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled)
{
return false;
@@ -191,6 +198,23 @@ void NewSettings::SetHideStartingDigits(const bool hide_starting_digits)
new_settings.hide_starting_digits = hide_starting_digits;
}
bool NewSettings::GetReplaceVariables() const
{
const auto gpoSetting = powertoys_gpo::getConfiguredNewPlusReplaceVariablesValue();
if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled)
{
return false;
}
return new_settings.replace_variables;
}
void NewSettings::SetReplaceVariables(const bool replace_variables)
{
new_settings.replace_variables = replace_variables;
}
std::wstring NewSettings::GetTemplateLocation() const
{
return new_settings.template_location;
@@ -201,7 +225,7 @@ void NewSettings::SetTemplateLocation(const std::wstring template_location)
new_settings.template_location = template_location;
}
std::wstring NewSettings::GetTemplateLocationDefaultPath()
std::wstring NewSettings::GetTemplateLocationDefaultPath() const
{
static const std::wstring default_template_sub_folder_name =
GET_RESOURCE_STRING_FALLBACK(

View File

@@ -12,6 +12,8 @@ public:
void SetHideFileExtension(const bool hide_file_extension);
bool GetHideStartingDigits() const;
void SetHideStartingDigits(const bool hide_starting_digits);
bool GetReplaceVariables() const;
void SetReplaceVariables(const bool resolve_variables);
std::wstring GetTemplateLocation() const;
void SetTemplateLocation(const std::wstring template_location);
@@ -25,12 +27,13 @@ private:
bool enabled{ false };
bool hide_file_extension{ true };
bool hide_starting_digits{ true };
bool replace_variables{ true };
std::wstring template_location;
};
void RefreshEnabledState();
void InitializeWithDefaultSettings();
std::wstring GetTemplateLocationDefaultPath();
std::wstring GetTemplateLocationDefaultPath() const;
void Reload();
void ParseJson();

View File

@@ -69,8 +69,21 @@ IFACEMETHODIMP shell_context_menu::GetFlags(_Out_ EXPCMDFLAGS* returned_menu_ite
IFACEMETHODIMP shell_context_menu::EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** returned_enum_commands)
{
auto e = Make<shell_context_sub_menu>(site_of_folder);
return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands));
try
{
auto e = Make<shell_context_sub_menu>(site_of_folder);
return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands));
}
catch (const std::exception& ex)
{
Logger::error("New+ create submenu error: {}", ex.what());
return E_FAIL;
}
catch (...)
{
Logger::error("New+ create submenu error");
return E_FAIL;
}
}
#pragma endregion
@@ -80,8 +93,8 @@ IFACEMETHODIMP shell_context_menu::SetSite(_In_ IUnknown* site) noexcept
this->site_of_folder = site;
return S_OK;
}
IFACEMETHODIMP shell_context_menu::GetSite(_In_ REFIID riid, _COM_Outptr_ void** returned_site) noexcept
IFACEMETHODIMP shell_context_menu::GetSite(_In_ REFIID interface_type, _COM_Outptr_ void** returned_site) noexcept
{
return this->site_of_folder.CopyTo(riid, returned_site);
return this->site_of_folder.CopyTo(interface_type, returned_site);
}
#pragma endregion

View File

@@ -27,7 +27,7 @@ public:
#pragma region IObjectWithSite
IFACEMETHODIMP SetSite(_In_ IUnknown* site) noexcept;
IFACEMETHODIMP GetSite(_In_ REFIID riid, _COM_Outptr_ void** site) noexcept;
IFACEMETHODIMP GetSite(_In_ REFIID interface_type, _COM_Outptr_ void** site) noexcept;
#pragma endregion
protected:

View File

@@ -22,7 +22,8 @@ IFACEMETHODIMP shell_context_sub_menu_item::GetTitle(_In_opt_ IShellItemArray* i
{
return SHStrDup(this->template_entry->get_menu_title(
!utilities::get_newplus_setting_hide_extension(),
!utilities::get_newplus_setting_hide_starting_digits()
!utilities::get_newplus_setting_hide_starting_digits(),
utilities::get_newplus_setting_resolve_variables()
).c_str(), title);
}
@@ -95,6 +96,7 @@ IFACEMETHODIMP separator_context_menu_item::GetIcon(_In_opt_ IShellItemArray*, _
IFACEMETHODIMP separator_context_menu_item::GetFlags(_Out_ EXPCMDFLAGS* returned_flags)
{
// Separators no longer work on Windows 11 regular context menu. They do still work on the extended context menu.
*returned_flags = ECF_ISSEPARATOR;
return S_OK;
}

View File

@@ -35,7 +35,7 @@ void template_folder::rescan_template_folder()
}
else
{
if (!utilities::is_hidden(entry.path()))
if (!helpers::filesystem::is_hidden(entry.path()))
{
files.push_back({ entry.path().wstring(), new template_item(entry) });
}

View File

@@ -6,7 +6,6 @@
#include "new_utilities.h"
#include <cassert>
#include <thread>
#include <shellapi.h>
#include <shlobj_core.h>
using namespace Microsoft::WRL;
@@ -17,7 +16,7 @@ template_item::template_item(const std::filesystem::path entry)
path = entry;
}
std::wstring template_item::get_menu_title(const bool show_extension, const bool show_starting_digits) const
std::wstring template_item::get_menu_title(const bool show_extension, const bool show_starting_digits, const bool show_resolved_variables) const
{
std::wstring title = path.filename();
@@ -27,13 +26,21 @@ std::wstring template_item::get_menu_title(const bool show_extension, const bool
title = remove_starting_digits_from_filename(title);
}
if (show_resolved_variables)
{
title = helpers::variables::resolve_variables_in_filename(title, constants::non_localizable::parent_folder_name_variable);
}
if (show_extension || !path.has_extension())
{
return title;
}
std::wstring ext = path.extension();
title = title.substr(0, title.length() - ext.length());
if (!helpers::filesystem::is_directory(path))
{
std::wstring ext = path.extension();
title = title.substr(0, title.length() - ext.length());
}
return title;
}
@@ -53,7 +60,8 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi
std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const
{
filename.erase(0, min(filename.find_first_not_of(L"0123456789 ."), filename.size()));
filename.erase(0, min(filename.find_first_not_of(L"0123456789"), filename.size()));
filename.erase(0, min(filename.find_first_not_of(L" ."), filename.size()));
return filename;
}
@@ -70,7 +78,7 @@ HICON template_item::get_explorer_icon_handle() const
std::filesystem::path template_item::copy_object_to(const HWND window_handle, const std::filesystem::path destination) const
{
// SHFILEOPSTRUCT wants the from and to paths to be terminated with two NULLs,
// SHFILEOPSTRUCT wants the from and to paths to be terminated with two NULLs.
wchar_t double_terminated_path_from[MAX_PATH + 1] = { 0 };
wcsncpy_s(double_terminated_path_from, this->path.c_str(), this->path.wstring().length());
double_terminated_path_from[this->path.wstring().length() + 1] = 0;
@@ -84,37 +92,16 @@ std::filesystem::path template_item::copy_object_to(const HWND window_handle, co
file_operation_params.hwnd = window_handle;
file_operation_params.pFrom = double_terminated_path_from;
file_operation_params.pTo = double_terminated_path_to;
file_operation_params.fFlags = FOF_RENAMEONCOLLISION | FOF_ALLOWUNDO | FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS | FOF_WANTMAPPINGHANDLE;
file_operation_params.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS;
const int result = SHFileOperation(&file_operation_params);
if (!file_operation_params.hNameMappings)
if (result != 0)
{
// No file name collision on copy
if (utilities::is_directory(this->path))
{
// Append dir for consistency on directory naming inclusion for with and without collision
std::filesystem::path with_dir = destination;
with_dir /= this->path.filename();
return with_dir;
}
return destination;
throw std::runtime_error("Failed to copy template");
}
struct file_operation_collision_mapping
{
int index;
SHNAMEMAPPING* mapping;
};
file_operation_collision_mapping* mapping = static_cast<file_operation_collision_mapping*>(file_operation_params.hNameMappings);
SHNAMEMAPPING* map = &mapping->mapping[0];
std::wstring final_path(map->pszNewPath);
SHFreeNameMappings(file_operation_params.hNameMappings);
return final_path;
return destination;
}
void template_item::refresh_target(const std::filesystem::path target_final_fullpath) const

View File

@@ -15,7 +15,7 @@ namespace newplus
public:
template_item(const std::filesystem::path entry);
std::wstring get_menu_title(const bool show_extension, const bool show_starting_digits) const;
std::wstring get_menu_title(const bool show_extension, const bool show_starting_digits, const bool show_resolved_variables) const;
std::wstring get_target_filename(const bool include_starting_digits) const;

View File

@@ -6,10 +6,10 @@ using System;
using System.Globalization;
using System.Threading;
using System.Windows;
using Common.UI;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using WorkspacesEditor.Telemetry;
using WorkspacesEditor.Utils;
using WorkspacesEditor.ViewModels;
@@ -36,6 +36,8 @@ namespace WorkspacesEditor
public App()
{
PowerToysTelemetry.Log.WriteEvent(new WorkspacesEditorStartEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
WorkspacesEditorIO = new WorkspacesEditorIO();
}

View File

@@ -10,6 +10,7 @@ using System.Windows;
using System.Windows.Interop;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using WorkspacesEditor.Telemetry;
using WorkspacesEditor.Utils;
using WorkspacesEditor.ViewModels;
@@ -87,6 +88,8 @@ namespace WorkspacesEditor
},
Application.Current.Dispatcher,
cancellationToken.Token);
PowerToysTelemetry.Log.WriteEvent(new WorkspacesEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
private bool IsEditorInsideVisibleArea()

View File

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

View File

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

View File

@@ -52,6 +52,11 @@ namespace SnapshotUtils
for (const auto window : windows)
{
if (WindowFilter::FilterPopup(window))
{
continue;
}
// filter by window rect size
RECT rect = WindowUtils::GetWindowRect(window);
if (rect.right - rect.left <= 0 || rect.bottom - rect.top <= 0)
@@ -93,7 +98,7 @@ namespace SnapshotUtils
continue;
}
// fix for the packaged apps that are not caught when minimized, e.g., Settings.
// fix for the packaged apps that are not caught when minimized, e.g. Settings, Microsoft ToDo, ...
if (processPath.ends_with(NonLocalizable::ApplicationFrameHost))
{
for (auto otherWindow : windows)
@@ -110,11 +115,6 @@ namespace SnapshotUtils
}
}
if (WindowFilter::FilterPopup(window))
{
continue;
}
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
if (!data.has_value() || data->name.empty())
{

View File

@@ -14,6 +14,11 @@
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
#include <WorkspacesLib/PwaHelper.h>
namespace NonLocalizable
{
const std::wstring ApplicationFrameHost = L"ApplicationFrameHost.exe";
}
namespace PlacementHelper
{
// When calculating the coordinates difference (== 'distance') between 2 windows, there are additional values added to the real distance
@@ -157,6 +162,11 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
for (HWND window : m_windowsBefore)
{
if (WindowFilter::FilterPopup(window))
{
continue;
}
if (std::find(movedWindows.begin(), movedWindows.end(), window) != movedWindows.end())
{
continue;
@@ -170,6 +180,24 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
std::wstring title = WindowUtils::GetWindowTitle(window);
// fix for the packaged apps that are not caught when minimized, e.g. Settings, Microsoft ToDo, ...
if (processPath.ends_with(NonLocalizable::ApplicationFrameHost))
{
for (auto otherWindow : m_windowsBefore)
{
DWORD otherPid{};
GetWindowThreadProcessId(otherWindow, &otherPid);
// searching for the window with the same title but different PID
if (pid != otherPid && title == WindowUtils::GetWindowTitle(otherWindow))
{
processPath = get_process_path(otherPid);
break;
}
}
}
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
if (!data.has_value())

View File

@@ -50,11 +50,6 @@ namespace WindowFilter
return false;
}
if (WindowFilter::FilterPopup(window))
{
return false;
}
if (!VirtualDesktop::instance().IsWindowOnCurrentDesktop(window))
{
return false;

View File

@@ -0,0 +1,44 @@
// 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 FancyZonesEditorCommon.Data
{
public enum CustomLayout
{
Canvas,
Grid,
}
public static class CustomLayoutEnumExtension
{
private const string CanvasJsonTag = "canvas";
private const string GridJsonTag = "grid";
public static string TypeToString(this CustomLayout value)
{
switch (value)
{
case CustomLayout.Canvas:
return CanvasJsonTag;
case CustomLayout.Grid:
return GridJsonTag;
}
return CanvasJsonTag;
}
public static CustomLayout TypeFromString(string value)
{
switch (value)
{
case CanvasJsonTag:
return CustomLayout.Canvas;
case GridJsonTag:
return CustomLayout.Grid;
}
return CustomLayout.Canvas;
}
}
}

View File

@@ -0,0 +1,308 @@
// 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.Globalization;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ModernWpf.Controls;
using OpenQA.Selenium;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class ApplyLayoutTests : UITestBase
{
public ApplyLayoutTests()
: base(PowerToysModule.FancyZone)
{
}
private static readonly EditorParameters.ParamsWrapper Parameters = new EditorParameters.ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<EditorParameters.NativeMonitorDataWrapper>
{
new EditorParameters.NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
new EditorParameters.NativeMonitorDataWrapper
{
Monitor = "monitor-2",
MonitorInstanceId = "instance-id-2",
MonitorSerialNumber = "serial-number-2",
MonitorNumber = 2,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 1920,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = false,
},
},
};
private static readonly CustomLayouts.CustomLayoutListWrapper CustomLayoutsList = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
{
new CustomLayouts.CustomLayoutWrapper
{
Uuid = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Custom layout",
Info = new CustomLayouts().ToJsonElement(new CustomLayouts.CanvasInfoWrapper
{
RefHeight = 952,
RefWidth = 1500,
SensitivityRadius = 10,
Zones = new List<CustomLayouts.CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
},
};
private static readonly LayoutTemplates.TemplateLayoutsListWrapper TemplateLayoutsList = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(Parameters));
LayoutTemplates layoutTemplates = new LayoutTemplates();
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(TemplateLayoutsList));
CustomLayouts customLayouts = new CustomLayouts();
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(CustomLayoutsList));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper>
{
new DefaultLayouts.DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Horizontal.TypeToString(),
Layout = new DefaultLayouts.DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 4,
ShowSpacing = true,
Spacing = 5,
SensitivityRadius = 20,
},
},
new DefaultLayouts.DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Vertical.TypeToString(),
Layout = new DefaultLayouts.DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.Custom.TypeToString(),
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
ZoneCount = 0,
ShowSpacing = false,
Spacing = 0,
SensitivityRadius = 0,
},
},
},
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void ApplyCustomLayout()
{
var layout = CustomLayoutsList.CustomLayouts[0];
Assert.IsFalse(Session.Find<Element>(layout.Name).Selected);
Session.Find<Element>(layout.Name).Click();
Assert.IsTrue(Session.Find<Element>(layout.Name).Selected);
AppliedLayouts appliedLayouts = new AppliedLayouts();
var data = appliedLayouts.Read(appliedLayouts.File);
Assert.AreEqual(Parameters.Monitors.Count, data.AppliedLayouts.Count);
Assert.AreEqual(layout.Uuid, data.AppliedLayouts[0].AppliedLayout.Uuid);
Assert.AreEqual(Parameters.Monitors[0].MonitorNumber, data.AppliedLayouts[0].Device.MonitorNumber);
}
[TestMethod]
public void ApplyTemplateLayout()
{
var layoutType = LayoutType.Columns;
var layout = TestConstants.TemplateLayoutNames[layoutType];
Assert.IsFalse(Session.Find<Element>(layout).Selected);
Session.Find<Element>(layout).Click();
Assert.IsTrue(Session.Find<Element>(layout).Selected);
AppliedLayouts appliedLayouts = new AppliedLayouts();
var data = appliedLayouts.Read(appliedLayouts.File);
Assert.AreEqual(Parameters.Monitors.Count, data.AppliedLayouts.Count);
Assert.AreEqual(layoutType.TypeToString(), data.AppliedLayouts[0].AppliedLayout.Type);
Assert.AreEqual(Parameters.Monitors[0].MonitorNumber, data.AppliedLayouts[0].Device.MonitorNumber);
}
[TestMethod]
public void ApplyLayoutsOnEachMonitor()
{
// apply the layout on the first monitor
var firstLayoutType = LayoutType.Columns;
var firstLayoutName = TestConstants.TemplateLayoutNames[firstLayoutType];
Session.Find<Element>(firstLayoutName).Click();
Assert.IsTrue(Session.Find<Element>(firstLayoutName)!.Selected);
// apply the layout on the second monitor
Session.Find<Element>(PowerToys.UITest.By.AccessibilityId("Monitors")).Find<Element>("Monitor 2").Click();
var secondLayout = CustomLayoutsList.CustomLayouts[0];
Session.Find<Element>(secondLayout.Name).Click();
Assert.IsTrue(Session.Find<Element>(secondLayout.Name)!.Selected);
// verify the layout on the first monitor wasn't changed
Session.Find<Element>(PowerToys.UITest.By.AccessibilityId("Monitors")).Find<Element>("Monitor 1").Click();
Assert.IsTrue(Session.Find<Element>(firstLayoutName)!.Selected);
// verify the file
var appliedLayouts = new AppliedLayouts();
var data = appliedLayouts.Read(appliedLayouts.File);
Assert.AreEqual(Parameters.Monitors.Count, data.AppliedLayouts.Count);
Assert.AreEqual(firstLayoutType.TypeToString(), data.AppliedLayouts.Find(x => x.Device.MonitorNumber == 1).AppliedLayout.Type);
Assert.AreEqual(secondLayout.Uuid, data.AppliedLayouts.Find(x => x.Device.MonitorNumber == 2).AppliedLayout.Uuid);
}
[TestMethod]
public void ApplyTemplateWithDifferentParametersOnEachMonitor()
{
var layoutType = LayoutType.Columns;
var layoutName = TestConstants.TemplateLayoutNames[layoutType];
// apply the layout on the first monitor, set parameters
Session.Find<Element>(layoutName).Click();
Session.Find<Element>(layoutName).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.TemplateZoneSlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
slider.SendKeys(Keys.Right);
var expectedFirstLayoutZoneCount = int.Parse(slider.Text!, CultureInfo.InvariantCulture);
Session.Find<Button>(ElementName.Save).Click();
// apply the layout on the second monitor, set different parameters
Session.Find<Element>(PowerToys.UITest.By.AccessibilityId("Monitors")).Find<Element>("Monitor 2").Click();
Session.Find<Element>(layoutName).Click();
Session.Find<Element>(layoutName).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.TemplateZoneSlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Left);
var expectedSecondLayoutZoneCount = int.Parse(slider.Text!, CultureInfo.InvariantCulture);
Session.Find<Button>(ElementName.Save).Click();
// verify the layout on the first monitor wasn't changed
Session.Find<Element>(PowerToys.UITest.By.AccessibilityId("Monitors")).Find<Element>("Monitor 1").Click();
Session.Find<Element>(layoutName).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.TemplateZoneSlider));
Assert.IsNotNull(slider);
Assert.AreEqual(expectedFirstLayoutZoneCount, int.Parse(slider.Text!, CultureInfo.InvariantCulture));
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var appliedLayouts = new AppliedLayouts();
var data = appliedLayouts.Read(appliedLayouts.File);
Assert.AreEqual(Parameters.Monitors.Count, data.AppliedLayouts.Count);
Assert.AreEqual(layoutType.TypeToString(), data.AppliedLayouts.Find(x => x.Device.MonitorNumber == 1).AppliedLayout.Type);
Assert.AreEqual(expectedFirstLayoutZoneCount, data.AppliedLayouts.Find(x => x.Device.MonitorNumber == 1).AppliedLayout.ZoneCount);
Assert.AreEqual(layoutType.TypeToString(), data.AppliedLayouts.Find(x => x.Device.MonitorNumber == 2).AppliedLayout.Type);
Assert.AreEqual(expectedSecondLayoutZoneCount, data.AppliedLayouts.Find(x => x.Device.MonitorNumber == 2).AppliedLayout.ZoneCount);
}
}
}

View File

@@ -0,0 +1,357 @@
// 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 FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class CopyLayoutTests : UITestBase
{
public CopyLayoutTests()
: base(PowerToysModule.FancyZone)
{
}
private static readonly CustomLayouts.CustomLayoutListWrapper CustomLayouts = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
{
new CustomLayouts.CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Grid.TypeToString(),
Name = "Grid custom layout",
Info = new CustomLayouts().ToJsonElement(new CustomLayouts.GridInfoWrapper
{
Rows = 2,
Columns = 3,
RowsPercentage = new List<int> { 2967, 7033 },
ColumnsPercentage = new List<int> { 2410, 6040, 1550 },
CellChildMap = new int[][] { [0, 1, 1], [0, 2, 3] },
SensitivityRadius = 30,
Spacing = 26,
ShowSpacing = false,
}),
},
},
};
private static readonly LayoutHotkeys.LayoutHotkeysWrapper Hotkeys = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper>
{
new LayoutHotkeys.LayoutHotkeyWrapper
{
LayoutId = CustomLayouts.CustomLayouts[0].Uuid,
Key = 0,
},
},
};
private static readonly DefaultLayouts.DefaultLayoutsListWrapper DefaultLayouts = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper>
{
new DefaultLayouts.DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Vertical.TypeToString(),
Layout = new DefaultLayouts.DefaultLayoutWrapper.LayoutWrapper
{
Type = "custom",
Uuid = CustomLayouts.CustomLayouts[0].Uuid,
},
},
},
};
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
CustomLayouts customLayouts = new CustomLayouts();
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(CustomLayouts));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
DefaultLayouts defaultLayouts = new DefaultLayouts();
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(DefaultLayouts));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(Hotkeys));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void CopyTemplate_FromEditLayoutWindow()
{
string copiedLayoutName = TestConstants.TemplateLayoutNames[LayoutType.Focus] + " (1)";
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Focus]).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
ClickCopyLayout();
// verify the layout is copied
Assert.IsNotNull(Session.Find<Element>(copiedLayoutName)); // new name is presented
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count + 1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Name == copiedLayoutName));
}
[TestMethod]
public void CopyTemplate_FromContextMenu()
{
string copiedLayoutName = TestConstants.TemplateLayoutNames[LayoutType.Rows] + " (1)";
FancyZonesEditorHelper.ClickContextMenuItem(Session, TestConstants.TemplateLayoutNames[LayoutType.Rows], ElementName.CreateCustomLayout);
// verify the layout is copied
Assert.IsNotNull(Session.Find<Element>(copiedLayoutName)); // new name is presented
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count + 1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Name == copiedLayoutName));
}
[TestMethod]
public void CopyTemplate_DefaultLayout()
{
string copiedLayoutName = TestConstants.TemplateLayoutNames[LayoutType.PriorityGrid] + " (1)";
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.PriorityGrid]).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
ClickCopyLayout();
// verify the layout is copied
Assert.IsNotNull(Session.Find<Element>(copiedLayoutName)); // new name is presented
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count + 1, data.CustomLayouts.Count);
// verify the default layout wasn't changed
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.PriorityGrid]).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var horizontalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonChecked));
Assert.IsNotNull(horizontalDefaultButton);
Session.Find<Button>(ElementName.Cancel).Click();
Session.Find<Element>(copiedLayoutName).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
horizontalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonUnchecked));
Assert.IsNotNull(horizontalDefaultButton);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the default layouts file wasn't changed
var defaultLayouts = new DefaultLayouts();
var defaultLayoutData = defaultLayouts.Read(defaultLayouts.File);
Assert.AreEqual(defaultLayouts.Serialize(DefaultLayouts), defaultLayouts.Serialize(defaultLayoutData));
}
[TestMethod]
public void CopyCustomLayout_FromEditLayoutWindow()
{
string copiedLayoutName = CustomLayouts.CustomLayouts[0].Name + " (1)";
Session.Find<Element>(CustomLayouts.CustomLayouts[0].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
ClickCopyLayout();
// verify the layout is copied
Assert.IsNotNull(Session.Find<Element>(copiedLayoutName)); // new name is presented
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count + 1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Name == copiedLayoutName));
}
[TestMethod]
public void CopyCustomLayout_FromContextMenu()
{
string copiedLayoutName = CustomLayouts.CustomLayouts[0].Name + " (1)";
FancyZonesEditorHelper.ClickContextMenuItem(Session, CustomLayouts.CustomLayouts[0].Name, ElementName.Duplicate);
// verify the layout is copied
Assert.IsNotNull(Session.Find<Element>(copiedLayoutName)); // new name is presented
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count + 1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Name == copiedLayoutName));
}
[TestMethod]
public void CopyCustomLayout_DefaultLayout()
{
string copiedLayoutName = CustomLayouts.CustomLayouts[0].Name + " (1)";
Session.Find<Element>(CustomLayouts.CustomLayouts[0].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
ClickCopyLayout();
// verify the layout is copied
Assert.IsNotNull(Session.Find<Element>(copiedLayoutName)); // new name is presented
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count + 1, data.CustomLayouts.Count);
// verify the default layout wasn't changed
Session.Find<Element>(CustomLayouts.CustomLayouts[0].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var verticalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonChecked));
Assert.IsNotNull(verticalDefaultButton);
Session.Find<Button>(ElementName.Cancel).Click();
Session.Find<Element>(copiedLayoutName).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
verticalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonUnchecked));
Assert.IsNotNull(verticalDefaultButton);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the default layouts file wasn't changed
var defaultLayouts = new DefaultLayouts();
var defaultLayoutData = defaultLayouts.Read(defaultLayouts.File);
Assert.AreEqual(defaultLayouts.Serialize(DefaultLayouts), defaultLayouts.Serialize(defaultLayoutData));
}
[TestMethod]
public void CopyCustomLayout_Hotkey()
{
string copiedLayoutName = CustomLayouts.CustomLayouts[0].Name + " (1)";
Session.Find<Element>(CustomLayouts.CustomLayouts[0].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
ClickCopyLayout();
// verify the layout is copied
Assert.IsNotNull(Session.Find<Element>(copiedLayoutName)); // new name is presented
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count + 1, data.CustomLayouts.Count);
// verify the hotkey wasn't changed
Session.Find<Element>(CustomLayouts.CustomLayouts[0].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
Assert.AreEqual("0", hotkeyComboBox.Text);
Session.Find<Button>(ElementName.Cancel).Click();
Session.Find<Element>(copiedLayoutName).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
Assert.AreEqual("None", hotkeyComboBox.Text);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the hotkey file wasn't changed
var hotkeys = new LayoutHotkeys();
var hotkeyData = hotkeys.Read(hotkeys.File);
Assert.AreEqual(hotkeys.Serialize(Hotkeys), hotkeys.Serialize(hotkeyData));
}
public void ClickCopyLayout()
{
if (Session.FindAll<Element>(By.AccessibilityId(AccessibilityId.CopyTemplate)).Count != 0)
{
Session.Find<Element>(By.AccessibilityId(AccessibilityId.CopyTemplate)).Click();
}
else
{
Session.Find<Element>(By.AccessibilityId(AccessibilityId.DuplicateLayoutButton)).Click();
}
}
}
}

View File

@@ -0,0 +1,240 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class CreateLayoutTests : UITestBase
{
public CreateLayoutTests()
: base(PowerToysModule.FancyZone)
{
}
[TestInitialize]
public void TestInitialize()
{
// prepare test editor parameters with 2 monitors before launching the editor
EditorParameters editorParameters = new EditorParameters();
EditorParameters.ParamsWrapper parameters = new EditorParameters.ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<EditorParameters.NativeMonitorDataWrapper>
{
new EditorParameters.NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void CreateWithDefaultName()
{
string name = "Custom layout 1";
Session.Find<Element>(By.AccessibilityId(AccessibilityId.NewLayoutButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.PrimaryButton)).Click();
Session.Find<Button>(ElementName.Save).Click();
// verify new layout presented
Assert.IsNotNull(Session.Find<Element>(name));
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Name == name));
}
[TestMethod]
public void CreateWithCustomName()
{
string name = "Layout Name";
Session.Find<Element>(By.AccessibilityId(AccessibilityId.NewLayoutButton)).Click();
var input = Session.Find<TextBox>(By.ClassName(ClassName.TextBox));
Assert.IsNotNull(input);
input.SetText(name, true);
Session.Find<Element>(By.AccessibilityId(AccessibilityId.PrimaryButton)).Click();
Session.Find<Button>(ElementName.Save).Click();
// verify new layout presented
Assert.IsNotNull(Session.Find<Element>(name));
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Name == name));
}
[TestMethod]
public void CreateGrid()
{
CustomLayout type = CustomLayout.Grid;
Session.Find<Element>(By.AccessibilityId(AccessibilityId.NewLayoutButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.GridRadioButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.PrimaryButton)).Click();
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Type == type.TypeToString()));
}
[TestMethod]
public void CreateCanvas()
{
CustomLayout type = CustomLayout.Canvas;
Session.Find<Element>(By.AccessibilityId(AccessibilityId.NewLayoutButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.CanvasRadioButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.PrimaryButton)).Click();
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(1, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Type == type.TypeToString()));
}
[TestMethod]
public void CancelGridCreation()
{
Session.Find<Element>(By.AccessibilityId(AccessibilityId.NewLayoutButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.GridRadioButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.PrimaryButton)).Click();
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(0, data.CustomLayouts.Count);
}
[TestMethod]
public void CancelCanvasCreation()
{
Session.Find<Element>(By.AccessibilityId(AccessibilityId.NewLayoutButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.CanvasRadioButton)).Click();
Session.Find<Element>(By.AccessibilityId(AccessibilityId.PrimaryButton)).Click();
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(0, data.CustomLayouts.Count);
}
}
}

View File

@@ -0,0 +1,457 @@
// 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 FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using static FancyZonesEditorCommon.Data.CustomLayouts;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class CustomLayoutsTests : UITestBase
{
public CustomLayoutsTests()
: base(PowerToysModule.FancyZone)
{
}
private static readonly CustomLayoutListWrapper Layouts = new CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayoutWrapper>
{
new CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Grid.TypeToString(),
Name = "Grid custom layout",
Info = new CustomLayouts().ToJsonElement(new GridInfoWrapper
{
Rows = 2,
Columns = 3,
RowsPercentage = new List<int> { 2967, 7033 },
ColumnsPercentage = new List<int> { 2410, 6040, 1550 },
CellChildMap = new int[][] { [0, 1, 1], [0, 2, 3] },
SensitivityRadius = 30,
Spacing = 26,
ShowSpacing = false,
}),
},
new CustomLayoutWrapper
{
Uuid = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Canvas custom layout",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 952,
RefWidth = 1500,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper>
{
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 0,
Y = 0,
Width = 900,
Height = 522,
},
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 900,
Y = 0,
Width = 600,
Height = 750,
},
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 0,
Y = 522,
Width = 1500,
Height = 430,
},
},
}),
},
new CustomLayoutWrapper
{
Uuid = "{F1A94F38-82B6-4876-A653-70D0E882DE2A}",
Type = CustomLayout.Grid.TypeToString(),
Name = "Grid custom layout spacing enabled",
Info = new CustomLayouts().ToJsonElement(new GridInfoWrapper
{
Rows = 2,
Columns = 3,
RowsPercentage = new List<int> { 2967, 7033 },
ColumnsPercentage = new List<int> { 2410, 6040, 1550 },
CellChildMap = new int[][] { [0, 1, 1], [0, 2, 3] },
SensitivityRadius = 30,
Spacing = 10,
ShowSpacing = true,
}),
},
},
};
[TestInitialize]
public void TestInitialize()
{
CustomLayouts customLayouts = new CustomLayouts();
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(Layouts));
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void Name_Initialize()
{
// verify all custom layouts are presented
foreach (var layout in Layouts.CustomLayouts)
{
Assert.IsNotNull(Session.Find<Element>(layout.Name));
}
}
[TestMethod]
public void Rename_Save()
{
string newName = "New layout name";
var oldName = Layouts.CustomLayouts[0].Name;
// rename the layout
Session.Find<Element>(oldName).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var input = Session.Find<TextBox>(PowerToys.UITest.By.ClassName(ClassName.TextBox));
Assert.IsNotNull(input);
input.SetText(newName, true);
// verify new name
Session.Find<Button>(ElementName.Save).Click();
if (Session.FindAll<Element>(oldName).Count != 0)
{
Assert.Fail("Error : previous name isn't presented"); // previous name isn't presented
}
if (Session.FindAll<Element>(newName).Count == 0)
{
Assert.Fail("Error : new name is presented"); // new name is presented
}
}
[TestMethod]
public void Rename_Cancel()
{
string newName = "New layout name";
var oldName = Layouts.CustomLayouts[0].Name;
// rename the layout
Session.Find<Element>(oldName).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var input = Session.Find<TextBox>(PowerToys.UITest.By.ClassName(ClassName.TextBox));
Assert.IsNotNull(input);
input.SetText(newName, true);
// verify new name
Session.Find<Button>(ElementName.Cancel).Click();
Assert.IsTrue(Session.FindAll<Element>(oldName).Count == 0);
Assert.IsTrue(Session.FindAll<Element>(newName).Count != 0);
}
[TestMethod]
public void HighlightDistance_Initialize()
{
foreach (var layout in Layouts.CustomLayouts)
{
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SensitivitySlider));
Assert.IsNotNull(slider);
var expected = layout.Type == CustomLayout.Canvas.TypeToString() ?
new CustomLayouts().CanvasFromJsonElement(layout.Info.GetRawText()).SensitivityRadius :
new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).SensitivityRadius;
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Cancel).Click();
}
}
[TestMethod]
public void HighlightDistance_Save()
{
var layout = Layouts.CustomLayouts[0];
var type = layout.Type;
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SensitivitySlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
var value = type == CustomLayout.Canvas.TypeToString() ?
new CustomLayouts().CanvasFromJsonElement(layout.Info.GetRawText()).SensitivityRadius :
new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).SensitivityRadius;
var expected = value + 1; // one step right
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Save).Click();
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var actual = type == CustomLayout.Canvas.TypeToString() ?
new CustomLayouts().CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).SensitivityRadius :
new CustomLayouts().GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).SensitivityRadius;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void HighlightDistance_Cancel()
{
var layout = Layouts.CustomLayouts[0];
var type = layout.Type;
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SensitivitySlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
var expected = type == CustomLayout.Canvas.TypeToString() ?
new CustomLayouts().CanvasFromJsonElement(layout.Info.GetRawText()).SensitivityRadius :
new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).SensitivityRadius;
Session.Find<Button>(ElementName.Cancel).Click();
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var actual = type == CustomLayout.Canvas.TypeToString() ?
new CustomLayouts().CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).SensitivityRadius :
new CustomLayouts().GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).SensitivityRadius;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Initialize()
{
foreach (var layout in Layouts.CustomLayouts)
{
if (layout.Type != CustomLayout.Grid.TypeToString())
{
// only for grid layouts
continue;
}
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var toggle = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingToggle));
Assert.IsNotNull(toggle);
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider));
Assert.IsNotNull(slider);
var spacingEnabled = new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).ShowSpacing;
Assert.AreEqual(spacingEnabled, slider.Enabled);
Assert.AreEqual(spacingEnabled, toggle.Selected);
var expected = new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).Spacing;
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Cancel).Click();
}
}
[TestMethod]
public void SpaceAroundZones_Slider_Save()
{
var layout = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString() && new CustomLayouts().GridFromJsonElement(x.Info.GetRawText()).ShowSpacing);
var expected = new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).Spacing + 1; // one step right
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Save).Click();
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var actual = new CustomLayouts().GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).Spacing;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Slider_Cancel()
{
var layout = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString() && new CustomLayouts().GridFromJsonElement(x.Info.GetRawText()).ShowSpacing);
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var expected = new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).Spacing;
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var actual = new CustomLayouts().GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).Spacing;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Toggle_Save()
{
var layout = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString());
var value = new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).ShowSpacing;
var expected = !value;
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var toggle = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingToggle));
Assert.IsNotNull(toggle);
toggle.Click();
Assert.AreEqual(expected, toggle.Selected, "Toggle value not changed");
Assert.AreEqual(expected, Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider))?.Enabled);
Session.Find<Button>(ElementName.Save).Click();
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var actual = new CustomLayouts().GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).ShowSpacing;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Toggle_Cancel()
{
var layout = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString());
var expected = new CustomLayouts().GridFromJsonElement(layout.Info.GetRawText()).ShowSpacing;
Session.Find<Element>(layout.Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var toggle = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingToggle));
Assert.IsNotNull(toggle);
toggle.Click();
Assert.AreNotEqual(expected, toggle.Selected, "Toggle value not changed");
Assert.AreNotEqual(expected, Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider))?.Enabled);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var actual = new CustomLayouts().GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == layout.Uuid).Info.GetRawText()).ShowSpacing;
Assert.AreEqual(expected, actual);
}
}
}

View File

@@ -0,0 +1,363 @@
// 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.Xml.Linq;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static FancyZonesEditorCommon.Data.CustomLayouts;
using static FancyZonesEditorCommon.Data.DefaultLayouts;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class DefaultLayoutsTest : UITestBase
{
public DefaultLayoutsTest()
: base(PowerToysModule.FancyZone)
{
}
private static readonly string Vertical = MonitorConfigurationType.Vertical.TypeToString();
private static readonly string Horizontal = MonitorConfigurationType.Horizontal.TypeToString();
private static readonly CustomLayoutListWrapper CustomLayouts = new CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayoutWrapper>
{
new CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 0",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
new CustomLayoutWrapper
{
Uuid = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 1",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
new CustomLayoutWrapper
{
Uuid = "{F1A94F38-82B6-4876-A653-70D0E882DE2A}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 2",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
new CustomLayoutWrapper
{
Uuid = "{F5FDBC04-0760-4776-9F05-96AAC4AE613F}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 3",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
},
};
private static readonly DefaultLayoutsListWrapper Layouts = new DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayoutWrapper>
{
new DefaultLayoutWrapper
{
MonitorConfiguration = Horizontal,
Layout = new DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = true,
Spacing = 5,
SensitivityRadius = 20,
},
},
new DefaultLayoutWrapper
{
MonitorConfiguration = Vertical,
Layout = new DefaultLayoutWrapper.LayoutWrapper
{
Type = "custom",
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
ZoneCount = 0,
ShowSpacing = false,
Spacing = 0,
SensitivityRadius = 0,
},
},
},
};
[TestInitialize]
public void TestInitialize()
{
var defaultLayouts = new DefaultLayouts();
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(Layouts));
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
new NativeMonitorDataWrapper
{
Monitor = "monitor-2",
MonitorInstanceId = "instance-id-2",
MonitorSerialNumber = "serial-number-2",
MonitorNumber = 2,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 1920,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = false,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
CustomLayouts customLayouts = new CustomLayouts();
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(CustomLayouts));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void Initialize()
{
CheckTemplateLayouts(LayoutType.Grid, null);
CheckCustomLayouts(string.Empty, CustomLayouts.CustomLayouts[0].Uuid);
}
[TestMethod]
public void Assign_Cancel()
{
// assign Focus as a default horizontal and vertical layout
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Focus]).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonUnchecked)).Click();
Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonUnchecked)).Click();
// cancel
Session.Find<Button>(ElementName.Cancel).Click();
// check that default layouts weren't changed
CheckTemplateLayouts(LayoutType.Grid, null);
CheckCustomLayouts(string.Empty, CustomLayouts.CustomLayouts[0].Uuid);
}
[TestMethod]
public void Assign_Save()
{
// assign Focus as a default horizontal and vertical layout
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Focus]).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonUnchecked)).Click();
Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonUnchecked)).Click();
// save
Session.Find<Button>(ElementName.Save).Click();
// check that default layout was changed
CheckTemplateLayouts(LayoutType.Focus, LayoutType.Focus);
CheckCustomLayouts(string.Empty, string.Empty);
}
private void CheckTemplateLayouts(LayoutType? horizontalDefault, LayoutType? verticalDefault)
{
foreach (var (key, name) in TestConstants.TemplateLayoutNames)
{
if (key == LayoutType.Blank)
{
continue;
}
Session.Find<Element>(name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
bool isCheckedHorizontal = key == horizontalDefault;
bool isCheckedVertical = key == verticalDefault;
Button? horizontalDefaultButton;
Button? verticalDefaultButton;
if (isCheckedHorizontal)
{
horizontalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonChecked));
}
else
{
horizontalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonUnchecked));
}
if (isCheckedVertical)
{
verticalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonChecked));
}
else
{
verticalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonUnchecked));
}
Assert.IsNotNull(horizontalDefaultButton, "Incorrect horizontal default layout set at " + name);
Assert.IsNotNull(verticalDefaultButton, "Incorrect vertical default layout set at " + name);
Session.Find<Button>(ElementName.Cancel).Click();
}
}
private void CheckCustomLayouts(string horizontalDefaultLayoutUuid, string verticalDefaultLayoutUuid)
{
foreach (var layout in CustomLayouts.CustomLayouts)
{
Session.Find<Element>(layout.Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
bool isCheckedHorizontal = layout.Uuid == horizontalDefaultLayoutUuid;
bool isCheckedVertical = layout.Uuid == verticalDefaultLayoutUuid;
Button? horizontalDefaultButton;
Button? verticalDefaultButton;
if (isCheckedHorizontal)
{
horizontalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonChecked));
}
else
{
horizontalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonUnchecked));
}
if (isCheckedVertical)
{
verticalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonChecked));
}
else
{
verticalDefaultButton = Session.Find<Button>(By.AccessibilityId(AccessibilityId.VerticalDefaultButtonUnchecked));
}
Assert.IsNotNull(horizontalDefaultButton, "Incorrect horizontal custom layout set at " + layout.Name);
Assert.IsNotNull(verticalDefaultButton, "Incorrect vertical custom layout set at " + layout.Name);
Session.Find<Button>(ElementName.Cancel).Click();
}
}
}
}

View File

@@ -0,0 +1,365 @@
// 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 FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ModernWpf.Controls;
using OpenQA.Selenium;
using static FancyZonesEditorCommon.Data.CustomLayouts;
using static FancyZonesEditorCommon.Data.DefaultLayouts;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static FancyZonesEditorCommon.Data.LayoutHotkeys;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class DeleteLayoutTests : UITestBase
{
public DeleteLayoutTests()
: base(PowerToysModule.FancyZone)
{
}
private static readonly CustomLayoutListWrapper CustomLayouts = new CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayoutWrapper>
{
new CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Grid.TypeToString(),
Name = "Custom layout 1",
Info = new CustomLayouts().ToJsonElement(new CustomLayouts.GridInfoWrapper
{
Rows = 2,
Columns = 3,
RowsPercentage = new List<int> { 2967, 7033 },
ColumnsPercentage = new List<int> { 2410, 6040, 1550 },
CellChildMap = new int[][] { [0, 1, 1], [0, 2, 3] },
SensitivityRadius = 30,
Spacing = 26,
ShowSpacing = false,
}),
},
new CustomLayoutWrapper
{
Uuid = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Custom layout 2",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 952,
RefWidth = 1500,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper>
{
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 0,
Y = 0,
Width = 900,
Height = 522,
},
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 900,
Y = 0,
Width = 600,
Height = 750,
},
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 0,
Y = 522,
Width = 1500,
Height = 430,
},
},
}),
},
},
};
private static readonly DefaultLayouts.DefaultLayoutsListWrapper DefaultLayoutsList = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper>
{
new DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Horizontal.TypeToString(),
Layout = new DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.Custom.TypeToString(),
Uuid = CustomLayouts.CustomLayouts[1].Uuid,
},
},
},
};
private static readonly LayoutHotkeys.LayoutHotkeysWrapper Hotkeys = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper>
{
new LayoutHotkeyWrapper
{
LayoutId = CustomLayouts.CustomLayouts[1].Uuid,
Key = 0,
},
},
};
private static readonly ParamsWrapper Parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(Parameters));
CustomLayouts customLayouts = new CustomLayouts();
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(CustomLayouts));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
DefaultLayouts defaultLayouts = new DefaultLayouts();
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(DefaultLayoutsList));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(Hotkeys));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
Session.Find<Element>(CustomLayouts.CustomLayouts[0].Name).Click();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void DeleteNotAppliedLayout()
{
var deletedLayout = CustomLayouts.CustomLayouts[1].Name;
Session.Find<Element>(deletedLayout).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Session.Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.DeleteLayoutButton)).Click();
Session.KeyboardAction(Keys.Tab, Keys.Enter);
// verify the layout is removed
Assert.IsTrue(Session.FindAll<Element>(deletedLayout).Count == 0);
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count - 1, data.CustomLayouts.Count);
Assert.IsFalse(data.CustomLayouts.Exists(x => x.Name == deletedLayout));
}
[TestMethod]
public void DeleteAppliedLayout()
{
var deletedLayout = CustomLayouts.CustomLayouts[0].Name;
Session.Find<Element>(deletedLayout).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Session.Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.DeleteLayoutButton)).Click();
Session.KeyboardAction(Keys.Tab, Keys.Enter);
// verify the layout is removed
Assert.IsTrue(Session.FindAll<Element>(deletedLayout).Count == 0);
// verify the empty layout is selected
Assert.IsTrue(Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Blank])!.Selected);
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count - 1, data.CustomLayouts.Count);
Assert.IsFalse(data.CustomLayouts.Exists(x => x.Name == deletedLayout));
var appliedLayouts = new AppliedLayouts();
var appliedLayoutsData = appliedLayouts.Read(appliedLayouts.File);
Assert.AreEqual(LayoutType.Blank.TypeToString(), appliedLayoutsData.AppliedLayouts.Find(x => x.Device.Monitor == Parameters.Monitors[0].Monitor).AppliedLayout.Type);
}
[TestMethod]
public void CancelDeletion()
{
var deletedLayout = CustomLayouts.CustomLayouts[1].Name;
Session.Find<Element>(deletedLayout).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Session.Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.DeleteLayoutButton)).Click();
Session.KeyboardAction(Keys.Tab, Keys.Tab, Keys.Enter);
// verify the layout is not removed
Assert.IsNotNull(Session.Find<Element>(deletedLayout));
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count, data.CustomLayouts.Count);
Assert.IsTrue(data.CustomLayouts.Exists(x => x.Name == deletedLayout));
}
[TestMethod]
public void DeleteFromContextMenu()
{
var deletedLayout = CustomLayouts.CustomLayouts[1].Name;
FancyZonesEditorHelper.ClickContextMenuItem(Session, deletedLayout, FancyZonesEditorHelper.ElementName.Delete);
Session.KeyboardAction(Keys.Tab, Keys.Enter);
// verify the layout is removed
Assert.IsTrue(Session.FindAll<Element>(deletedLayout).Count == 0);
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
Assert.AreEqual(CustomLayouts.CustomLayouts.Count - 1, data.CustomLayouts.Count);
Assert.IsFalse(data.CustomLayouts.Exists(x => x.Name == deletedLayout));
}
[TestMethod]
public void DeleteDefaultLayout()
{
var deletedLayout = CustomLayouts.CustomLayouts[1].Name;
FancyZonesEditorHelper.ClickContextMenuItem(Session, deletedLayout, FancyZonesEditorHelper.ElementName.Delete);
Session.KeyboardAction(Keys.Tab, Keys.Enter);
// verify the default layout is reset to the "default" default
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.PriorityGrid]).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Assert.IsNotNull(Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonChecked)));
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var defaultLayouts = new DefaultLayouts();
var data = defaultLayouts.Read(defaultLayouts.File);
string configuration = MonitorConfigurationType.Horizontal.TypeToString();
Assert.AreEqual(LayoutType.PriorityGrid.TypeToString(), data.DefaultLayouts.Find(x => x.MonitorConfiguration == configuration).Layout.Type);
}
[TestMethod]
public void DeleteLayoutWithHotkey()
{
var deletedLayout = CustomLayouts.CustomLayouts[1].Name;
FancyZonesEditorHelper.ClickContextMenuItem(Session, deletedLayout, FancyZonesEditorHelper.ElementName.Delete);
Session.KeyboardAction(Keys.Tab, Keys.Enter);
// verify the hotkey is available
Session.Find<Element>(CustomLayouts.CustomLayouts[0].Name).Find<Button>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var hotkeyComboBox = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
var popup = Session.Find<Element>(PowerToys.UITest.By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
try
{
for (int i = 0; i < 10; i++)
{
popup.Find<Element>($"{i}");
}
}
catch
{
Assert.Fail("Hotkey not found");
}
Session.Find<Button>(ElementName.Cancel).DoubleClick();
// check the file
var hotkeys = new LayoutHotkeys();
var data = hotkeys.Read(hotkeys.File);
int layoutHotkeyCount = 0;
foreach (var layout in data.LayoutHotkeys)
{
if (layout.Key != -1)
{
layoutHotkeyCount++;
}
}
Assert.AreEqual(0, layoutHotkeyCount);
}
}
}

View File

@@ -0,0 +1,653 @@
// 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 FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ModernWpf.Controls;
using OpenQA.Selenium.Appium.Windows;
using static FancyZonesEditorCommon.Data.CustomLayouts;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class EditLayoutTests : UITestBase
{
public EditLayoutTests()
: base(PowerToysModule.FancyZone)
{
}
private static readonly CustomLayouts.CustomLayoutListWrapper Layouts = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
{
new CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Grid.TypeToString(),
Name = "Grid custom layout",
Info = new CustomLayouts().ToJsonElement(new GridInfoWrapper
{
Rows = 2,
Columns = 2,
RowsPercentage = new List<int> { 5000, 5000 },
ColumnsPercentage = new List<int> { 5000, 5000 },
CellChildMap = new int[][] { [0, 1], [2, 3] },
SensitivityRadius = 30,
Spacing = 26,
ShowSpacing = false,
}),
},
new CustomLayoutWrapper
{
Uuid = "{0EB9BF3E-010E-46D7-8681-1879D1E111E1}",
Type = CustomLayout.Grid.TypeToString(),
Name = "Grid-9",
Info = new CustomLayouts().ToJsonElement(new GridInfoWrapper
{
Rows = 3,
Columns = 3,
RowsPercentage = new List<int> { 2333, 3333, 4334 },
ColumnsPercentage = new List<int> { 2333, 3333, 4334 },
CellChildMap = new int[][] { [0, 1, 2], [3, 4, 5], [6, 7, 8] },
SensitivityRadius = 20,
Spacing = 3,
ShowSpacing = false,
}),
},
new CustomLayoutWrapper
{
Uuid = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Canvas custom layout",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1040,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper>
{
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 0,
Y = 0,
Width = 500,
Height = 250,
},
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 500,
Y = 0,
Width = 1420,
Height = 500,
},
new CanvasInfoWrapper.CanvasZoneWrapper
{
X = 0,
Y = 250,
Width = 1920,
Height = 500,
},
},
}),
},
},
};
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
CustomLayouts customLayouts = new CustomLayouts();
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(Layouts));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void OpenEditMode()
{
Session.Find<Element>(Layouts.CustomLayouts[0].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Session.Find<Button>(By.AccessibilityId(AccessibilityId.EditZonesButton)).Click();
Assert.IsNotNull(Session.Find<Element>(ElementName.GridLayoutEditor));
Session.Find<Button>(ElementName.Cancel).Click();
}
[TestMethod]
public void OpenEditModeFromContextMenu()
{
FancyZonesEditorHelper.ClickContextMenuItem(Session, Layouts.CustomLayouts[0].Name, FancyZonesEditorHelper.ElementName.EditZones);
Assert.IsNotNull(Session.Find<Element>(ElementName.GridLayoutEditor));
Session.Find<Button>(ElementName.Cancel).Click();
}
[TestMethod]
public void Canvas_AddZone_Save()
{
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
Session.Find<Button>(By.AccessibilityId(AccessibilityId.NewZoneButton)).Click();
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
Assert.AreEqual(expected.Zones.Count + 1, actual.Zones.Count);
}
[TestMethod]
public void Canvas_AddZone_Cancel()
{
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
Session.Find<Button>(By.AccessibilityId(AccessibilityId.NewZoneButton)).Click();
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
Assert.AreEqual(expected.Zones.Count, actual.Zones.Count);
}
[TestMethod]
public void Canvas_DeleteZone_Save()
{
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.ClickDeleteZone(Session, 1);
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
Assert.AreEqual(expected.Zones.Count - 1, actual.Zones.Count);
}
[TestMethod]
public void Canvas_DeleteZone_Cancel()
{
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.ClickDeleteZone(Session, 1);
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
Assert.AreEqual(expected.Zones.Count, actual.Zones.Count);
}
[TestMethod]
public void Canvas_MoveZone_Save()
{
int zoneNumber = 1;
int xOffset = 100;
int yOffset = 100;
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.GetZone(Session, zoneNumber, FancyZonesEditorHelper.ClassName.CanvasZone)?.Drag(xOffset, yOffset);
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
// changed zone, exact offset may vary depending on screen resolution
Assert.IsTrue(expected.Zones[zoneNumber - 1].X < actual.Zones[zoneNumber - 1].X, $"X: {expected.Zones[zoneNumber - 1].X} > {actual.Zones[zoneNumber - 1].X}");
Assert.IsTrue(expected.Zones[zoneNumber - 1].Y < actual.Zones[zoneNumber - 1].Y, $"Y: {expected.Zones[zoneNumber - 1].Y} > {actual.Zones[zoneNumber - 1].Y}");
Assert.AreEqual(expected.Zones[zoneNumber - 1].Width, actual.Zones[zoneNumber - 1].Width);
Assert.AreEqual(expected.Zones[zoneNumber - 1].Height, actual.Zones[zoneNumber - 1].Height);
// other zones
for (int i = 0; i < expected.Zones.Count; i++)
{
if (i != zoneNumber - 1)
{
Assert.AreEqual(expected.Zones[i].X, actual.Zones[i].X);
Assert.AreEqual(expected.Zones[i].Y, actual.Zones[i].Y);
Assert.AreEqual(expected.Zones[i].Width, actual.Zones[i].Width);
Assert.AreEqual(expected.Zones[i].Height, actual.Zones[i].Height);
}
}
}
[TestMethod]
public void Canvas_MoveZone_Cancel()
{
int zoneNumber = 1;
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.GetZone(Session, zoneNumber, FancyZonesEditorHelper.ClassName.CanvasZone)?.Drag(100, 100);
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
for (int i = 0; i < expected.Zones.Count; i++)
{
Assert.AreEqual(expected.Zones[i].X, actual.Zones[i].X);
Assert.AreEqual(expected.Zones[i].Y, actual.Zones[i].Y);
Assert.AreEqual(expected.Zones[i].Width, actual.Zones[i].Width);
Assert.AreEqual(expected.Zones[i].Height, actual.Zones[i].Height);
}
}
[TestMethod]
public void Canvas_ResizeZone_Save()
{
int zoneNumber = 1;
int xOffset = 100;
int yOffset = 100;
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.GetZone(Session, zoneNumber, FancyZonesEditorHelper.ClassName.CanvasZone)?.Find<Element>(By.AccessibilityId(FancyZonesEditorHelper.AccessibilityId.TopRightCorner)).Drag(xOffset, yOffset);
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
// changed zone, exact offset may vary depending on screen resolution
Assert.AreEqual(expected.Zones[zoneNumber - 1].X, actual.Zones[zoneNumber - 1].X);
Assert.IsTrue(expected.Zones[zoneNumber - 1].Y < actual.Zones[zoneNumber - 1].Y, $"Y: {expected.Zones[zoneNumber - 1].Y} > {actual.Zones[zoneNumber - 1].Y}");
Assert.IsTrue(expected.Zones[zoneNumber - 1].Width < actual.Zones[zoneNumber - 1].Width, $"Width: {expected.Zones[zoneNumber - 1].Width} < {actual.Zones[zoneNumber - 1].Width}");
Assert.IsTrue(expected.Zones[zoneNumber - 1].Height > actual.Zones[zoneNumber - 1].Height, $"Height: {expected.Zones[zoneNumber - 1].Height} < {actual.Zones[zoneNumber - 1].Height}");
// other zones
for (int i = 0; i < expected.Zones.Count; i++)
{
if (i != zoneNumber - 1)
{
Assert.AreEqual(expected.Zones[i].X, actual.Zones[i].X);
Assert.AreEqual(expected.Zones[i].Y, actual.Zones[i].Y);
Assert.AreEqual(expected.Zones[i].Width, actual.Zones[i].Width);
Assert.AreEqual(expected.Zones[i].Height, actual.Zones[i].Height);
}
}
}
[TestMethod]
public void Canvas_ResizeZone_Cancel()
{
int zoneNumber = 1;
int xOffset = 100;
int yOffset = 100;
var canvas = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Canvas.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, canvas.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.GetZone(Session, zoneNumber, FancyZonesEditorHelper.ClassName.CanvasZone)?.Find<Element>(By.AccessibilityId(FancyZonesEditorHelper.AccessibilityId.TopRightCorner)).Drag(xOffset, yOffset);
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.CanvasFromJsonElement(canvas.Info.ToString());
var actual = customLayouts.CanvasFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == canvas.Uuid).Info.GetRawText());
for (int i = 0; i < expected.Zones.Count; i++)
{
Assert.AreEqual(expected.Zones[i].X, actual.Zones[i].X);
Assert.AreEqual(expected.Zones[i].Y, actual.Zones[i].Y);
Assert.AreEqual(expected.Zones[i].Width, actual.Zones[i].Width);
Assert.AreEqual(expected.Zones[i].Height, actual.Zones[i].Height);
}
}
[TestMethod]
public void Grid_SplitZone_Save()
{
int zoneNumber = 1;
var grid = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, grid.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.GetZone(Session, zoneNumber, FancyZonesEditorHelper.ClassName.GridZone)?.Click(); // horizontal split in the middle of the zone
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.GridFromJsonElement(grid.Info.ToString());
var actual = customLayouts.GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == grid.Uuid).Info.GetRawText());
// new column added
Assert.AreEqual(expected.Columns + 1, actual.Columns);
Assert.AreEqual(expected.ColumnsPercentage[0], actual.ColumnsPercentage[0] + actual.ColumnsPercentage[1]);
Assert.AreEqual(expected.ColumnsPercentage[1], actual.ColumnsPercentage[2]);
// rows are not changed
Assert.AreEqual(expected.Rows, actual.Rows);
for (int i = 0; i < expected.Rows; i++)
{
Assert.AreEqual(expected.RowsPercentage[i], actual.RowsPercentage[i]);
}
}
[TestMethod]
public void Grid_SplitZone_Cancel()
{
int zoneNumber = 1;
var grid = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, grid.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.GetZone(Session, zoneNumber, FancyZonesEditorHelper.ClassName.GridZone)?.Click(); // horizontal split in the middle of the zone
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.GridFromJsonElement(grid.Info.ToString());
var actual = customLayouts.GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == grid.Uuid).Info.GetRawText());
// columns are not changed
Assert.AreEqual(expected.Columns, actual.Columns);
for (int i = 0; i < expected.Columns; i++)
{
Assert.AreEqual(expected.ColumnsPercentage[i], actual.ColumnsPercentage[i]);
}
// rows are not changed
Assert.AreEqual(expected.Rows, actual.Rows);
for (int i = 0; i < expected.Rows; i++)
{
Assert.AreEqual(expected.RowsPercentage[i], actual.RowsPercentage[i]);
}
}
[TestMethod]
public void Grid_MergeZones_Save()
{
var grid = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, grid.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.MergeGridZones(Session, 1, 2);
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.GridFromJsonElement(grid.Info.ToString());
var actual = customLayouts.GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == grid.Uuid).Info.GetRawText());
// columns are not changed
Assert.AreEqual(expected.Columns, actual.Columns);
for (int i = 0; i < expected.Columns; i++)
{
Assert.AreEqual(expected.ColumnsPercentage[i], actual.ColumnsPercentage[i]);
}
// rows are not changed
Assert.AreEqual(expected.Rows, actual.Rows);
for (int i = 0; i < expected.Rows; i++)
{
Assert.AreEqual(expected.RowsPercentage[i], actual.RowsPercentage[i]);
}
// cells are updated to [0,0][1,2]
Assert.IsTrue(actual.CellChildMap[0].SequenceEqual([0, 0]));
Assert.IsTrue(actual.CellChildMap[1].SequenceEqual([1, 2]));
}
[TestMethod]
public void Grid_MergeZones_Cancel()
{
var grid = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString());
FancyZonesEditorHelper.ClickContextMenuItem(Session, grid.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.MergeGridZones(Session, 1, 2);
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.GridFromJsonElement(grid.Info.ToString());
var actual = customLayouts.GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == grid.Uuid).Info.GetRawText());
// columns are not changed
Assert.AreEqual(expected.Columns, actual.Columns);
for (int i = 0; i < expected.Columns; i++)
{
Assert.AreEqual(expected.ColumnsPercentage[i], actual.ColumnsPercentage[i]);
}
// rows are not changed
Assert.AreEqual(expected.Rows, actual.Rows);
for (int i = 0; i < expected.Rows; i++)
{
Assert.AreEqual(expected.RowsPercentage[i], actual.RowsPercentage[i]);
}
// cells are not changed
for (int i = 0; i < expected.CellChildMap.Length; i++)
{
Assert.IsTrue(actual.CellChildMap[i].SequenceEqual(expected.CellChildMap[i]));
}
}
[TestMethod]
public void Grid_MoveSplitter_Save()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 780,
WorkAreaWidth = 1240,
MonitorHeight = 780,
MonitorWidth = 1240,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
this.RestartScopeExe();
var grid = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString() && x.Name == "Grid-9");
FancyZonesEditorHelper.ClickContextMenuItem(Session, grid.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.MoveSplitter(Session, 2, -50, 0);
Session.Find<Button>(ElementName.Save).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.GridFromJsonElement(grid.Info.ToString());
var actual = customLayouts.GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == grid.Uuid).Info.GetRawText());
// rows are not changed
Assert.AreEqual(expected.Rows, actual.Rows);
for (int i = 0; i < expected.Rows; i++)
{
Assert.AreEqual(expected.RowsPercentage[i], actual.RowsPercentage[i]);
}
// Columns are changed
Assert.AreEqual(expected.Columns, actual.Columns);
Assert.IsTrue(expected.ColumnsPercentage[0] > actual.ColumnsPercentage[0], $"{expected.ColumnsPercentage[0]} > {actual.ColumnsPercentage[0]}");
Assert.IsTrue(expected.ColumnsPercentage[1] < actual.ColumnsPercentage[1], $"{expected.ColumnsPercentage[1]} < {actual.ColumnsPercentage[1]}");
Assert.AreEqual(expected.ColumnsPercentage[2], actual.ColumnsPercentage[2], $"{expected.ColumnsPercentage[2]} == {actual.ColumnsPercentage[2]}");
// cells are not changed
for (int i = 0; i < expected.CellChildMap.Length; i++)
{
Assert.IsTrue(actual.CellChildMap[i].SequenceEqual(expected.CellChildMap[i]));
}
}
[TestMethod]
public void Grid_MoveSplitter_Cancel()
{
var grid = Layouts.CustomLayouts.Find(x => x.Type == CustomLayout.Grid.TypeToString() && x.Name == "Grid-9");
FancyZonesEditorHelper.ClickContextMenuItem(Session, grid.Name, FancyZonesEditorHelper.ElementName.EditZones);
FancyZonesEditorHelper.MoveSplitter(Session, 2, -100, 0);
Session.Find<Button>(ElementName.Cancel).Click();
// check the file
var customLayouts = new CustomLayouts();
var data = customLayouts.Read(customLayouts.File);
var expected = customLayouts.GridFromJsonElement(grid.Info.ToString());
var actual = customLayouts.GridFromJsonElement(data.CustomLayouts.Find(x => x.Uuid == grid.Uuid).Info.GetRawText());
// columns are not changed
Assert.AreEqual(expected.Columns, actual.Columns);
for (int i = 0; i < expected.Columns; i++)
{
Assert.AreEqual(expected.ColumnsPercentage[i], actual.ColumnsPercentage[i]);
}
// rows are not changed
Assert.AreEqual(expected.Rows, actual.Rows);
for (int i = 0; i < expected.Rows; i++)
{
Assert.AreEqual(expected.RowsPercentage[i], actual.RowsPercentage[i]);
}
// cells are not changed
for (int i = 0; i < expected.CellChildMap.Length; i++)
{
Assert.IsTrue(actual.CellChildMap[i].SequenceEqual(expected.CellChildMap[i]));
}
}
}
}

View File

@@ -0,0 +1,157 @@
// 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.Globalization;
using System.Windows.Forms;
using System.Xml.Linq;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UITests;
using Microsoft.FancyZonesEditor.UITests.Utils;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ModernWpf.Controls;
using Windows.UI;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class FirstLunchTest : UITestBase
{
public FirstLunchTest()
: base(PowerToysModule.FancyZone)
{
}
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192, // 200% scaling
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
// verify editor opens without errors
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void FirstLaunch()
{
Session.Find<Element>(By.AccessibilityId(FancyZonesEditorHelper.AccessibilityId.MainWindow)).Click();
Assert.IsNotNull(Session.Find<Element>(By.AccessibilityId(FancyZonesEditorHelper.AccessibilityId.MainWindow)));
}
}
}

View File

@@ -0,0 +1,468 @@
// 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.Globalization;
using System.Linq;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ModernWpf.Controls;
using static FancyZonesEditorCommon.Data.CustomLayouts;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static FancyZonesEditorCommon.Data.LayoutHotkeys;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class LayoutHotkeysTests : UITestBase
{
public LayoutHotkeysTests()
: base(PowerToysModule.FancyZone)
{
}
private static readonly CustomLayoutListWrapper CustomLayouts = new CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayoutWrapper>
{
new CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 0",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
new CustomLayoutWrapper
{
Uuid = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 1",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
new CustomLayoutWrapper
{
Uuid = "{F1A94F38-82B6-4876-A653-70D0E882DE2A}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 2",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
new CustomLayoutWrapper
{
Uuid = "{F5FDBC04-0760-4776-9F05-96AAC4AE613F}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Layout 3",
Info = new CustomLayouts().ToJsonElement(new CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
},
};
private static readonly LayoutHotkeysWrapper Hotkeys = new LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeyWrapper>
{
new LayoutHotkeyWrapper
{
LayoutId = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Key = 0,
},
new LayoutHotkeyWrapper
{
LayoutId = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Key = 1,
},
},
};
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
CustomLayouts customLayouts = new CustomLayouts();
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(CustomLayouts));
var layoutHotkeys = new LayoutHotkeys();
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(Hotkeys));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void Initialize()
{
foreach (var layout in CustomLayouts.CustomLayouts)
{
Session.Find<Element>(layout.Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
// verify the selected key
string expected = "None";
if (Hotkeys.LayoutHotkeys.Any(x => x.LayoutId == layout.Uuid))
{
expected = $"{Hotkeys.LayoutHotkeys.Find(x => x.LayoutId == layout.Uuid).Key}";
}
Assert.AreEqual($"{expected}", hotkeyComboBox.Text);
// verify the available values
hotkeyComboBox.Click();
var popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup, "Hotkey combo box wasn't opened");
try
{
popup.Find<Element>(expected); // the current value should be available
// 0 and 1 are assigned, all others should be available
for (int i = 2; i < 10; i++)
{
popup.Find<Element>($"{i}");
}
}
catch
{
Assert.Fail("Hotkey is missed");
}
Session.Find<Button>(ElementName.Cancel).DoubleClick();
}
}
[TestMethod]
public void Assign_Save()
{
var layout = CustomLayouts.CustomLayouts[2]; // a layout without assigned hotkey
Session.Find<Element>(layout.Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
// assign hotkey
const string key = "3";
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
var popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
popup.Find<Element>($"{key}").Click(); // assign a free hotkey
Assert.AreEqual(key, hotkeyComboBox.Text);
// verify the file
Session.Find<Button>(ElementName.Save).Click();
var hotkeys = new LayoutHotkeys();
var actualData = hotkeys.Read(hotkeys.File);
Assert.IsTrue(actualData.LayoutHotkeys.Contains(new LayoutHotkeyWrapper { Key = int.Parse(key, CultureInfo.InvariantCulture), LayoutId = layout.Uuid }));
// verify the availability
Session.Find<Element>(CustomLayouts.CustomLayouts[3].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
try
{
popup.Find<Element>($"{key}"); // verify the key is not available
Assert.Fail($"{key} The assigned key is still available for other layouts.");
}
catch
{
// key not found as expected
}
}
[TestMethod]
public void Assign_Cancel()
{
var layout = CustomLayouts.CustomLayouts[2]; // a layout without assigned hotkey
Session.Find<Element>(layout.Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
// assign a hotkey
const string key = "3";
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
var popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
popup.Find<Element>($"{key}").Click();
Assert.AreEqual(key, hotkeyComboBox.Text);
// verify the file
Session.Find<Button>(ElementName.Cancel).Click();
var hotkeys = new LayoutHotkeys();
var actualData = hotkeys.Read(hotkeys.File);
Assert.AreEqual(Hotkeys.ToString(), actualData.ToString());
// verify the availability
Session.Find<Element>(CustomLayouts.CustomLayouts[3].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
try
{
popup.Find<Element>($"{key}"); // verify the key is available
}
catch
{
Assert.Fail("The key is not available for other layouts.");
}
}
[TestMethod]
public void Assign_AllPossibleValues()
{
for (int i = 0; i < 4; i++)
{
string layoutName = $"Layout {i}";
Session.Find<Element>(layoutName).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
var popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
popup.Find<Element>($"{i}").Click();
Session.Find<Button>(ElementName.Save).Click();
}
// check there nothing except None
{
int layout = 3;
string layoutName = $"Layout {layout}";
Session.Find<Element>(layoutName).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
var popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
for (int i = 0; i < 10; i++)
{
try
{
popup.Find<Element>($"{i}");
Assert.Fail("The assigned key is still available for other layouts.");
}
catch
{
}
}
popup.Find<Element>($"None").Click();
Session.Find<Button>(ElementName.Save).Click();
}
}
[TestMethod]
public void Reset_Save()
{
var layout = CustomLayouts.CustomLayouts[0]; // a layout with assigned hotkey
int assignedKey = Hotkeys.LayoutHotkeys.Find(x => x.LayoutId == layout.Uuid).Key;
Session.Find<Element>(layout.Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
const string None = "None";
// reset the hotkey
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
var popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
popup.Find<Element>(None).Click();
Assert.AreEqual(None, hotkeyComboBox.Text);
// verify the file
Session.Find<Button>(ElementName.Save).Click();
var hotkeys = new LayoutHotkeys();
var actualData = hotkeys.Read(hotkeys.File);
Assert.IsFalse(actualData.LayoutHotkeys.Contains(new LayoutHotkeyWrapper { Key = assignedKey, LayoutId = layout.Uuid }));
// verify the previously assigned key is available
Session.Find<Element>(CustomLayouts.CustomLayouts[3].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
try
{
popup.Find<Element>($"{assignedKey}"); // verify the key is available
}
catch
{
Assert.Fail("The key is not available for other layouts.");
}
}
[TestMethod]
public void Reset_Cancel()
{
var layout = CustomLayouts.CustomLayouts[0]; // a layout with assigned hotkey
int assignedKey = Hotkeys.LayoutHotkeys.Find(x => x.LayoutId == layout.Uuid).Key;
Session.Find<Element>(layout.Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
const string None = "None";
// assign hotkey
var hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
var popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
popup.Find<Element>(None).Click(); // reset the hotkey
Assert.AreEqual(None, hotkeyComboBox.Text);
// verify the file
Session.Find<Button>(ElementName.Cancel).Click();
var hotkeys = new LayoutHotkeys();
var actualData = hotkeys.Read(hotkeys.File);
Assert.IsTrue(actualData.LayoutHotkeys.Contains(new LayoutHotkeyWrapper { Key = assignedKey, LayoutId = layout.Uuid }));
// verify the previously assigned key is not available
Session.Find<Element>(CustomLayouts.CustomLayouts[3].Name).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
hotkeyComboBox = Session.Find<Element>(By.AccessibilityId(AccessibilityId.HotkeyComboBox));
Assert.IsNotNull(hotkeyComboBox);
hotkeyComboBox.Click();
popup = Session.Find<Element>(By.ClassName(ClassName.Popup));
Assert.IsNotNull(popup);
try
{
popup.Find<Element>($"{assignedKey}"); // verify the key is not available
Assert.Fail("The key is still available for other layouts.");
}
catch
{
// the key is not available as expected
}
}
}
}

View File

@@ -0,0 +1,153 @@
// 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 FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UITests;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.UI;
using static FancyZonesEditorCommon.Data.EditorParameters;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class NewFancyZonesEditorTest
{
public NewFancyZonesEditorTest()
{
// FancyZonesEditorHelper.InitFancyZonesLayout();
}
[TestClass]
public class TestCaseFirstLaunch : UITestBase
{
public TestCaseFirstLaunch()
: base(PowerToysModule.FancyZone)
{
}
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192, // 200% scaling
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
// files not yet exist
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
// verify editor opens without errors
this.RestartScopeExe();
}
[TestMethod]
public void FirstLaunch() // verify the session is initialized
{
Assert.IsNotNull(Session.Find<Element>("FancyZones Layout"));
}
}
}
}

View File

@@ -3,26 +3,31 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UITests;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium.Windows;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace UITests_FancyZonesEditor
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class RunFancyZonesEditorTest
public class RunFancyZonesEditorTest : UITestBase
{
private static FancyZonesEditorSession? _session;
private static TestContext? _context;
public RunFancyZonesEditorTest()
: base(PowerToysModule.FancyZone)
{
}
[ClassInitialize]
public static void ClassInitialize(TestContext testContext)
{
_context = testContext;
FancyZonesEditorHelper.Files.Restore();
// prepare files to launch Editor without errors
// prepare test editor parameters with 2 monitors before launching the editor
EditorParameters editorParameters = new EditorParameters();
EditorParameters.ParamsWrapper parameters = new EditorParameters.ParamsWrapper
{
@@ -46,9 +51,25 @@ namespace UITests_FancyZonesEditor
MonitorWidth = 1920,
IsSelected = true,
},
new EditorParameters.NativeMonitorDataWrapper
{
Monitor = "monitor-2",
MonitorInstanceId = "instance-id-2",
MonitorSerialNumber = "serial-number-2",
MonitorNumber = 2,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 1920,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = false,
},
},
};
FancyZonesEditorSession.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
@@ -57,16 +78,16 @@ namespace UITests_FancyZonesEditor
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Empty],
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Focus],
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Rows],
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
@@ -74,7 +95,7 @@ namespace UITests_FancyZonesEditor
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Columns],
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
@@ -82,7 +103,7 @@ namespace UITests_FancyZonesEditor
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Grid],
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
@@ -90,7 +111,7 @@ namespace UITests_FancyZonesEditor
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.PriorityGrid],
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
@@ -98,81 +119,118 @@ namespace UITests_FancyZonesEditor
},
},
};
FancyZonesEditorSession.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> { },
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
{
new CustomLayouts.CustomLayoutWrapper
{
Uuid = "{E7807D0D-6223-4883-B15B-1F3883944C09}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Custom layout",
Info = new CustomLayouts().ToJsonElement(new CustomLayouts.CanvasInfoWrapper
{
RefHeight = 952,
RefWidth = 1500,
SensitivityRadius = 10,
Zones = new List<CustomLayouts.CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
},
};
FancyZonesEditorSession.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorSession.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorSession.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorSession.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
}
[ClassCleanup]
public static void ClassCleanup()
{
FancyZonesEditorSession.Files.Restore();
_context = null;
}
[TestInitialize]
public void TestInitialize()
{
_session = new FancyZonesEditorSession(_context!);
}
[TestCleanup]
public void TestCleanup()
{
_session?.Close(_context!);
}
[TestMethod]
public void OpenEditorWindow() // verify the session is initialized
{
Assert.IsNotNull(_session?.Session);
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void OpenNewLayoutDialog() // verify the new layout dialog is opened
{
_session?.Click_CreateNewLayout();
Assert.IsNotNull(_session?.Session?.FindElementsByName("Choose layout type")); // check the pane header
Session.Find<Button>(By.AccessibilityId(AccessibilityId.NewLayoutButton)).Click();
Assert.IsNotNull(Session.Find<Element>("Choose layout type")); // check the pane header
}
[TestMethod]
public void OpenEditLayoutDialog() // verify the edit layout dialog is opened
{
_session?.Click_EditLayout(TestConstants.TemplateLayoutNames[Constants.TemplateLayout.Grid]);
Assert.IsNotNull(_session?.Session?.FindElementByAccessibilityId("EditLayoutDialogTitle")); // check the pane header
Assert.IsNotNull(_session?.Session?.FindElementsByName("Edit 'Grid'")); // verify it's opened for the correct layout
Session.Find<Button>(TestConstants.TemplateLayoutNames[LayoutType.Grid]).Click();
Assert.IsNotNull(Session.Find<Element>(By.AccessibilityId(FancyZonesEditorHelper.AccessibilityId.DialogTitle))); // check the pane header
Assert.IsNotNull(Session.Find<Element>($"Edit '{TestConstants.TemplateLayoutNames[LayoutType.Grid]}'")); // verify it's opened for the correct layout
}
[TestMethod]
public void OpenEditLayoutDialog_ByContextMenu_TemplateLayout() // verify the edit layout dialog is opened
{
Session.Find<Button>(TestConstants.TemplateLayoutNames[LayoutType.Grid]).Click(true);
var menu = Session.Find<Element>(By.ClassName(ClassName.ContextMenu));
menu.Find<Element>(FancyZonesEditorHelper.ElementName.Edit).Click();
Assert.IsNotNull(Session.Find<Element>(By.AccessibilityId(FancyZonesEditorHelper.AccessibilityId.DialogTitle))); // check the pane header
Assert.IsNotNull(Session.Find<Element>($"Edit '{TestConstants.TemplateLayoutNames[LayoutType.Grid]}'")); // verify it's opened for the correct layout
}
[TestMethod]
public void OpenEditLayoutDialog_ByContextMenu_CustomLayout() // verify the edit layout dialog is opened
{
string layoutName = "Custom layout";
Session.Find<Button>(layoutName).Click(true);
var menu = Session.Find<Element>(By.ClassName(ClassName.ContextMenu));
menu.Find<Element>(FancyZonesEditorHelper.ElementName.Edit).Click();
Assert.IsNotNull(Session.Find<Element>(By.AccessibilityId(FancyZonesEditorHelper.AccessibilityId.DialogTitle))); // check the pane header
Assert.IsNotNull(Session.Find<Element>($"Edit '{layoutName}'")); // verify it's opened for the correct layout
}
[TestMethod]
public void OpenContextMenu() // verify the context menu is opened
{
Assert.IsNotNull(_session?.OpenContextMenu(TestConstants.TemplateLayoutNames[Constants.TemplateLayout.Columns]));
Session.Find<Button>(TestConstants.TemplateLayoutNames[LayoutType.Columns]).Click(true);
Assert.IsNotNull(Session.Find<Element>(By.ClassName(ClassName.ContextMenu)));
}
[TestMethod]
public void ClickMonitor()
{
Assert.IsNotNull(Session.Find<Element>("Monitor 1"));
Assert.IsNotNull(Session.Find<Element>("Monitor 2"));
// verify that the monitor 1 is selected initially
Assert.IsTrue(Session.Find<Element>("Monitor 1").Selected);
Assert.IsFalse(Session.Find<Element>("Monitor 2").Selected);
Session.Find<Element>("Monitor 2").Click();
// verify that the monitor 2 is selected after click
Assert.IsFalse(Session.Find<Element>("Monitor 1").Selected);
Assert.IsTrue(Session.Find<Element>("Monitor 2").Selected);
}
}
}

View File

@@ -0,0 +1,402 @@
// 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 FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UITests;
using Microsoft.FancyZonesEditor.UITests.Utils;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using Windows.UI;
using static FancyZonesEditorCommon.Data.AppliedLayouts;
using static FancyZonesEditorCommon.Data.DefaultLayouts;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static FancyZonesEditorCommon.Data.LayoutTemplates;
using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class TemplateLayoutsTests : UITestBase
{
private static readonly TemplateLayoutsListWrapper Layouts = new TemplateLayoutsListWrapper
{
LayoutTemplates = new List<TemplateLayoutWrapper>
{
new TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
public TemplateLayoutsTests()
: base(PowerToysModule.FancyZone)
{
}
[TestInitialize]
public void TestInitialize()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
LayoutTemplates layoutTemplates = new LayoutTemplates();
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(Layouts));
// Default layouts should match templates
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayoutsListWrapper defaultLayoutsList = new DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayoutWrapper>
{
new DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Vertical.ToString(),
Layout = new DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
},
new DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Horizontal.ToString(),
Layout = new DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
},
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsList));
// Make sure applied layouts don't replate template settings
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayoutsListWrapper appliedLayoutsList = new AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayoutWrapper>
{
new AppliedLayoutWrapper
{
Device = new AppliedLayoutWrapper.DeviceIdWrapper
{
Monitor = "monitor-1",
MonitorInstance = "instance-id-1",
MonitorNumber = 1,
SerialNumber = "serial-number-1",
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
},
AppliedLayout = new AppliedLayoutWrapper.LayoutWrapper
{
Uuid = "{72409DFC-2B87-469B-AAC4-557273791C26}",
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
},
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsList));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
this.RestartScopeExe();
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void ZoneNumber_Cancel()
{
var type = LayoutType.Rows;
var layout = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString());
var expected = layout.ZoneCount;
Session.Find<Button>(TestConstants.TemplateLayoutNames[type]).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.TemplateZoneSlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Left);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the file
var templateLayouts = new LayoutTemplates();
var data = templateLayouts.Read(templateLayouts.File);
var actual = data.LayoutTemplates.Find(x => x.Type == type.TypeToString()).ZoneCount;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void HighlightDistance_Initialize()
{
foreach (var (type, name) in TestConstants.TemplateLayoutNames)
{
if (type == LayoutType.Blank)
{
continue;
}
Session.Find<Button>(name).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SensitivitySlider));
Assert.IsNotNull(slider);
var expected = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString()).SensitivityRadius;
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Cancel).Click();
}
}
[TestMethod]
public void HighlightDistance_Save()
{
var type = LayoutType.Focus;
var layout = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString());
var value = layout.SensitivityRadius;
Session.Find<Button>(TestConstants.TemplateLayoutNames[type]).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SensitivitySlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
var expected = value + 1; // one step right
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Save).Click();
// verify the file
var templateLayouts = new LayoutTemplates();
var data = templateLayouts.Read(templateLayouts.File);
var actual = data.LayoutTemplates.Find(x => x.Type == type.TypeToString()).SensitivityRadius;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void HighlightDistance_Cancel()
{
var type = LayoutType.Focus;
var layout = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString());
var expected = layout.SensitivityRadius;
Session.Find<Button>(TestConstants.TemplateLayoutNames[type]).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SensitivitySlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the file
var templateLayouts = new LayoutTemplates();
var data = templateLayouts.Read(templateLayouts.File);
var actual = data.LayoutTemplates.Find(x => x.Type == type.TypeToString()).SensitivityRadius;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Initialize()
{
foreach (var (type, name) in TestConstants.TemplateLayoutNames)
{
if (type == LayoutType.Blank || type == LayoutType.Focus)
{
// only for grid layouts
continue;
}
Session.Find<Button>(name).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider));
Assert.IsNotNull(slider);
var spacingEnabled = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString()).ShowSpacing;
Assert.AreEqual(spacingEnabled, slider.Enabled);
var expected = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString()).Spacing;
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Cancel).Click();
}
}
[TestMethod]
public void SpaceAroundZones_Slider_Save()
{
var type = LayoutType.PriorityGrid;
var layout = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString());
var expected = layout.Spacing + 1;
Session.Find<Button>(TestConstants.TemplateLayoutNames[type]).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
Assert.AreEqual($"{expected}", slider.Text);
Session.Find<Button>(ElementName.Save).Click();
// verify the file
var templateLayouts = new LayoutTemplates();
var data = templateLayouts.Read(templateLayouts.File);
var actual = data.LayoutTemplates.Find(x => x.Type == type.TypeToString()).Spacing;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Slider_Cancel()
{
var type = LayoutType.PriorityGrid;
var layout = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString());
var expected = layout.Spacing;
Session.Find<Button>(TestConstants.TemplateLayoutNames[type]).Click();
var slider = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider));
Assert.IsNotNull(slider);
slider.SendKeys(Keys.Right);
Assert.AreEqual($"{expected + 1}", slider.Text);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the file
var templateLayouts = new LayoutTemplates();
var data = templateLayouts.Read(templateLayouts.File);
var actual = data.LayoutTemplates.Find(x => x.Type == type.TypeToString()).Spacing;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Toggle_Save()
{
var type = LayoutType.PriorityGrid;
var layout = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString());
var expected = !layout.ShowSpacing;
Session.Find<Button>(TestConstants.TemplateLayoutNames[type]).Click();
var toggle = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingToggle));
Assert.IsNotNull(toggle);
toggle.Click();
Assert.AreEqual(expected, toggle.Selected);
Assert.AreEqual(expected, Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider))?.Enabled);
Session.Find<Button>(ElementName.Save).Click();
// verify the file
var templateLayouts = new LayoutTemplates();
var data = templateLayouts.Read(templateLayouts.File);
var actual = data.LayoutTemplates.Find(x => x.Type == type.TypeToString()).ShowSpacing;
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void SpaceAroundZones_Toggle_Cancel()
{
var type = LayoutType.PriorityGrid;
var layout = Layouts.LayoutTemplates.Find(x => x.Type == type.TypeToString());
var expected = layout.ShowSpacing;
Session.Find<Button>(TestConstants.TemplateLayoutNames[type]).Click();
var toggle = Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingToggle));
Assert.IsNotNull(toggle);
toggle.Click();
Assert.AreNotEqual(expected, toggle.Selected);
Assert.AreNotEqual(expected, Session.Find<Element>(PowerToys.UITest.By.AccessibilityId(AccessibilityId.SpacingSlider))?.Enabled);
Session.Find<Button>(ElementName.Cancel).Click();
// verify the file
var templateLayouts = new LayoutTemplates();
var data = templateLayouts.Read(templateLayouts.File);
var actual = data.LayoutTemplates.Find(x => x.Type == type.TypeToString()).ShowSpacing;
Assert.AreEqual(expected, actual);
}
}
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using FancyZonesEditor.Models;
using static FancyZonesEditorCommon.Data.Constants;
@@ -10,14 +11,14 @@ namespace Microsoft.FancyZonesEditor.UITests
{
public static class TestConstants
{
public static readonly Dictionary<TemplateLayout, string> TemplateLayoutNames = new Dictionary<TemplateLayout, string>()
public static readonly Dictionary<LayoutType, string> TemplateLayoutNames = new Dictionary<LayoutType, string>()
{
{ TemplateLayout.Empty, "No layout" },
{ TemplateLayout.Focus, "Focus" },
{ TemplateLayout.Rows, "Rows" },
{ TemplateLayout.Columns, "Columns" },
{ TemplateLayout.Grid, "Grid" },
{ TemplateLayout.PriorityGrid, "PriorityGrid" },
{ LayoutType.Blank, "No layout" },
{ LayoutType.Focus, "Focus" },
{ LayoutType.Rows, "Rows" },
{ LayoutType.Columns, "Columns" },
{ LayoutType.Grid, "Grid" },
{ LayoutType.PriorityGrid, "Priority Grid" },
};
}
}

View File

@@ -0,0 +1,846 @@
// 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.Globalization;
using System.Xml.Linq;
using FancyZonesEditor.Models;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UITests;
using Microsoft.FancyZonesEditor.UITests.Utils;
using Microsoft.FancyZonesEditor.UnitTests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.UI;
using static FancyZonesEditorCommon.Data.EditorParameters;
using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext;
using static Microsoft.FancyZonesEditor.UnitTests.Utils.FancyZonesEditorHelper;
namespace Microsoft.FancyZonesEditor.UITests
{
[TestClass]
public class UIInitializeTest : UITestBase
{
public UIInitializeTest()
: base(PowerToysModule.FancyZone)
{
}
[TestCleanup]
public void TestCleanup()
{
FancyZonesEditorHelper.Files.Restore();
}
[TestMethod]
public void EditorParams_VerifySelectedMonitor()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = false,
},
new NativeMonitorDataWrapper
{
Monitor = "monitor-2",
MonitorInstanceId = "instance-id-2",
MonitorSerialNumber = "serial-number-2",
MonitorNumber = 2,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 1920,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
this.RestartScopeExe();
Session.Find<Element>("Monitor 1").Click();
Session.Find<Element>("Monitor 2").Click();
Assert.IsFalse(Session.Find<Element>("Monitor 1").Selected);
Assert.IsTrue(Session.Find<Element>("Monitor 2").Selected);
}
[TestMethod]
public void EditorParams_VerifyMonitorScaling()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192, // 200% scaling
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
this.RestartScopeExe();
Session.Find<Element>("Monitor 1").Click();
var monitor = Session.Find<Element>("Monitor 1");
var scaling = monitor.Find<Element>(By.AccessibilityId("ScalingText"));
Assert.AreEqual("200%", scaling.Text);
}
[TestMethod]
public void EditorParams_VerifyMonitorResolution()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
this.RestartScopeExe();
Session.Find<Element>("Monitor 1").Click();
var monitor = Session.Find<Element>("Monitor 1");
var resolution = monitor.Find<Element>(By.AccessibilityId("ResolutionText"));
Assert.AreEqual("1920 × 1080", resolution.Text);
}
[TestMethod]
public void EditorParams_SpanAcrossMonitors()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = true,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
this.RestartScopeExe();
Session.Find<Element>("Monitor 1").Click();
var monitor = Session.Find<Element>("Monitor 1");
Assert.IsNotNull(monitor);
Assert.IsTrue(monitor.Selected);
}
[TestMethod]
public void AppliedLayouts_LayoutsApplied()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
new NativeMonitorDataWrapper
{
Monitor = "monitor-2",
MonitorInstanceId = "instance-id-2",
MonitorSerialNumber = "serial-number-2",
MonitorNumber = 2,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 1920,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = false,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
{
new CustomLayouts.CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Custom layout 1",
Info = new CustomLayouts().ToJsonElement(new CustomLayouts.CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CustomLayouts.CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
},
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper>
{
new AppliedLayouts.AppliedLayoutWrapper
{
Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
{
Monitor = "monitor-1",
MonitorInstance = "instance-id-1",
SerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
},
AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
{
Uuid = "{00000000-0000-0000-0000-000000000000}",
Type = LayoutType.Columns.TypeToString(),
ShowSpacing = true,
Spacing = 10,
ZoneCount = 1,
SensitivityRadius = 20,
},
},
new AppliedLayouts.AppliedLayoutWrapper
{
Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
{
Monitor = "monitor-2",
MonitorInstance = "instance-id-2",
SerialNumber = "serial-number-2",
MonitorNumber = 2,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
},
AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
{
Uuid = customLayoutListWrapper.CustomLayouts[0].Uuid,
Type = LayoutType.Custom.TypeToString(),
},
},
},
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
// check layout on monitor 1
var layoutOnMonitor1 = Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Columns]);
Assert.IsNotNull(layoutOnMonitor1);
Assert.IsTrue(layoutOnMonitor1.Selected);
// check layout on monitor 2
Session.Find<Element>(By.AccessibilityId("Monitors")).Find<Element>("Monitor 2").Click();
var layoutOnMonitor2 = Session.Find<Element>(customLayoutListWrapper.CustomLayouts[0].Name);
Assert.IsNotNull(layoutOnMonitor2);
Assert.IsTrue(layoutOnMonitor2.Selected);
}
[TestMethod]
public void AppliedLayouts_CustomLayoutsApplied_LayoutIdNotFound()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
{
new CustomLayouts.CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Custom layout 1",
Info = new CustomLayouts().ToJsonElement(new CustomLayouts.CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CustomLayouts.CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
},
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper>
{
new AppliedLayouts.AppliedLayoutWrapper
{
Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
{
Monitor = "monitor-1",
MonitorInstance = "instance-id-1",
SerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
},
AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
{
Uuid = "{00000000-0000-0000-0000-000000000000}",
Type = LayoutType.Custom.TypeToString(),
},
},
},
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
var emptyLayout = Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Blank]);
Assert.IsNotNull(emptyLayout);
Assert.IsTrue(emptyLayout.Selected);
}
[TestMethod]
public void AppliedLayouts_NoLayoutsApplied_CustomDefaultLayout()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
{
new CustomLayouts.CustomLayoutWrapper
{
Uuid = "{0D6D2F58-9184-4804-81E4-4E4CC3476DC1}",
Type = CustomLayout.Canvas.TypeToString(),
Name = "Custom layout 1",
Info = new CustomLayouts().ToJsonElement(new CustomLayouts.CanvasInfoWrapper
{
RefHeight = 1080,
RefWidth = 1920,
SensitivityRadius = 10,
Zones = new List<CustomLayouts.CanvasInfoWrapper.CanvasZoneWrapper> { },
}),
},
},
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.RestoreData();
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper>
{
new DefaultLayouts.DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Horizontal.TypeToString(),
Layout = new DefaultLayouts.DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.Custom.TypeToString(),
Uuid = customLayoutListWrapper.CustomLayouts[0].Uuid,
},
},
},
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
this.RestartScopeExe();
Session.Find<Element>(customLayoutListWrapper.CustomLayouts[0].Name).Click();
var defaultLayout = Session.Find<Element>(customLayoutListWrapper.CustomLayouts[0].Name);
Assert.IsNotNull(defaultLayout);
Assert.IsTrue(defaultLayout.Selected);
}
[TestMethod]
public void AppliedLayouts_NoLayoutsApplied_TemplateDefaultLayout()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.RestoreData();
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper>
{
new DefaultLayouts.DefaultLayoutWrapper
{
MonitorConfiguration = MonitorConfigurationType.Horizontal.TypeToString(),
Layout = new DefaultLayouts.DefaultLayoutWrapper.LayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 6,
ShowSpacing = true,
Spacing = 5,
SensitivityRadius = 20,
},
},
},
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
this.RestartScopeExe();
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Grid]).Click();
var defaultLayout = Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Grid]);
Assert.IsNotNull(defaultLayout);
Assert.IsTrue(defaultLayout.Selected);
// check the number of zones and spacing
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Grid]).Find<Button>(By.AccessibilityId(AccessibilityId.EditLayoutButton)).Click();
Assert.AreEqual(defaultLayoutsListWrapper.DefaultLayouts[0].Layout.ZoneCount, int.Parse(Session.Find<Element>(By.AccessibilityId(AccessibilityId.TemplateZoneSlider))?.Text!, CultureInfo.InvariantCulture));
Assert.AreEqual(defaultLayoutsListWrapper.DefaultLayouts[0].Layout.Spacing, int.Parse(Session.Find<Element>(By.AccessibilityId(AccessibilityId.SpacingSlider))?.Text!, CultureInfo.InvariantCulture));
Assert.AreEqual(defaultLayoutsListWrapper.DefaultLayouts[0].Layout.ShowSpacing, Session.Find<Element>(By.AccessibilityId(AccessibilityId.SpacingSlider))?.Enabled);
Assert.AreEqual(defaultLayoutsListWrapper.DefaultLayouts[0].Layout.ShowSpacing, Session.Find<Element>(By.AccessibilityId(AccessibilityId.SpacingToggle))?.Selected);
Assert.AreEqual(defaultLayoutsListWrapper.DefaultLayouts[0].Layout.SensitivityRadius, int.Parse(Session.Find<Element>(By.AccessibilityId(AccessibilityId.SensitivitySlider))?.Text!, CultureInfo.InvariantCulture));
Assert.IsNotNull(Session.Find<Element>(By.AccessibilityId(AccessibilityId.HorizontalDefaultButtonChecked)));
}
[TestMethod]
public void AppliedLayouts_VerifyDisconnectedMonitorsLayoutsAreNotChanged()
{
InitFileData();
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper>
{
new AppliedLayouts.AppliedLayoutWrapper
{
Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
{
Monitor = "monitor-2",
MonitorInstance = "instance-id-2",
SerialNumber = "serial-number-2",
MonitorNumber = 2,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
},
AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
{
Uuid = "{00000000-0000-0000-0000-000000000000}",
Type = LayoutType.Focus.TypeToString(),
ShowSpacing = true,
Spacing = 10,
ZoneCount = 4,
SensitivityRadius = 30,
},
},
new AppliedLayouts.AppliedLayoutWrapper
{
Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
{
Monitor = "monitor-3",
MonitorInstance = "instance-id-3",
SerialNumber = "serial-number-3",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
},
AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
{
Uuid = "{00000000-0000-0000-0000-000000000000}",
Type = LayoutType.Columns.TypeToString(),
ShowSpacing = true,
Spacing = 10,
ZoneCount = 1,
SensitivityRadius = 20,
},
},
},
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Rows]).Click();
// check the file
var data = appliedLayouts.Read(appliedLayouts.File);
Assert.AreEqual(parameters.Monitors.Count + appliedLayoutsWrapper.AppliedLayouts.Count, data.AppliedLayouts.Count);
foreach (var monitor in parameters.Monitors)
{
Assert.IsNotNull(data.AppliedLayouts.Find(x => x.Device.Monitor == monitor.Monitor));
}
foreach (var layout in appliedLayoutsWrapper.AppliedLayouts)
{
Assert.IsNotNull(data.AppliedLayouts.Find(x => x.Device.Monitor == layout.Device.Monitor));
}
}
[TestMethod]
public void AppliedLayouts_VerifyOtherVirtualDesktopsAreNotChanged()
{
InitFileData();
string virtualDesktop1 = "{11111111-1111-1111-1111-111111111111}";
string virtualDesktop2 = "{22222222-2222-2222-2222-222222222222}";
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = virtualDesktop1,
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper>
{
new AppliedLayouts.AppliedLayoutWrapper
{
Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
{
Monitor = "monitor-1",
MonitorInstance = "instance-id-1",
SerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = virtualDesktop2,
},
AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
{
Uuid = "{00000000-0000-0000-0000-000000000000}",
Type = LayoutType.Focus.TypeToString(),
ShowSpacing = true,
Spacing = 10,
ZoneCount = 4,
SensitivityRadius = 30,
},
},
},
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe();
Session.Find<Element>(TestConstants.TemplateLayoutNames[LayoutType.Rows]).Click();
// check the file
var data = appliedLayouts.Read(appliedLayouts.File);
Assert.AreEqual(parameters.Monitors.Count + appliedLayoutsWrapper.AppliedLayouts.Count, data.AppliedLayouts.Count);
Assert.IsNotNull(data.AppliedLayouts.Find(x => x.Device.VirtualDesktop == virtualDesktop1));
Assert.IsNotNull(data.AppliedLayouts.Find(x => x.Device.VirtualDesktop == virtualDesktop2));
Assert.AreEqual(appliedLayoutsWrapper.AppliedLayouts[0].AppliedLayout.Type, data.AppliedLayouts.Find(x => x.Device.VirtualDesktop == virtualDesktop2).AppliedLayout.Type);
Assert.AreEqual(LayoutType.Rows.TypeToString(), data.AppliedLayouts.Find(x => x.Device.VirtualDesktop == virtualDesktop1).AppliedLayout.Type);
}
private void InitFileData()
{
EditorParameters editorParameters = new EditorParameters();
ParamsWrapper parameters = new ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<NativeMonitorDataWrapper>
{
new NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 192, // 200% scaling
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Blank.TypeToString(),
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Focus.TypeToString(),
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Rows.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Columns.TypeToString(),
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.Grid.TypeToString(),
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = LayoutType.PriorityGrid.TypeToString(),
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
}
}
}

View File

@@ -28,6 +28,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj" />
<ProjectReference Include="..\editor\FancyZonesEditor\FancyZonesEditor.csproj" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,288 @@
// 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.Globalization;
using System.IO;
using System.Reflection;
using FancyZonesEditorCommon.Data;
using Microsoft.FancyZonesEditor.UITests.Utils;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ModernWpf.Controls;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
namespace Microsoft.FancyZonesEditor.UnitTests.Utils
{
public class FancyZonesEditorHelper
{
private static FancyZonesEditorFiles? _files;
public static FancyZonesEditorFiles Files
{
get
{
if (_files == null)
{
_files = new FancyZonesEditorFiles();
}
return _files;
}
}
public static class AccessibilityId
{
// main window
public const string MainWindow = "MainWindow1";
public const string Monitors = "Monitors";
public const string NewLayoutButton = "NewLayoutButton";
// layout card
public const string EditLayoutButton = "EditLayoutButton";
// edit layout window: common for template and custom layouts
public const string DialogTitle = "EditLayoutDialogTitle";
public const string SensitivitySlider = "SensitivityInput";
public const string SpacingSlider = "Spacing";
public const string SpacingToggle = "spaceAroundSetting";
public const string HorizontalDefaultButtonUnchecked = "SetLayoutAsHorizontalDefaultButton";
public const string VerticalDefaultButtonUnchecked = "SetLayoutAsVerticalDefaultButton";
public const string HorizontalDefaultButtonChecked = "HorizontalDefaultLayoutButton";
public const string VerticalDefaultButtonChecked = "VerticalDefaultLayoutButton";
// edit template layout window
public const string CopyTemplate = "createFromTemplateLayoutButton";
public const string TemplateZoneSlider = "TemplateZoneCount";
// edit custom layout window
public const string DuplicateLayoutButton = "duplicateLayoutButton";
public const string DeleteLayoutButton = "deleteLayoutButton";
public const string KeySelectionComboBox = "quickKeySelectionComboBox";
public const string EditZonesButton = "editZoneLayoutButton";
public const string DeleteTextButton = "DeleteButton";
public const string HotkeyComboBox = "quickKeySelectionComboBox";
public const string NewZoneButton = "newZoneButton";
public const string TopRightCorner = "NEResize";
// layout creation dialog
public const string GridRadioButton = "GridLayoutRadioButton";
public const string CanvasRadioButton = "CanvasLayoutRadioButton";
// confirmation dialog
public const string PrimaryButton = "PrimaryButton";
public const string SecondaryButton = "SecondaryButton";
}
public static class ElementName
{
public const string Save = "Save";
public const string Cancel = "Cancel";
// context menu
public const string Edit = "Edit";
public const string EditZones = "Edit zones";
public const string Delete = "Delete";
public const string Duplicate = "Duplicate";
public const string CreateCustomLayout = "Create custom layout";
// canvas layout editor
public const string CanvasEditorWindow = "Canvas layout editor";
// grid layout editor
public const string GridLayoutEditor = "Grid layout editor";
public const string MergeZonesButton = "Merge zones";
}
public static class ClassName
{
public const string ContextMenu = "ContextMenu";
public const string TextBox = "TextBox";
public const string Popup = "Popup";
// layout editor
public const string CanvasZone = "CanvasZone";
public const string GridZone = "GridZone";
public const string Button = "Button";
public const string Thumb = "Thumb";
}
public static void ClickContextMenuItem(Session session, string layoutName, string menuItem)
{
session.Find<Element>(layoutName).Click(true);
session.Find<Element>(By.ClassName(ClassName.ContextMenu)).Find<Element>(menuItem).Click();
}
public static Element? GetZone(Session session, int zoneNumber, string zoneClassName)
{
var zones = session.FindAll<Element>(By.ClassName(zoneClassName));
foreach (var zone in zones)
{
try
{
zone.Find<Element>(zoneNumber.ToString(CultureInfo.InvariantCulture));
Assert.IsNotNull(zone, "zone not found");
return zone;
}
catch
{
// required number not found in the zone
}
}
Assert.IsNotNull(zones, $"zoneClassName : {zoneClassName} not found");
return null;
}
public static void MergeGridZones(Session session, int zoneNumber1, int zoneNumber2)
{
var zone1 = GetZone(session, zoneNumber1, ClassName.GridZone);
var zone2 = GetZone(session, zoneNumber2, ClassName.GridZone);
Assert.IsNotNull(zone1, "first zone not found");
Assert.IsNotNull(zone2, "second zone not found");
if (zone1 == null || zone2 == null)
{
Assert.Fail("zone is null");
return;
}
zone1.Drag(zone2);
session.Find<Element>(ElementName.MergeZonesButton).Click();
}
public static void MoveSplitter(Session session, int index, int xOffset, int yOffset)
{
var thumbs = session.FindAll<Element>(By.ClassName(ClassName.Thumb));
if (thumbs.Count == 0 || index >= thumbs.Count)
{
return;
}
thumbs[index].Drag(xOffset, yOffset);
Console.WriteLine($"Moving splitter {index} by ({xOffset}, {yOffset})");
}
public static void ClickDeleteZone(Session session, int zoneNumber)
{
var zone = FancyZonesEditorHelper.GetZone(session, zoneNumber, ClassName.CanvasZone);
Assert.IsNotNull(zone);
var button = zone.Find<Button>(By.ClassName(ClassName.Button));
Assert.IsNotNull(button);
button.Click();
}
public static void InitFancyZonesLayout()
{
// prepare files to launch Editor without errors
EditorParameters editorParameters = new EditorParameters();
EditorParameters.ParamsWrapper parameters = new EditorParameters.ParamsWrapper
{
ProcessId = 1,
SpanZonesAcrossMonitors = false,
Monitors = new List<EditorParameters.NativeMonitorDataWrapper>
{
new EditorParameters.NativeMonitorDataWrapper
{
Monitor = "monitor-1",
MonitorInstanceId = "instance-id-1",
MonitorSerialNumber = "serial-number-1",
MonitorNumber = 1,
VirtualDesktop = "{FF34D993-73F3-4B8C-AA03-73730A01D6A8}",
Dpi = 96,
LeftCoordinate = 0,
TopCoordinate = 0,
WorkAreaHeight = 1040,
WorkAreaWidth = 1920,
MonitorHeight = 1080,
MonitorWidth = 1920,
IsSelected = true,
},
},
};
FancyZonesEditorHelper.Files.ParamsIOHelper.WriteData(editorParameters.Serialize(parameters));
LayoutTemplates layoutTemplates = new LayoutTemplates();
LayoutTemplates.TemplateLayoutsListWrapper templateLayoutsListWrapper = new LayoutTemplates.TemplateLayoutsListWrapper
{
LayoutTemplates = new List<LayoutTemplates.TemplateLayoutWrapper>
{
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Empty],
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Focus],
ZoneCount = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Rows],
ZoneCount = 2,
ShowSpacing = true,
Spacing = 10,
SensitivityRadius = 10,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Columns],
ZoneCount = 2,
ShowSpacing = true,
Spacing = 20,
SensitivityRadius = 20,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.Grid],
ZoneCount = 4,
ShowSpacing = false,
Spacing = 10,
SensitivityRadius = 30,
},
new LayoutTemplates.TemplateLayoutWrapper
{
Type = Constants.TemplateLayoutJsonTags[Constants.TemplateLayout.PriorityGrid],
ZoneCount = 3,
ShowSpacing = true,
Spacing = 1,
SensitivityRadius = 40,
},
},
};
FancyZonesEditorHelper.Files.LayoutTemplatesIOHelper.WriteData(layoutTemplates.Serialize(templateLayoutsListWrapper));
CustomLayouts customLayouts = new CustomLayouts();
CustomLayouts.CustomLayoutListWrapper customLayoutListWrapper = new CustomLayouts.CustomLayoutListWrapper
{
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.WriteData(customLayouts.Serialize(customLayoutListWrapper));
DefaultLayouts defaultLayouts = new DefaultLayouts();
DefaultLayouts.DefaultLayoutsListWrapper defaultLayoutsListWrapper = new DefaultLayouts.DefaultLayoutsListWrapper
{
DefaultLayouts = new List<DefaultLayouts.DefaultLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.DefaultLayoutsIOHelper.WriteData(defaultLayouts.Serialize(defaultLayoutsListWrapper));
LayoutHotkeys layoutHotkeys = new LayoutHotkeys();
LayoutHotkeys.LayoutHotkeysWrapper layoutHotkeysWrapper = new LayoutHotkeys.LayoutHotkeysWrapper
{
LayoutHotkeys = new List<LayoutHotkeys.LayoutHotkeyWrapper> { },
};
FancyZonesEditorHelper.Files.LayoutHotkeysIOHelper.WriteData(layoutHotkeys.Serialize(layoutHotkeysWrapper));
AppliedLayouts appliedLayouts = new AppliedLayouts();
AppliedLayouts.AppliedLayoutsListWrapper appliedLayoutsWrapper = new AppliedLayouts.AppliedLayoutsListWrapper
{
AppliedLayouts = new List<AppliedLayouts.AppliedLayoutWrapper> { },
};
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
}
}
}

View File

@@ -0,0 +1,108 @@
## This is for tracking UI-Tests migration progress for FancyZones Editor Module
Refer to [release check list] (https://github.com/microsoft/PowerToys/blob/releaseChecklist/doc/releases/tests-checklist-template.md) for all manual tests.
### Existing Manual Test-cases run by previous PowerToys owner
For existing manual test-cases, we will convert them to UI-Tests and run them in CI and Release pipeline
* Launch Host File Editor:
- [ ] Open editor from the settings
- [ ] Open editor with a shortcut
- [ ] Create a new layout (grid and canvas)
- [ ] Duplicate a template and a custom layout
- [ ] Delete layout
- [ ] Edit templates (number of zones, spacing, distance to highlight adjacent zones). Verify after reopening the editor that saved settings are kept the same.
- [ ] Edit canvas layout: zones size and position, create or delete zones.
- [ ] Edit grid layout: split, merge, resize zones.
- [ ] Check Save and apply and Cancel buttons behavior after editing.
- [ ] Assign a layout to each monitor.
- [ ] Assign keys to quickly switch layouts (custom layouts only), Win + Ctrl + Alt + number.
- [ ] Assign horizontal and vertical default layouts
- [ ] Test duplicate layout focus
- Select any layout X in 'Templates' or 'Custom' section by click left mouse button
- Mouse right button click on any layout Y in 'Templates' or 'Custom' sections
- Duplicate it by clicking 'Create custom layout' (Templates section) or 'Duplicate' in 'Custom' section
- Expect the layout Y is duplicated
### Additional UI-Tests cases
- [ ] Add test data and start → verify data is correct (custom layouts, template layouts, defaults, shortcut keys)
- [ ] Create a new canvas - verify layout exists
- [ ] Create a new canvas - cancel - doesnt exist
- [ ] Create a new grid - verify the layout exists
- [ ] Create a new grid - cancel - doesnt exist
- [ ] Duplicate template by button (+ check default)
- [ ] Duplicate template by menu (+ check default)
- [ ] Duplicate custom by button (+ check shortcut key and default)
- [ ] Duplicate custom by menu (+ check shortcut key and default)
- [ ] Delete non-applied layout
- [ ] Delete applied layout
- [ ] Delete-cancel
- [ ] Delete from context menu
- [ ] Delete: hotkey released
- [ ] Delete: default layout reset to default-default
- [ ] Edit template and save
- [ ] Edit template and cancel
- [ ] Edit custom and save
- [ ] Edit custom and cancel
- [ ] Edit canvas: add zone
- [ ] Edit canvas: delete zone
- [ ] Edit canvas: move zone
- [ ] Edit canvas: resize zone
- [ ] Edit grid: split zone
- [ ] Edit grid: merge zones
- [ ] Edit grid: move splitter
- [ ] UI Init: assigned layouts selected
- [ ] UI Init: applied default - check params
- [ ] UI Init: assigned custom layout, but id not found
- [ ] Assign the same template but with different params to monitors
- [ ] Assign layout on each monitor
- [ ] Assign custom
- [ ] Assign template
- [ ] Assign shortcut key and save
- [ ] Assign shortcut key and cancel
- [ ] Reset shortcut key and save
- [ ] Reset shortcut key and cancel
- [ ] Set default layout + verify both prev and current after reopening
- [ ] applied-layouts.json keeps info about not connected devices - verify theyre present after closing
- [ ] applied-layouts.json keeps info about other virtual desktops
- [ ] first launch without custom-layouts.json, default-layouts.json, layout-hotkeys.json and layout-templates.json

View File

@@ -5,13 +5,15 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using Common.UI;
using FancyZoneEditor.Telemetry;
using FancyZonesEditor.Utils;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
namespace FancyZonesEditor
{
@@ -56,6 +58,8 @@ namespace FancyZonesEditor
public App()
{
PowerToysTelemetry.Log.WriteEvent(new FancyZonesEditorStartEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
var languageTag = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(languageTag))

View File

@@ -335,6 +335,7 @@
</ScrollViewer.DataContext>
<Grid>
<ui:GridView
x:Name="Monitors"
HorizontalAlignment="Center"
IsItemClickEnabled="True"
IsSelectionEnabled="True"

View File

@@ -13,11 +13,12 @@ using System.Windows.Automation.Peers;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using Common.UI;
using FancyZoneEditor.Telemetry;
using FancyZonesEditor.Models;
using FancyZonesEditor.Utils;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using ModernWpf.Controls;
namespace FancyZonesEditor
@@ -69,6 +70,8 @@ namespace FancyZonesEditor
// reinit considering work area rect
_settings.InitModels();
PowerToysTelemetry.Log.WriteEvent(new FancyZonesEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
private void BringToFront()
@@ -491,6 +494,8 @@ namespace FancyZonesEditor
App.FancyZonesEditorIO.SerializeAppliedLayouts();
App.FancyZonesEditorIO.SerializeCustomLayouts();
App.FancyZonesEditorIO.SerializeDefaultLayouts();
App.FancyZonesEditorIO.SerializeLayoutHotkeys();
App.FancyZonesEditorIO.SerializeLayoutTemplates();
}
}

View File

@@ -328,12 +328,16 @@ namespace FancyZonesEditor.Models
// Removes this Layout from the registry and the loaded CustomModels list
public void Delete()
{
var customModels = MainWindowSettingsModel.CustomModels;
if (_quickKey != -1)
{
MainWindowSettingsModel.LayoutHotkeys.FreeKey(QuickKey);
foreach (var module in customModels)
{
module.FirePropertyChanged(nameof(QuickKeysAvailable));
}
}
var customModels = MainWindowSettingsModel.CustomModels;
int i = customModels.IndexOf(this);
if (i != -1)
{

View File

@@ -0,0 +1,59 @@
// 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 FancyZonesEditor.Models
{
public static class LayoutTypeEnumExtension
{
private const string BlankJsonTag = "blank";
private const string FocusJsonTag = "focus";
private const string RowsJsonTag = "rows";
private const string ColumnsJsonTag = "columns";
private const string GridJsonTag = "grid";
private const string PriorityGridJsonTag = "priority-grid";
private const string CustomLayoutJsonTag = "custom";
public static string TypeToString(this LayoutType value)
{
switch (value)
{
case LayoutType.Blank:
return BlankJsonTag;
case LayoutType.Focus:
return FocusJsonTag;
case LayoutType.Rows:
return RowsJsonTag;
case LayoutType.Columns:
return ColumnsJsonTag;
case LayoutType.Grid:
return GridJsonTag;
case LayoutType.PriorityGrid:
return PriorityGridJsonTag;
}
return CustomLayoutJsonTag;
}
public static LayoutType TypeFromString(string value)
{
switch (value)
{
case BlankJsonTag:
return LayoutType.Blank;
case FocusJsonTag:
return LayoutType.Focus;
case RowsJsonTag:
return LayoutType.Rows;
case ColumnsJsonTag:
return LayoutType.Columns;
case GridJsonTag:
return LayoutType.Grid;
case PriorityGridJsonTag:
return LayoutType.PriorityGrid;
}
return LayoutType.Custom;
}
}
}

View File

@@ -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.
namespace FancyZonesEditor.Models
{
public static class MonitorConfigurationTypeEnumExtensions
{
private const string HorizontalJsonTag = "horizontal";
private const string VerticalJsonTag = "vertical";
public static string TypeToString(this MonitorConfigurationType value)
{
switch (value)
{
case MonitorConfigurationType.Horizontal:
return HorizontalJsonTag;
case MonitorConfigurationType.Vertical:
return VerticalJsonTag;
}
return HorizontalJsonTag;
}
public static MonitorConfigurationType TypeFromString(string value)
{
switch (value)
{
case HorizontalJsonTag:
return MonitorConfigurationType.Horizontal;
case VerticalJsonTag:
return MonitorConfigurationType.Vertical;
}
return MonitorConfigurationType.Horizontal;
}
}
}

View File

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

View File

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

View File

@@ -258,14 +258,20 @@ private:
for (DWORD i = 0; i < fileCount; i++)
{
IShellItem* shellItem;
psiItemArray->GetItemAt(i, &shellItem);
LPWSTR itemName;
// Retrieves the entire file system path of the file from its shell item
shellItem->GetDisplayName(SIGDN_FILESYSPATH, &itemName);
CString fileName(itemName);
fileName.Append(_T("\r\n"));
// Write the file path into the input stream for image resizer
writePipe.Write(fileName, fileName.GetLength() * sizeof(TCHAR));
HRESULT getItemAtResult = psiItemArray->GetItemAt(i, &shellItem);
if (SUCCEEDED(getItemAtResult))
{
LPWSTR itemName;
// Retrieves the entire file system path of the file from its shell item
HRESULT getDisplayResult = shellItem->GetDisplayName(SIGDN_FILESYSPATH, &itemName);
if (SUCCEEDED(getDisplayResult))
{
CString fileName(itemName);
fileName.Append(_T("\r\n"));
// Write the file path into the input stream for image resizer
writePipe.Write(fileName, fileName.GetLength() * sizeof(TCHAR));
}
}
}
writePipe.Close();
}

View File

@@ -392,8 +392,14 @@ HRESULT __stdcall CContextMenuHandler::GetState(IShellItemArray* psiItemArray, B
PERCEIVED type;
PERCEIVEDFLAG flag;
IShellItem* shellItem;
//Check extension of first item in the list (the item which is right-clicked on)
psiItemArray->GetItemAt(0, &shellItem);
HRESULT getItemAtResult = psiItemArray->GetItemAt(0, &shellItem);
if(!SUCCEEDED(getItemAtResult)) {
// Avoid crashes in the following code.
return E_FAIL;
}
LPTSTR pszPath;
// Retrieves the entire file system path of the file from its shell item
HRESULT getDisplayResult = shellItem->GetDisplayName(SIGDN_FILESYSPATH, &pszPath);

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