Files
PowerToys/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs
Shawn Yuan (from Dev Box) b0754a3e25 update
2026-01-29 13:43:51 +08:00

1518 lines
52 KiB
C#

// 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.IO;
using System.IO.Pipes;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
#if !BUILD_INFO_PUBLISH_AOT
using StreamJsonRpc;
#endif
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class MouseWithoutBordersViewModel : PageViewModelBase
{
protected override string ModuleName => MouseWithoutBordersSettings.ModuleName;
// These should be in the same order as the ComboBoxItems in MouseWithoutBordersPage.xaml switch machine shortcut options
private readonly int[] _switchBetweenMachineShortcutOptions =
{
112,
49,
0,
};
private readonly Lock _machineMatrixStringLock = new();
private bool _disposed;
private static readonly Dictionary<SocketStatus, Brush> StatusColors = new Dictionary<SocketStatus, Brush>()
{
{ SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) },
{ SocketStatus.Resolving, new SolidColorBrush(Colors.Yellow) },
{ SocketStatus.Connecting, new SolidColorBrush(Colors.Orange) },
{ SocketStatus.Handshaking, new SolidColorBrush(Colors.Blue) },
{ SocketStatus.Error, new SolidColorBrush(Colors.Red) },
{ SocketStatus.ForceClosed, new SolidColorBrush(Colors.Purple) },
{ SocketStatus.InvalidKey, new SolidColorBrush(Colors.Brown) },
{ SocketStatus.Timeout, new SolidColorBrush(Colors.Pink) },
{ SocketStatus.SendError, new SolidColorBrush(Colors.Maroon) },
{ SocketStatus.Connected, new SolidColorBrush(Colors.Green) },
};
private bool _connectFieldsVisible;
public bool IsElevated { get => GeneralSettingsConfig.IsElevated; }
public bool CanUninstallService { get => GeneralSettingsConfig.IsElevated && !UseService; }
public ButtonClickCommand AddFirewallRuleEventHandler => new ButtonClickCommand(AddFirewallRule);
public ButtonClickCommand UninstallServiceEventHandler => new ButtonClickCommand(UninstallService);
public bool ShowOriginalUI
{
get
{
if (_useOriginalUserInterfaceGpoConfiguration == GpoRuleConfigured.Disabled)
{
return false;
}
return Settings.Properties.ShowOriginalUI;
}
set
{
if (!_useOriginalUserInterfaceIsGPOConfigured && (Settings.Properties.ShowOriginalUI != value))
{
Settings.Properties.ShowOriginalUI = value;
NotifyPropertyChanged(nameof(ShowOriginalUI));
}
}
}
public bool CardForOriginalUiSettingIsEnabled => _useOriginalUserInterfaceIsGPOConfigured == false;
public bool ShowPolicyConfiguredInfoForOriginalUiSetting => IsEnabled && _useOriginalUserInterfaceIsGPOConfigured;
public bool UseService
{
get
{
if (_allowServiceModeGpoConfiguration == GpoRuleConfigured.Disabled)
{
return false;
}
return Settings.Properties.UseService;
}
set
{
if (_allowServiceModeIsGPOConfigured)
{
return;
}
var valueChanged = Settings.Properties.UseService != value;
// Set the UI property itself instantly
if (valueChanged)
{
Settings.Properties.UseService = value;
OnPropertyChanged(nameof(UseService));
OnPropertyChanged(nameof(CanUninstallService));
OnPropertyChanged(nameof(ShowInfobarRunAsAdminText));
// Must block here until the process exits
Task.Run(async () =>
{
await SubmitShutdownRequestAsync();
_uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
{
Settings.Properties.UseService = value;
NotifyPropertyChanged(nameof(UseService));
});
});
}
}
}
public bool UseServiceSettingIsEnabled => _allowServiceModeIsGPOConfigured == false;
public bool ConnectFieldsVisible
{
get => _connectFieldsVisible;
set
{
if (_connectFieldsVisible != value)
{
_connectFieldsVisible = value;
OnPropertyChanged(nameof(ConnectFieldsVisible));
}
}
}
private string _connectSecurityKey;
public string ConnectSecurityKey
{
get => _connectSecurityKey;
set
{
if (_connectSecurityKey != value)
{
_connectSecurityKey = value;
OnPropertyChanged(nameof(ConnectSecurityKey));
}
}
}
private string _connectPCName;
public string ConnectPCName
{
get => _connectPCName;
set
{
if (_connectPCName != value)
{
_connectPCName = value;
OnPropertyChanged(nameof(ConnectPCName));
}
}
}
private SettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; }
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private bool _isEnabled;
// Configuration policy variables
private GpoRuleConfigured _clipboardSharingEnabledGpoConfiguration;
private bool _clipboardSharingEnabledIsGPOConfigured;
private GpoRuleConfigured _fileTransferEnabledGpoConfiguration;
private bool _fileTransferEnabledIsGPOConfigured;
private GpoRuleConfigured _useOriginalUserInterfaceGpoConfiguration;
private bool _useOriginalUserInterfaceIsGPOConfigured;
private GpoRuleConfigured _disallowBlockingScreensaverGpoConfiguration;
private bool _disallowBlockingScreensaverIsGPOConfigured;
private GpoRuleConfigured _allowServiceModeGpoConfiguration;
private bool _allowServiceModeIsGPOConfigured;
private GpoRuleConfigured _sameSubnetOnlyGpoConfiguration;
private bool _sameSubnetOnlyIsGPOConfigured;
private GpoRuleConfigured _validateRemoteIpGpoConfiguration;
private bool _validateRemoteIpIsGPOConfigured;
private GpoRuleConfigured _disableUserDefinedIpMappingRulesGpoConfiguration;
private bool _disableUserDefinedIpMappingRulesIsGPOConfigured;
private string _policyDefinedIpMappingRulesGPOData;
private bool _policyDefinedIpMappingRulesIsGPOConfigured;
public string MachineHostName
{
get
{
try
{
return Dns.GetHostName();
}
catch
{
return string.Empty;
}
}
}
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}
// SocketStatus enum is now defined in Settings.UI.Library\MouseWithoutBordersIpcModels.cs
#if !BUILD_INFO_PUBLISH_AOT
private interface ISettingsSyncHelper
{
[Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)]
public struct MachineSocketState
{
// Disable false-positive warning due to IPC
#pragma warning disable CS0649
[Newtonsoft.Json.JsonProperty]
public string Name;
[Newtonsoft.Json.JsonProperty]
public SocketStatus Status;
#pragma warning restore CS0649
}
void Shutdown();
void Reconnect();
void GenerateNewKey();
void ConnectToMachine(string machineName, string securityKey);
Task<MachineSocketState[]> RequestMachineSocketStateAsync();
}
#endif
private static CancellationTokenSource _cancellationTokenSource;
private static Task _machinePollingThreadTask;
private static SemaphoreSlim _ipcSemaphore = new SemaphoreSlim(1, 1);
private static NamedPipeClientStream syncHelperStream;
#if BUILD_INFO_PUBLISH_AOT
// AOT-compatible IPC client wrapper
private sealed partial class SyncHelper : IDisposable
{
public SyncHelper(NamedPipeClientStream stream)
{
Stream = stream;
Client = new MouseWithoutBordersIpcClient(stream);
}
public NamedPipeClientStream Stream { get; }
public MouseWithoutBordersIpcClient Client { get; private set; }
public void Dispose()
{
Client?.Dispose();
}
}
private async Task<SyncHelper> GetSettingsSyncHelperAsync()
{
try
{
var recreateStream = false;
if (syncHelperStream == null)
{
recreateStream = true;
}
else
{
if (!syncHelperStream.IsConnected || !syncHelperStream.CanWrite)
{
await syncHelperStream.DisposeAsync();
recreateStream = true;
}
}
if (recreateStream)
{
syncHelperStream = new NamedPipeClientStream(".", "MouseWithoutBorders/SettingsSync", PipeDirection.InOut, PipeOptions.Asynchronous);
await syncHelperStream.ConnectAsync(10000);
}
return new SyncHelper(syncHelperStream);
}
catch (Exception ex)
{
if (IsEnabled)
{
Logger.LogError($"Couldn't create SettingsSync (AOT): {ex}");
}
return null;
}
}
#else
// StreamJsonRpc-based IPC client wrapper (non-AOT builds)
private sealed partial class SyncHelper : IDisposable
{
public SyncHelper(NamedPipeClientStream stream)
{
Stream = stream;
Endpoint = JsonRpc.Attach<ISettingsSyncHelper>(Stream);
}
public NamedPipeClientStream Stream { get; }
public ISettingsSyncHelper Endpoint { get; private set; }
public void Dispose()
{
((IDisposable)Endpoint).Dispose();
}
}
private async Task<SyncHelper> GetSettingsSyncHelperAsync()
{
try
{
var recreateStream = false;
if (syncHelperStream == null)
{
recreateStream = true;
}
else
{
if (!syncHelperStream.IsConnected || !syncHelperStream.CanWrite)
{
await syncHelperStream.DisposeAsync();
recreateStream = true;
}
}
if (recreateStream)
{
syncHelperStream = new NamedPipeClientStream(".", "MouseWithoutBorders/SettingsSync", PipeDirection.InOut, PipeOptions.Asynchronous);
await syncHelperStream.ConnectAsync(10000);
}
return new SyncHelper(syncHelperStream);
}
catch (Exception ex)
{
if (IsEnabled)
{
Logger.LogError($"Couldn't create SettingsSync: {ex}");
}
return null;
}
}
#endif
public async Task SubmitShutdownRequestAsync()
{
await _ipcSemaphore.WaitAsync();
try
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
{
#if BUILD_INFO_PUBLISH_AOT
await syncHelper.Client.ShutdownAsync();
#else
syncHelper.Endpoint?.Shutdown();
var task = syncHelper.Stream.FlushAsync();
if (task != null)
{
await task;
}
#endif
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
public async Task SubmitReconnectRequestAsync()
{
await _ipcSemaphore.WaitAsync();
try
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
{
#if BUILD_INFO_PUBLISH_AOT
await syncHelper.Client.ReconnectAsync();
#else
syncHelper.Endpoint?.Reconnect();
var task = syncHelper.Stream.FlushAsync();
if (task != null)
{
await task;
}
#endif
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
public async Task SubmitNewKeyRequestAsync()
{
await _ipcSemaphore.WaitAsync();
try
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
{
#if BUILD_INFO_PUBLISH_AOT
await syncHelper.Client.GenerateNewKeyAsync();
#else
syncHelper.Endpoint?.GenerateNewKey();
var task = syncHelper.Stream.FlushAsync();
if (task != null)
{
await task;
}
#endif
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
public async Task SubmitConnectionRequestAsync(string pcName, string securityKey)
{
await _ipcSemaphore.WaitAsync();
try
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
{
#if BUILD_INFO_PUBLISH_AOT
await syncHelper.Client.ConnectToMachineAsync(pcName, securityKey);
#else
syncHelper.Endpoint?.ConnectToMachine(pcName, securityKey);
var task = syncHelper.Stream.FlushAsync();
if (task != null)
{
await task;
}
#endif
}
}
}
finally
{
_ipcSemaphore.Release();
}
}
private async Task<MachineSocketState[]> PollMachineSocketStateAsync()
{
await _ipcSemaphore.WaitAsync();
try
{
using (var syncHelper = await GetSettingsSyncHelperAsync())
{
if (syncHelper != null)
{
#if BUILD_INFO_PUBLISH_AOT
return await syncHelper.Client.RequestMachineSocketStateAsync();
#else
var task = syncHelper.Endpoint?.RequestMachineSocketStateAsync();
if (task != null)
{
var oldStates = await task;
// Convert from ISettingsSyncHelper.MachineSocketState to MachineSocketState
return oldStates.Select(s => new MachineSocketState
{
Name = s.Name,
Status = (SocketStatus)s.Status,
}).ToArray();
}
#endif
}
return Array.Empty<MachineSocketState>();
}
}
finally
{
_ipcSemaphore.Release();
}
}
private MouseWithoutBordersSettings Settings { get; set; }
private DispatcherQueue _uiDispatcherQueue;
public MouseWithoutBordersViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, DispatcherQueue uiDispatcherQueue)
{
SettingsUtils = settingsUtils;
_uiDispatcherQueue = uiDispatcherQueue;
// To obtain the general settings configurations of PowerToys Settings.
ArgumentNullException.ThrowIfNull(settingsRepository);
GeneralSettingsConfig = settingsRepository.SettingsConfig;
InitializeEnabledValue();
InitializePolicyValues();
// MouseWithoutBorders settings may be changed by the logic in the utility as machines connect. We need to get a fresh version every time instead of using a repository.
MouseWithoutBordersSettings moduleSettings;
moduleSettings = SettingsUtils.GetSettingsOrDefault<MouseWithoutBordersSettings>("MouseWithoutBorders");
LoadViewModelFromSettings(moduleSettings);
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
_machinePollingThreadTask = StartMachineStatusPollingThread(_machinePollingThreadTask, _cancellationTokenSource.Token);
}
private Task StartMachineStatusPollingThread(Task previousThreadTask, CancellationToken token)
{
return Task.Run(
async () =>
{
previousThreadTask?.Wait();
while (!token.IsCancellationRequested)
{
Dictionary<string, MachineSocketState> states = null;
try
{
states = (await PollMachineSocketStateAsync())?.ToDictionary(s => s.Name, StringComparer.OrdinalIgnoreCase);
}
catch (Exception ex)
{
Logger.LogInfo($"Poll MachineSocketState error: {ex}");
continue;
}
if (states != null)
{
lock (_machineMatrixStringLock)
{
foreach (var machine in machineMatrixString)
{
if (states.TryGetValue(machine.Item.Name, out var state))
{
_uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
{
try
{
machine.Item.StatusBrush = StatusColors[state.Status];
}
catch (Exception)
{
}
});
}
}
}
}
Thread.Sleep(500);
}
},
_cancellationTokenSource.Token);
}
private void InitializeEnabledValue()
{
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredMouseWithoutBordersEnabledValue();
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isEnabled = GeneralSettingsConfig.Enabled.MouseWithoutBorders;
}
}
private void InitializePolicyValues()
{
// Policies supporting only enabled state
_disallowBlockingScreensaverGpoConfiguration = GPOWrapper.GetConfiguredMwbDisallowBlockingScreensaverValue();
_disallowBlockingScreensaverIsGPOConfigured = _disallowBlockingScreensaverGpoConfiguration == GpoRuleConfigured.Enabled;
_disableUserDefinedIpMappingRulesGpoConfiguration = GPOWrapper.GetConfiguredMwbDisableUserDefinedIpMappingRulesValue();
_disableUserDefinedIpMappingRulesIsGPOConfigured = _disableUserDefinedIpMappingRulesGpoConfiguration == GpoRuleConfigured.Enabled;
// Policies supporting only disabled state
_allowServiceModeGpoConfiguration = GPOWrapper.GetConfiguredMwbAllowServiceModeValue();
_allowServiceModeIsGPOConfigured = _allowServiceModeGpoConfiguration == GpoRuleConfigured.Disabled;
_clipboardSharingEnabledGpoConfiguration = GPOWrapper.GetConfiguredMwbClipboardSharingEnabledValue();
_clipboardSharingEnabledIsGPOConfigured = _clipboardSharingEnabledGpoConfiguration == GpoRuleConfigured.Disabled;
_fileTransferEnabledGpoConfiguration = GPOWrapper.GetConfiguredMwbFileTransferEnabledValue();
_fileTransferEnabledIsGPOConfigured = _fileTransferEnabledGpoConfiguration == GpoRuleConfigured.Disabled;
_useOriginalUserInterfaceGpoConfiguration = GPOWrapper.GetConfiguredMwbUseOriginalUserInterfaceValue();
_useOriginalUserInterfaceIsGPOConfigured = _useOriginalUserInterfaceGpoConfiguration == GpoRuleConfigured.Disabled;
// Policies supporting enabled and disabled state
_sameSubnetOnlyGpoConfiguration = GPOWrapper.GetConfiguredMwbSameSubnetOnlyValue();
_sameSubnetOnlyIsGPOConfigured = _sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Enabled || _sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Disabled;
_validateRemoteIpGpoConfiguration = GPOWrapper.GetConfiguredMwbValidateRemoteIpValue();
_validateRemoteIpIsGPOConfigured = _validateRemoteIpGpoConfiguration == GpoRuleConfigured.Enabled || _validateRemoteIpGpoConfiguration == GpoRuleConfigured.Disabled;
// Special policies
_policyDefinedIpMappingRulesGPOData = GPOWrapper.GetConfiguredMwbPolicyDefinedIpMappingRules();
_policyDefinedIpMappingRulesIsGPOConfigured = !string.IsNullOrWhiteSpace(_policyDefinedIpMappingRulesGPOData);
}
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [
ToggleEasyMouseShortcut,
LockMachinesShortcut,
HotKeySwitch2AllPC,
ReconnectShortcut],
};
return hotkeysDict;
}
private void LoadViewModelFromSettings(MouseWithoutBordersSettings moduleSettings)
{
ArgumentNullException.ThrowIfNull(moduleSettings);
Settings = moduleSettings;
/* TODO: Error handling */
_selectedSwitchBetweenMachineShortcutOptionsIndex = Array.IndexOf(_switchBetweenMachineShortcutOptions, moduleSettings.Properties.HotKeySwitchMachine.Value);
_easyMouseOptionIndex = (EasyMouseOption)moduleSettings.Properties.EasyMouse.Value;
LoadMachineMatrixString();
}
// Loads the machine matrix, taking into account changes to the machine pool.
private void LoadMachineMatrixString()
{
List<string> loadMachineMatrixString = Settings.Properties.MachineMatrixString ?? new List<string>() { string.Empty, string.Empty, string.Empty, string.Empty };
if (loadMachineMatrixString.Count < 4)
{
// Current logic of MWB assumes there are always 4 slots. Any other configuration means data corruption here.
loadMachineMatrixString = new List<string>() { string.Empty, string.Empty, string.Empty, string.Empty };
}
bool editedTheMatrix = false; // keep track of changes to the matrix because of changes to the available machine pool.
if (!string.IsNullOrEmpty(Settings.Properties.MachinePool?.Value))
{
List<string> availableMachines = new List<string>();
// Format of this field is "NAME1:ID1,NAME2:ID2,..."
// Load the available machines
foreach (string availableMachineIdPair in Settings.Properties.MachinePool.Value.Split(","))
{
string availableMachineName = availableMachineIdPair.Split(':')[0];
availableMachines.Add(availableMachineName);
}
// Start by removing the machines from the matrix that are no longer available to pick.
for (int i = 0; i < loadMachineMatrixString.Count; i++)
{
if (!availableMachines.Contains(loadMachineMatrixString[i]))
{
editedTheMatrix = true;
loadMachineMatrixString[i] = string.Empty;
}
}
// If an available machine is not in the matrix already, fill it in the first available spot.
foreach (string availableMachineName in availableMachines)
{
if (!loadMachineMatrixString.Contains(availableMachineName))
{
int availableIndex = loadMachineMatrixString.FindIndex(name => string.IsNullOrEmpty(name));
if (availableIndex >= 0)
{
loadMachineMatrixString[availableIndex] = availableMachineName;
editedTheMatrix = true;
}
}
}
}
// Dragging while elevated crashes on WinUI3: https://github.com/microsoft/microsoft-ui-xaml/issues/7690
machineMatrixString = new IndexedObservableCollection<DeviceViewModel>(loadMachineMatrixString.Select(name => new DeviceViewModel { Name = name, CanDragDrop = !IsElevated }));
if (editedTheMatrix)
{
// Set the property directly to save the new matrix right away with the new available machines.
MachineMatrixString = machineMatrixString;
}
}
public bool CanBeEnabled
{
get => !_enabledStateIsGPOConfigured;
}
public bool CanToggleUseService
{
get
{
return IsEnabled && !(!IsElevated && !UseService);
}
}
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
if (_isEnabled != value)
{
_isEnabled = value;
GeneralSettingsConfig.Enabled.MouseWithoutBorders = value;
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(ShowInfobarRunAsAdminText));
OnPropertyChanged(nameof(ShowInfobarCannotDragDropAsAdmin));
OnPropertyChanged(nameof(ShowPolicyConfiguredInfoForBehaviorSettings));
OnPropertyChanged(nameof(ShowPolicyConfiguredInfoForName2IPSetting));
OnPropertyChanged(nameof(ShowPolicyConfiguredInfoForOriginalUiSetting));
OnPropertyChanged(nameof(Name2IpListPolicyIsConfigured));
Task.Run(async () =>
{
if (!value)
{
try
{
await SubmitShutdownRequestAsync();
}
catch (Exception ex)
{
Logger.LogError($"Failed to shutdown MWB via SettingsSync: {ex}");
}
}
_uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
{
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
NotifyPropertyChanged();
// Disable service mode if we're not elevated, because we cannot register service in that case
if (value == true && !IsElevated && UseService)
{
UseService = false;
}
});
});
}
}
}
public string SecurityKey
{
get => Settings.Properties.SecurityKey.Value;
set
{
if (value != Settings.Properties.SecurityKey.Value)
{
Settings.Properties.SecurityKey.Value = value;
NotifyPropertyChanged();
}
}
}
public bool WrapMouse
{
get
{
return Settings.Properties.WrapMouse;
}
set
{
if (Settings.Properties.WrapMouse != value)
{
Settings.Properties.WrapMouse = value;
NotifyPropertyChanged();
}
}
}
public bool MatrixOneRow
{
get
{
return Settings.Properties.MatrixOneRow;
}
set
{
if (Settings.Properties.MatrixOneRow != value)
{
Settings.Properties.MatrixOneRow = value;
NotifyPropertyChanged();
}
}
}
public bool ShareClipboard
{
get
{
if (_clipboardSharingEnabledGpoConfiguration == GpoRuleConfigured.Disabled)
{
return false;
}
return Settings.Properties.ShareClipboard;
}
set
{
if (!_clipboardSharingEnabledIsGPOConfigured && (Settings.Properties.ShareClipboard != value))
{
Settings.Properties.ShareClipboard = value;
NotifyPropertyChanged();
OnPropertyChanged(nameof(TransferFile));
OnPropertyChanged(nameof(CardForTransferFileSettingIsEnabled));
}
}
}
public bool CardForShareClipboardSettingIsEnabled => _clipboardSharingEnabledIsGPOConfigured == false;
public bool TransferFile
{
get
{
if (_fileTransferEnabledGpoConfiguration == GpoRuleConfigured.Disabled)
{
return false;
}
return Settings.Properties.TransferFile && Settings.Properties.ShareClipboard;
}
set
{
// If ShareClipboard is disabled the file transfer does not work and the setting is disabled. => Don't save toggle state.
// If FileTransferGpo is configured the file transfer does not work and the setting is disabled. => Don't save toggle state.
if (!ShareClipboard || _fileTransferEnabledIsGPOConfigured)
{
return;
}
if (Settings.Properties.TransferFile != value)
{
Settings.Properties.TransferFile = value;
NotifyPropertyChanged();
}
}
}
public bool CardForTransferFileSettingIsEnabled
{
get => ShareClipboard && !_fileTransferEnabledIsGPOConfigured;
}
public bool HideMouseAtScreenEdge
{
get
{
return Settings.Properties.HideMouseAtScreenEdge;
}
set
{
if (Settings.Properties.HideMouseAtScreenEdge != value)
{
Settings.Properties.HideMouseAtScreenEdge = value;
NotifyPropertyChanged();
}
}
}
public bool DrawMouseCursor
{
get
{
return Settings.Properties.DrawMouseCursor;
}
set
{
if (Settings.Properties.DrawMouseCursor != value)
{
Settings.Properties.DrawMouseCursor = value;
NotifyPropertyChanged();
}
}
}
public bool ValidateRemoteMachineIP
{
get
{
if (_validateRemoteIpGpoConfiguration == GpoRuleConfigured.Enabled)
{
return true;
}
else if (_validateRemoteIpGpoConfiguration == GpoRuleConfigured.Disabled)
{
return false;
}
return Settings.Properties.ValidateRemoteMachineIP;
}
set
{
if (!_validateRemoteIpIsGPOConfigured && (Settings.Properties.ValidateRemoteMachineIP != value))
{
Settings.Properties.ValidateRemoteMachineIP = value;
NotifyPropertyChanged();
}
}
}
public bool CardForValidateRemoteIpSettingIsEnabled => _validateRemoteIpIsGPOConfigured == false;
public string Name2IP
{
// Due to https://github.com/microsoft/microsoft-ui-xaml/issues/1826, we must
// add back \n chars on set and remove them on get for the widget
// to make its behavior consistent with the old UI and MWB internal code.
get
{
if (_disableUserDefinedIpMappingRulesGpoConfiguration == GpoRuleConfigured.Enabled)
{
return string.Empty;
}
return Settings.Properties.Name2IP.Value.Replace("\r\n", "\r");
}
set
{
if (_disableUserDefinedIpMappingRulesIsGPOConfigured)
{
return;
}
var newValue = value.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
if (Settings.Properties.Name2IP.Value != newValue)
{
Settings.Properties.Name2IP.Value = newValue;
NotifyPropertyChanged();
}
}
}
private string _easyMouseIgnoredFullscreenAppsString;
public string EasyMouseFullscreenSwitchBlockExcludedApps
{
// Convert the list of excluded apps retrieved from the settings
// to a single string that can be displayed in the bound textbox
get
{
if (_easyMouseIgnoredFullscreenAppsString == null)
{
var excludedApps = Settings.Properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value;
_easyMouseIgnoredFullscreenAppsString = excludedApps.Count == 0 ? string.Empty : string.Join('\r', excludedApps);
}
return _easyMouseIgnoredFullscreenAppsString;
}
set
{
if (EasyMouseFullscreenSwitchBlockExcludedApps == value)
{
return;
}
_easyMouseIgnoredFullscreenAppsString = value;
var ignoredAppsSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (value != string.Empty)
{
ignoredAppsSet.UnionWith(value.Split('\r', StringSplitOptions.RemoveEmptyEntries));
}
Settings.Properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value = ignoredAppsSet;
NotifyPropertyChanged();
}
}
public bool CardForName2IpSettingIsEnabled => _disableUserDefinedIpMappingRulesIsGPOConfigured == false;
public bool ShowPolicyConfiguredInfoForName2IPSetting => _disableUserDefinedIpMappingRulesIsGPOConfigured && IsEnabled;
public string Name2IpListPolicyData
{
// Due to https://github.com/microsoft/microsoft-ui-xaml/issues/1826, we must
// add back \n chars on set and remove them on get for the widget
// to make its behavior consistent with the old UI and MWB internal code.
// get => GPOWrapper.GetConfiguredMwbPolicyDefinedIpMappingRules().Replace("\r\n", "\r");
get => _policyDefinedIpMappingRulesGPOData.Replace("\r\n", "\r");
}
public bool Name2IpListPolicyIsConfigured => _policyDefinedIpMappingRulesIsGPOConfigured && IsEnabled;
public bool SameSubnetOnly
{
get
{
if (_sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Enabled)
{
return true;
}
else if (_sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Disabled)
{
return false;
}
return Settings.Properties.SameSubnetOnly;
}
set
{
if (!_sameSubnetOnlyIsGPOConfigured && (Settings.Properties.SameSubnetOnly != value))
{
Settings.Properties.SameSubnetOnly = value;
NotifyPropertyChanged();
}
}
}
public bool CardForSameSubnetOnlySettingIsEnabled => _sameSubnetOnlyIsGPOConfigured == false;
public bool BlockScreenSaverOnOtherMachines
{
get
{
if (_disallowBlockingScreensaverGpoConfiguration == GpoRuleConfigured.Enabled)
{
return false;
}
return Settings.Properties.BlockScreenSaverOnOtherMachines;
}
set
{
if (_disallowBlockingScreensaverIsGPOConfigured)
{
return;
}
if (Settings.Properties.BlockScreenSaverOnOtherMachines != value)
{
Settings.Properties.BlockScreenSaverOnOtherMachines = value;
NotifyPropertyChanged();
}
}
}
public bool CardForBlockScreensaverSettingIsEnabled => _disallowBlockingScreensaverIsGPOConfigured == false;
// Should match EasyMouseOption enum from MouseWithoutBorders and the ComboBox in the MouseWithoutBordersView.cs
private enum EasyMouseOption
{
Disable = 0,
Enable = 1,
Ctrl = 2,
Shift = 3,
}
private EasyMouseOption _easyMouseOptionIndex;
public int EasyMouseOptionIndex
{
get
{
return (int)_easyMouseOptionIndex;
}
set
{
if (value != (int)_easyMouseOptionIndex)
{
_easyMouseOptionIndex = (EasyMouseOption)value;
Settings.Properties.EasyMouse.Value = value;
NotifyPropertyChanged(nameof(EasyMouseOptionIndex));
}
}
}
public bool EasyMouseEnabled => (EasyMouseOption)EasyMouseOptionIndex != EasyMouseOption.Disable;
public bool IsEasyMouseBlockingOnFullscreenEnabled =>
EasyMouseEnabled && DisableEasyMouseWhenForegroundWindowIsFullscreen;
public bool DisableEasyMouseWhenForegroundWindowIsFullscreen
{
get
{
return Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen;
}
set
{
if (Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen == value)
{
return;
}
Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen = value;
NotifyPropertyChanged();
}
}
public HotkeySettings ToggleEasyMouseShortcut
{
get => Settings.Properties.ToggleEasyMouseShortcut;
set
{
if (Settings.Properties.ToggleEasyMouseShortcut != value)
{
Settings.Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse;
NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
}
}
}
public HotkeySettings LockMachinesShortcut
{
get => Settings.Properties.LockMachineShortcut;
set
{
if (Settings.Properties.LockMachineShortcut != value)
{
Settings.Properties.LockMachineShortcut = value;
Settings.Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine;
NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
}
}
}
public HotkeySettings ReconnectShortcut
{
get => Settings.Properties.ReconnectShortcut;
set
{
if (Settings.Properties.ReconnectShortcut != value)
{
Settings.Properties.ReconnectShortcut = value;
Settings.Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect;
NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
}
}
}
public HotkeySettings HotKeySwitch2AllPC
{
get => Settings.Properties.Switch2AllPCShortcut;
set
{
if (Settings.Properties.Switch2AllPCShortcut != value)
{
Settings.Properties.Switch2AllPCShortcut = value;
Settings.Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC;
NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
}
}
}
private int _selectedSwitchBetweenMachineShortcutOptionsIndex;
public int SelectedSwitchBetweenMachineShortcutOptionsIndex
{
get
{
return _selectedSwitchBetweenMachineShortcutOptionsIndex;
}
set
{
if (_selectedSwitchBetweenMachineShortcutOptionsIndex != value)
{
_selectedSwitchBetweenMachineShortcutOptionsIndex = value;
Settings.Properties.HotKeySwitchMachine.Value = _switchBetweenMachineShortcutOptions[value];
NotifyPropertyChanged();
}
}
}
public bool MoveMouseRelatively
{
get
{
return Settings.Properties.MoveMouseRelatively;
}
set
{
if (Settings.Properties.MoveMouseRelatively != value)
{
Settings.Properties.MoveMouseRelatively = value;
NotifyPropertyChanged();
}
}
}
public bool BlockMouseAtScreenCorners
{
get
{
return Settings.Properties.BlockMouseAtScreenCorners;
}
set
{
if (Settings.Properties.BlockMouseAtScreenCorners != value)
{
Settings.Properties.BlockMouseAtScreenCorners = value;
NotifyPropertyChanged();
}
}
}
private IndexedObservableCollection<DeviceViewModel> machineMatrixString;
public partial class DeviceViewModel : Observable
{
public string Name { get; set; }
public bool CanDragDrop { get; set; }
private Brush _statusBrush = StatusColors[SocketStatus.NA];
public Brush StatusBrush
{
get
{
return _statusBrush;
}
set
{
if (_statusBrush != value)
{
_statusBrush = value;
OnPropertyChanged(nameof(StatusBrush));
}
}
}
}
public IndexedObservableCollection<DeviceViewModel> MachineMatrixString
{
get
{
lock (_machineMatrixStringLock)
{
return machineMatrixString;
}
}
set
{
lock (_machineMatrixStringLock)
{
machineMatrixString = value;
}
Settings.Properties.MachineMatrixString = new List<string>(value.ToEnumerable().Select(d => d.Name));
NotifyPropertyChanged();
}
}
public bool ShowClipboardAndNetworkStatusMessages
{
get
{
return Settings.Properties.ShowClipboardAndNetworkStatusMessages;
}
set
{
if (Settings.Properties.ShowClipboardAndNetworkStatusMessages != value)
{
Settings.Properties.ShowClipboardAndNetworkStatusMessages = value;
NotifyPropertyChanged();
}
}
}
public bool LoadUpdatedSettings()
{
try
{
LoadViewModelFromSettings(SettingsUtils.GetSettings<MouseWithoutBordersSettings>("MouseWithoutBorders"));
return true;
}
catch (System.Exception ex)
{
Logger.LogError(ex.Message);
return false;
}
}
private void SendCustomAction(string actionName)
{
SendConfigMSG("{\"action\":{\"MouseWithoutBorders\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
}
public void AddFirewallRule()
{
SendCustomAction("add_firewall");
}
public void RefreshEnabledState()
{
InitializeEnabledValue();
OnPropertyChanged(nameof(IsEnabled));
}
private void NotifyModuleUpdatedSettings()
{
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
MouseWithoutBordersSettings.ModuleName,
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.MouseWithoutBordersSettings)));
}
public void NotifyUpdatedSettings()
{
OnPropertyChanged(null); // Notify all properties might have changed.
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
// Skip saving settings for UI properties
if (propertyName == nameof(ShowInfobarCannotDragDropAsAdmin) ||
propertyName == nameof(ShowInfobarRunAsAdminText))
{
return;
}
SettingsUtils.SaveSettings(Settings.ToJsonString(), MouseWithoutBordersSettings.ModuleName);
if (propertyName == nameof(UseService))
{
NotifyModuleUpdatedSettings();
}
}
private Func<string, int> SendConfigMSG { get; }
public void CopyMachineNameToClipboard()
{
var data = new DataPackage();
data.SetText(Dns.GetHostName());
Clipboard.SetContent(data);
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Cancel the cancellation token source
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
// Wait for the machine polling task to complete
try
{
_machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1));
}
catch (AggregateException)
{
// Task was cancelled, which is expected
}
// Dispose the named pipe stream
try
{
syncHelperStream?.Dispose();
}
catch (Exception ex)
{
Logger.LogError($"Error disposing sync helper stream: {ex}");
}
finally
{
syncHelperStream = null;
}
// Dispose the semaphore
_ipcSemaphore?.Dispose();
}
_disposed = true;
}
base.Dispose(disposing);
}
internal void UninstallService()
{
SendCustomAction("uninstall_service");
}
public bool ShowPolicyConfiguredInfoForServiceSettings
{
get
{
return IsEnabled && _allowServiceModeIsGPOConfigured;
}
}
public bool ShowPolicyConfiguredInfoForBehaviorSettings
{
get
{
return IsEnabled && (_disallowBlockingScreensaverIsGPOConfigured
|| _clipboardSharingEnabledIsGPOConfigured || _fileTransferEnabledIsGPOConfigured
|| _sameSubnetOnlyIsGPOConfigured || _validateRemoteIpIsGPOConfigured);
}
}
public bool ShowInfobarCannotDragDropAsAdmin
{
get { return IsElevated && IsEnabled; }
}
public bool ShowInfobarRunAsAdminText
{
get { return !CanToggleUseService && IsEnabled && !ShowPolicyConfiguredInfoForServiceSettings; }
}
}
}