MouseWithoutBordersViewModel.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Globalization; 8 using System.IO; 9 using System.IO.Pipes; 10 using System.Linq; 11 using System.Net; 12 using System.Runtime.CompilerServices; 13 using System.Text.Json; 14 using System.Threading; 15 using System.Threading.Tasks; 16 using global::PowerToys.GPOWrapper; 17 using ManagedCommon; 18 using Microsoft.PowerToys.Settings.UI.Helpers; 19 using Microsoft.PowerToys.Settings.UI.Library; 20 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 21 using Microsoft.PowerToys.Settings.UI.Library.Interfaces; 22 using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; 23 using Microsoft.PowerToys.Settings.UI.SerializationContext; 24 using Microsoft.UI; 25 using Microsoft.UI.Dispatching; 26 using Microsoft.UI.Xaml.Media; 27 using StreamJsonRpc; 28 using Windows.ApplicationModel.DataTransfer; 29 30 namespace Microsoft.PowerToys.Settings.UI.ViewModels 31 { 32 public partial class MouseWithoutBordersViewModel : PageViewModelBase 33 { 34 protected override string ModuleName => MouseWithoutBordersSettings.ModuleName; 35 36 // These should be in the same order as the ComboBoxItems in MouseWithoutBordersPage.xaml switch machine shortcut options 37 private readonly int[] _switchBetweenMachineShortcutOptions = 38 { 39 112, 40 49, 41 0, 42 }; 43 44 private readonly Lock _machineMatrixStringLock = new(); 45 46 private bool _disposed; 47 48 private static readonly Dictionary<SocketStatus, Brush> StatusColors = new Dictionary<SocketStatus, Brush>() 49 { 50 { SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) }, 51 { SocketStatus.Resolving, new SolidColorBrush(Colors.Yellow) }, 52 { SocketStatus.Connecting, new SolidColorBrush(Colors.Orange) }, 53 { SocketStatus.Handshaking, new SolidColorBrush(Colors.Blue) }, 54 { SocketStatus.Error, new SolidColorBrush(Colors.Red) }, 55 { SocketStatus.ForceClosed, new SolidColorBrush(Colors.Purple) }, 56 { SocketStatus.InvalidKey, new SolidColorBrush(Colors.Brown) }, 57 { SocketStatus.Timeout, new SolidColorBrush(Colors.Pink) }, 58 { SocketStatus.SendError, new SolidColorBrush(Colors.Maroon) }, 59 { SocketStatus.Connected, new SolidColorBrush(Colors.Green) }, 60 }; 61 62 private bool _connectFieldsVisible; 63 64 public bool IsElevated { get => GeneralSettingsConfig.IsElevated; } 65 66 public bool CanUninstallService { get => GeneralSettingsConfig.IsElevated && !UseService; } 67 68 public ButtonClickCommand AddFirewallRuleEventHandler => new ButtonClickCommand(AddFirewallRule); 69 70 public ButtonClickCommand UninstallServiceEventHandler => new ButtonClickCommand(UninstallService); 71 72 public bool ShowOriginalUI 73 { 74 get 75 { 76 if (_useOriginalUserInterfaceGpoConfiguration == GpoRuleConfigured.Disabled) 77 { 78 return false; 79 } 80 81 return Settings.Properties.ShowOriginalUI; 82 } 83 84 set 85 { 86 if (!_useOriginalUserInterfaceIsGPOConfigured && (Settings.Properties.ShowOriginalUI != value)) 87 { 88 Settings.Properties.ShowOriginalUI = value; 89 NotifyPropertyChanged(nameof(ShowOriginalUI)); 90 } 91 } 92 } 93 94 public bool CardForOriginalUiSettingIsEnabled => _useOriginalUserInterfaceIsGPOConfigured == false; 95 96 public bool ShowPolicyConfiguredInfoForOriginalUiSetting => IsEnabled && _useOriginalUserInterfaceIsGPOConfigured; 97 98 public bool UseService 99 { 100 get 101 { 102 if (_allowServiceModeGpoConfiguration == GpoRuleConfigured.Disabled) 103 { 104 return false; 105 } 106 107 return Settings.Properties.UseService; 108 } 109 110 set 111 { 112 if (_allowServiceModeIsGPOConfigured) 113 { 114 return; 115 } 116 117 var valueChanged = Settings.Properties.UseService != value; 118 119 // Set the UI property itself instantly 120 if (valueChanged) 121 { 122 Settings.Properties.UseService = value; 123 OnPropertyChanged(nameof(UseService)); 124 OnPropertyChanged(nameof(CanUninstallService)); 125 OnPropertyChanged(nameof(ShowInfobarRunAsAdminText)); 126 127 // Must block here until the process exits 128 Task.Run(async () => 129 { 130 await SubmitShutdownRequestAsync(); 131 132 _uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => 133 { 134 Settings.Properties.UseService = value; 135 NotifyPropertyChanged(nameof(UseService)); 136 }); 137 }); 138 } 139 } 140 } 141 142 public bool UseServiceSettingIsEnabled => _allowServiceModeIsGPOConfigured == false; 143 144 public bool ConnectFieldsVisible 145 { 146 get => _connectFieldsVisible; 147 148 set 149 { 150 if (_connectFieldsVisible != value) 151 { 152 _connectFieldsVisible = value; 153 OnPropertyChanged(nameof(ConnectFieldsVisible)); 154 } 155 } 156 } 157 158 private string _connectSecurityKey; 159 160 public string ConnectSecurityKey 161 { 162 get => _connectSecurityKey; 163 164 set 165 { 166 if (_connectSecurityKey != value) 167 { 168 _connectSecurityKey = value; 169 OnPropertyChanged(nameof(ConnectSecurityKey)); 170 } 171 } 172 } 173 174 private string _connectPCName; 175 176 public string ConnectPCName 177 { 178 get => _connectPCName; 179 180 set 181 { 182 if (_connectPCName != value) 183 { 184 _connectPCName = value; 185 OnPropertyChanged(nameof(ConnectPCName)); 186 } 187 } 188 } 189 190 private SettingsUtils SettingsUtils { get; set; } 191 192 private GeneralSettings GeneralSettingsConfig { get; set; } 193 194 private GpoRuleConfigured _enabledGpoRuleConfiguration; 195 private bool _enabledStateIsGPOConfigured; 196 private bool _isEnabled; 197 198 // Configuration policy variables 199 private GpoRuleConfigured _clipboardSharingEnabledGpoConfiguration; 200 private bool _clipboardSharingEnabledIsGPOConfigured; 201 private GpoRuleConfigured _fileTransferEnabledGpoConfiguration; 202 private bool _fileTransferEnabledIsGPOConfigured; 203 private GpoRuleConfigured _useOriginalUserInterfaceGpoConfiguration; 204 private bool _useOriginalUserInterfaceIsGPOConfigured; 205 private GpoRuleConfigured _disallowBlockingScreensaverGpoConfiguration; 206 private bool _disallowBlockingScreensaverIsGPOConfigured; 207 private GpoRuleConfigured _allowServiceModeGpoConfiguration; 208 private bool _allowServiceModeIsGPOConfigured; 209 private GpoRuleConfigured _sameSubnetOnlyGpoConfiguration; 210 private bool _sameSubnetOnlyIsGPOConfigured; 211 private GpoRuleConfigured _validateRemoteIpGpoConfiguration; 212 private bool _validateRemoteIpIsGPOConfigured; 213 private GpoRuleConfigured _disableUserDefinedIpMappingRulesGpoConfiguration; 214 private bool _disableUserDefinedIpMappingRulesIsGPOConfigured; 215 private string _policyDefinedIpMappingRulesGPOData; 216 private bool _policyDefinedIpMappingRulesIsGPOConfigured; 217 218 public string MachineHostName 219 { 220 get 221 { 222 try 223 { 224 return Dns.GetHostName(); 225 } 226 catch 227 { 228 return string.Empty; 229 } 230 } 231 } 232 233 public bool IsEnabledGpoConfigured 234 { 235 get => _enabledStateIsGPOConfigured; 236 } 237 238 private enum SocketStatus : int 239 { 240 NA = 0, 241 Resolving = 1, 242 Connecting = 2, 243 Handshaking = 3, 244 Error = 4, 245 ForceClosed = 5, 246 InvalidKey = 6, 247 Timeout = 7, 248 SendError = 8, 249 Connected = 9, 250 } 251 252 private interface ISettingsSyncHelper 253 { 254 [Newtonsoft.Json.JsonObject(Newtonsoft.Json.MemberSerialization.OptIn)] 255 public struct MachineSocketState 256 { 257 // Disable false-positive warning due to IPC 258 #pragma warning disable CS0649 259 [Newtonsoft.Json.JsonProperty] 260 public string Name; 261 262 [Newtonsoft.Json.JsonProperty] 263 public SocketStatus Status; 264 #pragma warning restore CS0649 265 } 266 267 void Shutdown(); 268 269 void Reconnect(); 270 271 void GenerateNewKey(); 272 273 void ConnectToMachine(string machineName, string securityKey); 274 275 Task<MachineSocketState[]> RequestMachineSocketStateAsync(); 276 } 277 278 private static CancellationTokenSource _cancellationTokenSource; 279 280 private static Task _machinePollingThreadTask; 281 282 private static VisualStudio.Threading.AsyncSemaphore _ipcSemaphore = new VisualStudio.Threading.AsyncSemaphore(1); 283 284 private sealed partial class SyncHelper : IDisposable 285 { 286 public SyncHelper(NamedPipeClientStream stream) 287 { 288 Stream = stream; 289 Endpoint = JsonRpc.Attach<ISettingsSyncHelper>(Stream); 290 } 291 292 public NamedPipeClientStream Stream { get; } 293 294 public ISettingsSyncHelper Endpoint { get; private set; } 295 296 public void Dispose() 297 { 298 ((IDisposable)Endpoint).Dispose(); 299 } 300 } 301 302 private static NamedPipeClientStream syncHelperStream; 303 304 private async Task<SyncHelper> GetSettingsSyncHelperAsync() 305 { 306 try 307 { 308 var recreateStream = false; 309 if (syncHelperStream == null) 310 { 311 recreateStream = true; 312 } 313 else 314 { 315 if (!syncHelperStream.IsConnected || !syncHelperStream.CanWrite) 316 { 317 await syncHelperStream.DisposeAsync(); 318 recreateStream = true; 319 } 320 } 321 322 if (recreateStream) 323 { 324 syncHelperStream = new NamedPipeClientStream(".", "MouseWithoutBorders/SettingsSync", PipeDirection.InOut, PipeOptions.Asynchronous); 325 await syncHelperStream.ConnectAsync(10000); 326 } 327 328 return new SyncHelper(syncHelperStream); 329 } 330 catch (Exception ex) 331 { 332 if (IsEnabled) 333 { 334 Logger.LogError($"Couldn't create SettingsSync: {ex}"); 335 } 336 337 return null; 338 } 339 } 340 341 public async Task SubmitShutdownRequestAsync() 342 { 343 using (await _ipcSemaphore.EnterAsync()) 344 { 345 using (var syncHelper = await GetSettingsSyncHelperAsync()) 346 { 347 syncHelper?.Endpoint?.Shutdown(); 348 var task = syncHelper?.Stream.FlushAsync(); 349 if (task != null) 350 { 351 await task; 352 } 353 } 354 } 355 } 356 357 public async Task SubmitReconnectRequestAsync() 358 { 359 using (await _ipcSemaphore.EnterAsync()) 360 { 361 using (var syncHelper = await GetSettingsSyncHelperAsync()) 362 { 363 syncHelper?.Endpoint?.Reconnect(); 364 var task = syncHelper?.Stream.FlushAsync(); 365 if (task != null) 366 { 367 await task; 368 } 369 } 370 } 371 } 372 373 public async Task SubmitNewKeyRequestAsync() 374 { 375 using (await _ipcSemaphore.EnterAsync()) 376 { 377 using (var syncHelper = await GetSettingsSyncHelperAsync()) 378 { 379 syncHelper?.Endpoint?.GenerateNewKey(); 380 var task = syncHelper?.Stream.FlushAsync(); 381 if (task != null) 382 { 383 await task; 384 } 385 } 386 } 387 } 388 389 public async Task SubmitConnectionRequestAsync(string pcName, string securityKey) 390 { 391 using (await _ipcSemaphore.EnterAsync()) 392 { 393 using (var syncHelper = await GetSettingsSyncHelperAsync()) 394 { 395 syncHelper?.Endpoint?.ConnectToMachine(pcName, securityKey); 396 var task = syncHelper?.Stream.FlushAsync(); 397 if (task != null) 398 { 399 await task; 400 } 401 } 402 } 403 } 404 405 private async Task<ISettingsSyncHelper.MachineSocketState[]> PollMachineSocketStateAsync() 406 { 407 using (await _ipcSemaphore.EnterAsync()) 408 { 409 using (var syncHelper = await GetSettingsSyncHelperAsync()) 410 { 411 var task = syncHelper?.Endpoint?.RequestMachineSocketStateAsync(); 412 if (task != null) 413 { 414 return await task; 415 } 416 else 417 { 418 return null; 419 } 420 } 421 } 422 } 423 424 private MouseWithoutBordersSettings Settings { get; set; } 425 426 private DispatcherQueue _uiDispatcherQueue; 427 428 public MouseWithoutBordersViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, DispatcherQueue uiDispatcherQueue) 429 { 430 SettingsUtils = settingsUtils; 431 432 _uiDispatcherQueue = uiDispatcherQueue; 433 434 // To obtain the general settings configurations of PowerToys Settings. 435 ArgumentNullException.ThrowIfNull(settingsRepository); 436 437 GeneralSettingsConfig = settingsRepository.SettingsConfig; 438 439 InitializeEnabledValue(); 440 InitializePolicyValues(); 441 442 // 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. 443 MouseWithoutBordersSettings moduleSettings; 444 moduleSettings = SettingsUtils.GetSettingsOrDefault<MouseWithoutBordersSettings>("MouseWithoutBorders"); 445 446 LoadViewModelFromSettings(moduleSettings); 447 448 // set the callback functions value to handle outgoing IPC message. 449 SendConfigMSG = ipcMSGCallBackFunc; 450 451 _cancellationTokenSource?.Cancel(); 452 453 _cancellationTokenSource = new CancellationTokenSource(); 454 455 _machinePollingThreadTask = StartMachineStatusPollingThread(_machinePollingThreadTask, _cancellationTokenSource.Token); 456 } 457 458 private Task StartMachineStatusPollingThread(Task previousThreadTask, CancellationToken token) 459 { 460 return Task.Run( 461 async () => 462 { 463 previousThreadTask?.Wait(); 464 465 while (!token.IsCancellationRequested) 466 { 467 Dictionary<string, ISettingsSyncHelper.MachineSocketState> states = null; 468 try 469 { 470 states = (await PollMachineSocketStateAsync())?.ToDictionary(s => s.Name, StringComparer.OrdinalIgnoreCase); 471 } 472 catch (Exception ex) 473 { 474 Logger.LogInfo($"Poll ISettingsSyncHelper.MachineSocketState error: {ex}"); 475 continue; 476 } 477 478 if (states != null) 479 { 480 lock (_machineMatrixStringLock) 481 { 482 foreach (var machine in machineMatrixString) 483 { 484 if (states.TryGetValue(machine.Item.Name, out var state)) 485 { 486 _uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => 487 { 488 try 489 { 490 machine.Item.StatusBrush = StatusColors[state.Status]; 491 } 492 catch (Exception) 493 { 494 } 495 }); 496 } 497 } 498 } 499 } 500 501 Thread.Sleep(500); 502 } 503 }, 504 _cancellationTokenSource.Token); 505 } 506 507 private void InitializeEnabledValue() 508 { 509 _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredMouseWithoutBordersEnabledValue(); 510 if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) 511 { 512 // Get the enabled state from GPO. 513 _enabledStateIsGPOConfigured = true; 514 _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; 515 } 516 else 517 { 518 _isEnabled = GeneralSettingsConfig.Enabled.MouseWithoutBorders; 519 } 520 } 521 522 private void InitializePolicyValues() 523 { 524 // Policies supporting only enabled state 525 _disallowBlockingScreensaverGpoConfiguration = GPOWrapper.GetConfiguredMwbDisallowBlockingScreensaverValue(); 526 _disallowBlockingScreensaverIsGPOConfigured = _disallowBlockingScreensaverGpoConfiguration == GpoRuleConfigured.Enabled; 527 _disableUserDefinedIpMappingRulesGpoConfiguration = GPOWrapper.GetConfiguredMwbDisableUserDefinedIpMappingRulesValue(); 528 _disableUserDefinedIpMappingRulesIsGPOConfigured = _disableUserDefinedIpMappingRulesGpoConfiguration == GpoRuleConfigured.Enabled; 529 530 // Policies supporting only disabled state 531 _allowServiceModeGpoConfiguration = GPOWrapper.GetConfiguredMwbAllowServiceModeValue(); 532 _allowServiceModeIsGPOConfigured = _allowServiceModeGpoConfiguration == GpoRuleConfigured.Disabled; 533 _clipboardSharingEnabledGpoConfiguration = GPOWrapper.GetConfiguredMwbClipboardSharingEnabledValue(); 534 _clipboardSharingEnabledIsGPOConfigured = _clipboardSharingEnabledGpoConfiguration == GpoRuleConfigured.Disabled; 535 _fileTransferEnabledGpoConfiguration = GPOWrapper.GetConfiguredMwbFileTransferEnabledValue(); 536 _fileTransferEnabledIsGPOConfigured = _fileTransferEnabledGpoConfiguration == GpoRuleConfigured.Disabled; 537 _useOriginalUserInterfaceGpoConfiguration = GPOWrapper.GetConfiguredMwbUseOriginalUserInterfaceValue(); 538 _useOriginalUserInterfaceIsGPOConfigured = _useOriginalUserInterfaceGpoConfiguration == GpoRuleConfigured.Disabled; 539 540 // Policies supporting enabled and disabled state 541 _sameSubnetOnlyGpoConfiguration = GPOWrapper.GetConfiguredMwbSameSubnetOnlyValue(); 542 _sameSubnetOnlyIsGPOConfigured = _sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Enabled || _sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Disabled; 543 _validateRemoteIpGpoConfiguration = GPOWrapper.GetConfiguredMwbValidateRemoteIpValue(); 544 _validateRemoteIpIsGPOConfigured = _validateRemoteIpGpoConfiguration == GpoRuleConfigured.Enabled || _validateRemoteIpGpoConfiguration == GpoRuleConfigured.Disabled; 545 546 // Special policies 547 _policyDefinedIpMappingRulesGPOData = GPOWrapper.GetConfiguredMwbPolicyDefinedIpMappingRules(); 548 _policyDefinedIpMappingRulesIsGPOConfigured = !string.IsNullOrWhiteSpace(_policyDefinedIpMappingRulesGPOData); 549 } 550 551 public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings() 552 { 553 var hotkeysDict = new Dictionary<string, HotkeySettings[]> 554 { 555 [ModuleName] = [ 556 ToggleEasyMouseShortcut, 557 LockMachinesShortcut, 558 HotKeySwitch2AllPC, 559 ReconnectShortcut], 560 }; 561 562 return hotkeysDict; 563 } 564 565 private void LoadViewModelFromSettings(MouseWithoutBordersSettings moduleSettings) 566 { 567 ArgumentNullException.ThrowIfNull(moduleSettings); 568 569 Settings = moduleSettings; 570 /* TODO: Error handling */ 571 _selectedSwitchBetweenMachineShortcutOptionsIndex = Array.IndexOf(_switchBetweenMachineShortcutOptions, moduleSettings.Properties.HotKeySwitchMachine.Value); 572 _easyMouseOptionIndex = (EasyMouseOption)moduleSettings.Properties.EasyMouse.Value; 573 574 LoadMachineMatrixString(); 575 } 576 577 // Loads the machine matrix, taking into account changes to the machine pool. 578 private void LoadMachineMatrixString() 579 { 580 List<string> loadMachineMatrixString = Settings.Properties.MachineMatrixString ?? new List<string>() { string.Empty, string.Empty, string.Empty, string.Empty }; 581 582 if (loadMachineMatrixString.Count < 4) 583 { 584 // Current logic of MWB assumes there are always 4 slots. Any other configuration means data corruption here. 585 loadMachineMatrixString = new List<string>() { string.Empty, string.Empty, string.Empty, string.Empty }; 586 } 587 588 bool editedTheMatrix = false; // keep track of changes to the matrix because of changes to the available machine pool. 589 590 if (!string.IsNullOrEmpty(Settings.Properties.MachinePool?.Value)) 591 { 592 List<string> availableMachines = new List<string>(); 593 594 // Format of this field is "NAME1:ID1,NAME2:ID2,..." 595 // Load the available machines 596 foreach (string availableMachineIdPair in Settings.Properties.MachinePool.Value.Split(",")) 597 { 598 string availableMachineName = availableMachineIdPair.Split(':')[0]; 599 availableMachines.Add(availableMachineName); 600 } 601 602 // Start by removing the machines from the matrix that are no longer available to pick. 603 for (int i = 0; i < loadMachineMatrixString.Count; i++) 604 { 605 if (!availableMachines.Contains(loadMachineMatrixString[i])) 606 { 607 editedTheMatrix = true; 608 loadMachineMatrixString[i] = string.Empty; 609 } 610 } 611 612 // If an available machine is not in the matrix already, fill it in the first available spot. 613 foreach (string availableMachineName in availableMachines) 614 { 615 if (!loadMachineMatrixString.Contains(availableMachineName)) 616 { 617 int availableIndex = loadMachineMatrixString.FindIndex(name => string.IsNullOrEmpty(name)); 618 if (availableIndex >= 0) 619 { 620 loadMachineMatrixString[availableIndex] = availableMachineName; 621 editedTheMatrix = true; 622 } 623 } 624 } 625 } 626 627 // Dragging while elevated crashes on WinUI3: https://github.com/microsoft/microsoft-ui-xaml/issues/7690 628 machineMatrixString = new IndexedObservableCollection<DeviceViewModel>(loadMachineMatrixString.Select(name => new DeviceViewModel { Name = name, CanDragDrop = !IsElevated })); 629 630 if (editedTheMatrix) 631 { 632 // Set the property directly to save the new matrix right away with the new available machines. 633 MachineMatrixString = machineMatrixString; 634 } 635 } 636 637 public bool CanBeEnabled 638 { 639 get => !_enabledStateIsGPOConfigured; 640 } 641 642 public bool CanToggleUseService 643 { 644 get 645 { 646 return IsEnabled && !(!IsElevated && !UseService); 647 } 648 } 649 650 public bool IsEnabled 651 { 652 get => _isEnabled; 653 set 654 { 655 if (_enabledStateIsGPOConfigured) 656 { 657 // If it's GPO configured, shouldn't be able to change this state. 658 return; 659 } 660 661 if (_isEnabled != value) 662 { 663 _isEnabled = value; 664 GeneralSettingsConfig.Enabled.MouseWithoutBorders = value; 665 OnPropertyChanged(nameof(IsEnabled)); 666 OnPropertyChanged(nameof(ShowInfobarRunAsAdminText)); 667 OnPropertyChanged(nameof(ShowInfobarCannotDragDropAsAdmin)); 668 OnPropertyChanged(nameof(ShowPolicyConfiguredInfoForBehaviorSettings)); 669 OnPropertyChanged(nameof(ShowPolicyConfiguredInfoForName2IPSetting)); 670 OnPropertyChanged(nameof(ShowPolicyConfiguredInfoForOriginalUiSetting)); 671 OnPropertyChanged(nameof(Name2IpListPolicyIsConfigured)); 672 673 Task.Run(async () => 674 { 675 if (!value) 676 { 677 try 678 { 679 await SubmitShutdownRequestAsync(); 680 } 681 catch (Exception ex) 682 { 683 Logger.LogError($"Failed to shutdown MWB via SettingsSync: {ex}"); 684 } 685 } 686 687 _uiDispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => 688 { 689 OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); 690 SendConfigMSG(outgoing.ToString()); 691 692 NotifyPropertyChanged(); 693 694 // Disable service mode if we're not elevated, because we cannot register service in that case 695 if (value == true && !IsElevated && UseService) 696 { 697 UseService = false; 698 } 699 }); 700 }); 701 } 702 } 703 } 704 705 public string SecurityKey 706 { 707 get => Settings.Properties.SecurityKey.Value; 708 709 set 710 { 711 if (value != Settings.Properties.SecurityKey.Value) 712 { 713 Settings.Properties.SecurityKey.Value = value; 714 NotifyPropertyChanged(); 715 } 716 } 717 } 718 719 public bool WrapMouse 720 { 721 get 722 { 723 return Settings.Properties.WrapMouse; 724 } 725 726 set 727 { 728 if (Settings.Properties.WrapMouse != value) 729 { 730 Settings.Properties.WrapMouse = value; 731 NotifyPropertyChanged(); 732 } 733 } 734 } 735 736 public bool MatrixOneRow 737 { 738 get 739 { 740 return Settings.Properties.MatrixOneRow; 741 } 742 743 set 744 { 745 if (Settings.Properties.MatrixOneRow != value) 746 { 747 Settings.Properties.MatrixOneRow = value; 748 NotifyPropertyChanged(); 749 } 750 } 751 } 752 753 public bool ShareClipboard 754 { 755 get 756 { 757 if (_clipboardSharingEnabledGpoConfiguration == GpoRuleConfigured.Disabled) 758 { 759 return false; 760 } 761 762 return Settings.Properties.ShareClipboard; 763 } 764 765 set 766 { 767 if (!_clipboardSharingEnabledIsGPOConfigured && (Settings.Properties.ShareClipboard != value)) 768 { 769 Settings.Properties.ShareClipboard = value; 770 NotifyPropertyChanged(); 771 OnPropertyChanged(nameof(TransferFile)); 772 OnPropertyChanged(nameof(CardForTransferFileSettingIsEnabled)); 773 } 774 } 775 } 776 777 public bool CardForShareClipboardSettingIsEnabled => _clipboardSharingEnabledIsGPOConfigured == false; 778 779 public bool TransferFile 780 { 781 get 782 { 783 if (_fileTransferEnabledGpoConfiguration == GpoRuleConfigured.Disabled) 784 { 785 return false; 786 } 787 788 return Settings.Properties.TransferFile && Settings.Properties.ShareClipboard; 789 } 790 791 set 792 { 793 // If ShareClipboard is disabled the file transfer does not work and the setting is disabled. => Don't save toggle state. 794 // If FileTransferGpo is configured the file transfer does not work and the setting is disabled. => Don't save toggle state. 795 if (!ShareClipboard || _fileTransferEnabledIsGPOConfigured) 796 { 797 return; 798 } 799 800 if (Settings.Properties.TransferFile != value) 801 { 802 Settings.Properties.TransferFile = value; 803 NotifyPropertyChanged(); 804 } 805 } 806 } 807 808 public bool CardForTransferFileSettingIsEnabled 809 { 810 get => ShareClipboard && !_fileTransferEnabledIsGPOConfigured; 811 } 812 813 public bool HideMouseAtScreenEdge 814 { 815 get 816 { 817 return Settings.Properties.HideMouseAtScreenEdge; 818 } 819 820 set 821 { 822 if (Settings.Properties.HideMouseAtScreenEdge != value) 823 { 824 Settings.Properties.HideMouseAtScreenEdge = value; 825 NotifyPropertyChanged(); 826 } 827 } 828 } 829 830 public bool DrawMouseCursor 831 { 832 get 833 { 834 return Settings.Properties.DrawMouseCursor; 835 } 836 837 set 838 { 839 if (Settings.Properties.DrawMouseCursor != value) 840 { 841 Settings.Properties.DrawMouseCursor = value; 842 NotifyPropertyChanged(); 843 } 844 } 845 } 846 847 public bool ValidateRemoteMachineIP 848 { 849 get 850 { 851 if (_validateRemoteIpGpoConfiguration == GpoRuleConfigured.Enabled) 852 { 853 return true; 854 } 855 else if (_validateRemoteIpGpoConfiguration == GpoRuleConfigured.Disabled) 856 { 857 return false; 858 } 859 860 return Settings.Properties.ValidateRemoteMachineIP; 861 } 862 863 set 864 { 865 if (!_validateRemoteIpIsGPOConfigured && (Settings.Properties.ValidateRemoteMachineIP != value)) 866 { 867 Settings.Properties.ValidateRemoteMachineIP = value; 868 NotifyPropertyChanged(); 869 } 870 } 871 } 872 873 public bool CardForValidateRemoteIpSettingIsEnabled => _validateRemoteIpIsGPOConfigured == false; 874 875 public string Name2IP 876 { 877 // Due to https://github.com/microsoft/microsoft-ui-xaml/issues/1826, we must 878 // add back \n chars on set and remove them on get for the widget 879 // to make its behavior consistent with the old UI and MWB internal code. 880 get 881 { 882 if (_disableUserDefinedIpMappingRulesGpoConfiguration == GpoRuleConfigured.Enabled) 883 { 884 return string.Empty; 885 } 886 887 return Settings.Properties.Name2IP.Value.Replace("\r\n", "\r"); 888 } 889 890 set 891 { 892 if (_disableUserDefinedIpMappingRulesIsGPOConfigured) 893 { 894 return; 895 } 896 897 var newValue = value.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n"); 898 899 if (Settings.Properties.Name2IP.Value != newValue) 900 { 901 Settings.Properties.Name2IP.Value = newValue; 902 NotifyPropertyChanged(); 903 } 904 } 905 } 906 907 private string _easyMouseIgnoredFullscreenAppsString; 908 909 public string EasyMouseFullscreenSwitchBlockExcludedApps 910 { 911 // Convert the list of excluded apps retrieved from the settings 912 // to a single string that can be displayed in the bound textbox 913 get 914 { 915 if (_easyMouseIgnoredFullscreenAppsString == null) 916 { 917 var excludedApps = Settings.Properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value; 918 _easyMouseIgnoredFullscreenAppsString = excludedApps.Count == 0 ? string.Empty : string.Join('\r', excludedApps); 919 } 920 921 return _easyMouseIgnoredFullscreenAppsString; 922 } 923 924 set 925 { 926 if (EasyMouseFullscreenSwitchBlockExcludedApps == value) 927 { 928 return; 929 } 930 931 _easyMouseIgnoredFullscreenAppsString = value; 932 933 var ignoredAppsSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); 934 if (value != string.Empty) 935 { 936 ignoredAppsSet.UnionWith(value.Split('\r', StringSplitOptions.RemoveEmptyEntries)); 937 } 938 939 Settings.Properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value = ignoredAppsSet; 940 NotifyPropertyChanged(); 941 } 942 } 943 944 public bool CardForName2IpSettingIsEnabled => _disableUserDefinedIpMappingRulesIsGPOConfigured == false; 945 946 public bool ShowPolicyConfiguredInfoForName2IPSetting => _disableUserDefinedIpMappingRulesIsGPOConfigured && IsEnabled; 947 948 public string Name2IpListPolicyData 949 { 950 // Due to https://github.com/microsoft/microsoft-ui-xaml/issues/1826, we must 951 // add back \n chars on set and remove them on get for the widget 952 // to make its behavior consistent with the old UI and MWB internal code. 953 // get => GPOWrapper.GetConfiguredMwbPolicyDefinedIpMappingRules().Replace("\r\n", "\r"); 954 get => _policyDefinedIpMappingRulesGPOData.Replace("\r\n", "\r"); 955 } 956 957 public bool Name2IpListPolicyIsConfigured => _policyDefinedIpMappingRulesIsGPOConfigured && IsEnabled; 958 959 public bool SameSubnetOnly 960 { 961 get 962 { 963 if (_sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Enabled) 964 { 965 return true; 966 } 967 else if (_sameSubnetOnlyGpoConfiguration == GpoRuleConfigured.Disabled) 968 { 969 return false; 970 } 971 972 return Settings.Properties.SameSubnetOnly; 973 } 974 975 set 976 { 977 if (!_sameSubnetOnlyIsGPOConfigured && (Settings.Properties.SameSubnetOnly != value)) 978 { 979 Settings.Properties.SameSubnetOnly = value; 980 NotifyPropertyChanged(); 981 } 982 } 983 } 984 985 public bool CardForSameSubnetOnlySettingIsEnabled => _sameSubnetOnlyIsGPOConfigured == false; 986 987 public bool BlockScreenSaverOnOtherMachines 988 { 989 get 990 { 991 if (_disallowBlockingScreensaverGpoConfiguration == GpoRuleConfigured.Enabled) 992 { 993 return false; 994 } 995 996 return Settings.Properties.BlockScreenSaverOnOtherMachines; 997 } 998 999 set 1000 { 1001 if (_disallowBlockingScreensaverIsGPOConfigured) 1002 { 1003 return; 1004 } 1005 1006 if (Settings.Properties.BlockScreenSaverOnOtherMachines != value) 1007 { 1008 Settings.Properties.BlockScreenSaverOnOtherMachines = value; 1009 NotifyPropertyChanged(); 1010 } 1011 } 1012 } 1013 1014 public bool CardForBlockScreensaverSettingIsEnabled => _disallowBlockingScreensaverIsGPOConfigured == false; 1015 1016 // Should match EasyMouseOption enum from MouseWithoutBorders and the ComboBox in the MouseWithoutBordersView.cs 1017 private enum EasyMouseOption 1018 { 1019 Disable = 0, 1020 Enable = 1, 1021 Ctrl = 2, 1022 Shift = 3, 1023 } 1024 1025 private EasyMouseOption _easyMouseOptionIndex; 1026 1027 public int EasyMouseOptionIndex 1028 { 1029 get 1030 { 1031 return (int)_easyMouseOptionIndex; 1032 } 1033 1034 set 1035 { 1036 if (value != (int)_easyMouseOptionIndex) 1037 { 1038 _easyMouseOptionIndex = (EasyMouseOption)value; 1039 Settings.Properties.EasyMouse.Value = value; 1040 NotifyPropertyChanged(nameof(EasyMouseOptionIndex)); 1041 } 1042 } 1043 } 1044 1045 public bool EasyMouseEnabled => (EasyMouseOption)EasyMouseOptionIndex != EasyMouseOption.Disable; 1046 1047 public bool IsEasyMouseBlockingOnFullscreenEnabled => 1048 EasyMouseEnabled && DisableEasyMouseWhenForegroundWindowIsFullscreen; 1049 1050 public bool DisableEasyMouseWhenForegroundWindowIsFullscreen 1051 { 1052 get 1053 { 1054 return Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen; 1055 } 1056 1057 set 1058 { 1059 if (Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen == value) 1060 { 1061 return; 1062 } 1063 1064 Settings.Properties.DisableEasyMouseWhenForegroundWindowIsFullscreen = value; 1065 NotifyPropertyChanged(); 1066 } 1067 } 1068 1069 public HotkeySettings ToggleEasyMouseShortcut 1070 { 1071 get => Settings.Properties.ToggleEasyMouseShortcut; 1072 1073 set 1074 { 1075 if (Settings.Properties.ToggleEasyMouseShortcut != value) 1076 { 1077 Settings.Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse; 1078 NotifyPropertyChanged(); 1079 NotifyModuleUpdatedSettings(); 1080 } 1081 } 1082 } 1083 1084 public HotkeySettings LockMachinesShortcut 1085 { 1086 get => Settings.Properties.LockMachineShortcut; 1087 1088 set 1089 { 1090 if (Settings.Properties.LockMachineShortcut != value) 1091 { 1092 Settings.Properties.LockMachineShortcut = value; 1093 Settings.Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine; 1094 NotifyPropertyChanged(); 1095 NotifyModuleUpdatedSettings(); 1096 } 1097 } 1098 } 1099 1100 public HotkeySettings ReconnectShortcut 1101 { 1102 get => Settings.Properties.ReconnectShortcut; 1103 1104 set 1105 { 1106 if (Settings.Properties.ReconnectShortcut != value) 1107 { 1108 Settings.Properties.ReconnectShortcut = value; 1109 Settings.Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect; 1110 NotifyPropertyChanged(); 1111 NotifyModuleUpdatedSettings(); 1112 } 1113 } 1114 } 1115 1116 public HotkeySettings HotKeySwitch2AllPC 1117 { 1118 get => Settings.Properties.Switch2AllPCShortcut; 1119 1120 set 1121 { 1122 if (Settings.Properties.Switch2AllPCShortcut != value) 1123 { 1124 Settings.Properties.Switch2AllPCShortcut = value; 1125 Settings.Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC; 1126 NotifyPropertyChanged(); 1127 NotifyModuleUpdatedSettings(); 1128 } 1129 } 1130 } 1131 1132 private int _selectedSwitchBetweenMachineShortcutOptionsIndex; 1133 1134 public int SelectedSwitchBetweenMachineShortcutOptionsIndex 1135 { 1136 get 1137 { 1138 return _selectedSwitchBetweenMachineShortcutOptionsIndex; 1139 } 1140 1141 set 1142 { 1143 if (_selectedSwitchBetweenMachineShortcutOptionsIndex != value) 1144 { 1145 _selectedSwitchBetweenMachineShortcutOptionsIndex = value; 1146 Settings.Properties.HotKeySwitchMachine.Value = _switchBetweenMachineShortcutOptions[value]; 1147 NotifyPropertyChanged(); 1148 } 1149 } 1150 } 1151 1152 public bool MoveMouseRelatively 1153 { 1154 get 1155 { 1156 return Settings.Properties.MoveMouseRelatively; 1157 } 1158 1159 set 1160 { 1161 if (Settings.Properties.MoveMouseRelatively != value) 1162 { 1163 Settings.Properties.MoveMouseRelatively = value; 1164 NotifyPropertyChanged(); 1165 } 1166 } 1167 } 1168 1169 public bool BlockMouseAtScreenCorners 1170 { 1171 get 1172 { 1173 return Settings.Properties.BlockMouseAtScreenCorners; 1174 } 1175 1176 set 1177 { 1178 if (Settings.Properties.BlockMouseAtScreenCorners != value) 1179 { 1180 Settings.Properties.BlockMouseAtScreenCorners = value; 1181 NotifyPropertyChanged(); 1182 } 1183 } 1184 } 1185 1186 private IndexedObservableCollection<DeviceViewModel> machineMatrixString; 1187 1188 public partial class DeviceViewModel : Observable 1189 { 1190 public string Name { get; set; } 1191 1192 public bool CanDragDrop { get; set; } 1193 1194 private Brush _statusBrush = StatusColors[SocketStatus.NA]; 1195 1196 public Brush StatusBrush 1197 { 1198 get 1199 { 1200 return _statusBrush; 1201 } 1202 1203 set 1204 { 1205 if (_statusBrush != value) 1206 { 1207 _statusBrush = value; 1208 OnPropertyChanged(nameof(StatusBrush)); 1209 } 1210 } 1211 } 1212 } 1213 1214 public IndexedObservableCollection<DeviceViewModel> MachineMatrixString 1215 { 1216 get 1217 { 1218 lock (_machineMatrixStringLock) 1219 { 1220 return machineMatrixString; 1221 } 1222 } 1223 1224 set 1225 { 1226 lock (_machineMatrixStringLock) 1227 { 1228 machineMatrixString = value; 1229 } 1230 1231 Settings.Properties.MachineMatrixString = new List<string>(value.ToEnumerable().Select(d => d.Name)); 1232 NotifyPropertyChanged(); 1233 } 1234 } 1235 1236 public bool ShowClipboardAndNetworkStatusMessages 1237 { 1238 get 1239 { 1240 return Settings.Properties.ShowClipboardAndNetworkStatusMessages; 1241 } 1242 1243 set 1244 { 1245 if (Settings.Properties.ShowClipboardAndNetworkStatusMessages != value) 1246 { 1247 Settings.Properties.ShowClipboardAndNetworkStatusMessages = value; 1248 NotifyPropertyChanged(); 1249 } 1250 } 1251 } 1252 1253 public bool LoadUpdatedSettings() 1254 { 1255 try 1256 { 1257 LoadViewModelFromSettings(SettingsUtils.GetSettings<MouseWithoutBordersSettings>("MouseWithoutBorders")); 1258 return true; 1259 } 1260 catch (System.Exception ex) 1261 { 1262 Logger.LogError(ex.Message); 1263 return false; 1264 } 1265 } 1266 1267 private void SendCustomAction(string actionName) 1268 { 1269 SendConfigMSG("{\"action\":{\"MouseWithoutBorders\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}"); 1270 } 1271 1272 public void AddFirewallRule() 1273 { 1274 SendCustomAction("add_firewall"); 1275 } 1276 1277 public void RefreshEnabledState() 1278 { 1279 InitializeEnabledValue(); 1280 OnPropertyChanged(nameof(IsEnabled)); 1281 } 1282 1283 private void NotifyModuleUpdatedSettings() 1284 { 1285 SendConfigMSG( 1286 string.Format( 1287 CultureInfo.InvariantCulture, 1288 "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", 1289 MouseWithoutBordersSettings.ModuleName, 1290 JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.MouseWithoutBordersSettings))); 1291 } 1292 1293 public void NotifyUpdatedSettings() 1294 { 1295 OnPropertyChanged(null); // Notify all properties might have changed. 1296 } 1297 1298 public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) 1299 { 1300 OnPropertyChanged(propertyName); 1301 1302 // Skip saving settings for UI properties 1303 if (propertyName == nameof(ShowInfobarCannotDragDropAsAdmin) || 1304 propertyName == nameof(ShowInfobarRunAsAdminText)) 1305 { 1306 return; 1307 } 1308 1309 SettingsUtils.SaveSettings(Settings.ToJsonString(), MouseWithoutBordersSettings.ModuleName); 1310 1311 if (propertyName == nameof(UseService)) 1312 { 1313 NotifyModuleUpdatedSettings(); 1314 } 1315 } 1316 1317 private Func<string, int> SendConfigMSG { get; } 1318 1319 public void CopyMachineNameToClipboard() 1320 { 1321 var data = new DataPackage(); 1322 data.SetText(Dns.GetHostName()); 1323 Clipboard.SetContent(data); 1324 } 1325 1326 protected override void Dispose(bool disposing) 1327 { 1328 if (!_disposed) 1329 { 1330 if (disposing) 1331 { 1332 // Cancel the cancellation token source 1333 _cancellationTokenSource?.Cancel(); 1334 _cancellationTokenSource?.Dispose(); 1335 1336 // Wait for the machine polling task to complete 1337 try 1338 { 1339 _machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1)); 1340 } 1341 catch (AggregateException) 1342 { 1343 // Task was cancelled, which is expected 1344 } 1345 1346 // Dispose the named pipe stream 1347 try 1348 { 1349 syncHelperStream?.Dispose(); 1350 } 1351 catch (Exception ex) 1352 { 1353 Logger.LogError($"Error disposing sync helper stream: {ex}"); 1354 } 1355 finally 1356 { 1357 syncHelperStream = null; 1358 } 1359 1360 // Dispose the semaphore 1361 _ipcSemaphore?.Dispose(); 1362 } 1363 1364 _disposed = true; 1365 } 1366 1367 base.Dispose(disposing); 1368 } 1369 1370 internal void UninstallService() 1371 { 1372 SendCustomAction("uninstall_service"); 1373 } 1374 1375 public bool ShowPolicyConfiguredInfoForServiceSettings 1376 { 1377 get 1378 { 1379 return IsEnabled && _allowServiceModeIsGPOConfigured; 1380 } 1381 } 1382 1383 public bool ShowPolicyConfiguredInfoForBehaviorSettings 1384 { 1385 get 1386 { 1387 return IsEnabled && (_disallowBlockingScreensaverIsGPOConfigured 1388 || _clipboardSharingEnabledIsGPOConfigured || _fileTransferEnabledIsGPOConfigured 1389 || _sameSubnetOnlyIsGPOConfigured || _validateRemoteIpIsGPOConfigured); 1390 } 1391 } 1392 1393 public bool ShowInfobarCannotDragDropAsAdmin 1394 { 1395 get { return IsElevated && IsEnabled; } 1396 } 1397 1398 public bool ShowInfobarRunAsAdminText 1399 { 1400 get { return !CanToggleUseService && IsEnabled && !ShowPolicyConfiguredInfoForServiceSettings; } 1401 } 1402 } 1403 }