2020-08-18 01:00:56 +08:00
// 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 ;
2021-04-27 03:01:38 +08:00
using System.Diagnostics ;
2020-10-22 03:32:53 +08:00
using System.Diagnostics.CodeAnalysis ;
2021-04-27 03:01:38 +08:00
using System.Globalization ;
2020-09-24 04:20:32 +08:00
using System.IO ;
2020-08-18 01:00:56 +08:00
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using System.Windows.Input ;
2020-10-23 00:45:48 +08:00
using Microsoft.PowerToys.Settings.UI.Library.Helpers ;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces ;
using Microsoft.PowerToys.Settings.UI.Library.Utilities ;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands ;
2020-08-18 01:00:56 +08:00
2020-10-23 00:45:48 +08:00
namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
2020-08-18 01:00:56 +08:00
{
public class KeyboardManagerViewModel : Observable
{
2020-09-24 04:20:32 +08:00
private GeneralSettings GeneralSettingsConfig { get ; set ; }
2020-09-22 01:14:44 +08:00
private readonly ISettingsUtils _settingsUtils ;
2020-09-24 04:20:32 +08:00
private const string PowerToyName = KeyboardManagerSettings . ModuleName ;
2020-08-18 01:00:56 +08:00
private const string JsonFileType = ".json" ;
2021-04-09 01:42:46 +08:00
2021-04-27 03:01:38 +08:00
private const string KeyboardManagerEditorPath = "modules\\KeyboardManager\\KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe" ;
private Process editor ;
2021-04-09 01:42:46 +08:00
2021-04-27 03:01:38 +08:00
private enum KeyboardManagerEditorType
{
KeyEditor = 0 ,
ShortcutEditor ,
}
2020-08-18 01:00:56 +08:00
2020-08-20 06:59:10 +08:00
public KeyboardManagerSettings Settings { get ; set ; }
2020-08-18 01:00:56 +08:00
2020-08-20 06:59:10 +08:00
private ICommand _remapKeyboardCommand ;
private ICommand _editShortcutCommand ;
private KeyboardManagerProfile _profile ;
2020-08-18 01:00:56 +08:00
2020-08-18 06:00:19 +08:00
private Func < string , int > SendConfigMSG { get ; }
private Func < List < KeysDataModel > , int > FilterRemapKeysList { get ; }
2020-10-23 00:45:48 +08:00
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions should not crash the program but will be logged until we can understand common exception scenarios")]
2020-09-24 04:20:32 +08:00
public KeyboardManagerViewModel ( ISettingsUtils settingsUtils , ISettingsRepository < GeneralSettings > settingsRepository , Func < string , int > ipcMSGCallBackFunc , Func < List < KeysDataModel > , int > filterRemapKeysList )
2020-08-18 01:00:56 +08:00
{
2020-10-20 04:32:05 +08:00
if ( settingsRepository = = null )
{
throw new ArgumentNullException ( nameof ( settingsRepository ) ) ;
}
2020-09-24 04:20:32 +08:00
GeneralSettingsConfig = settingsRepository . SettingsConfig ;
2020-08-18 06:00:19 +08:00
// set the callback functions value to hangle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc ;
FilterRemapKeysList = filterRemapKeysList ;
2020-09-22 01:14:44 +08:00
_settingsUtils = settingsUtils ? ? throw new ArgumentNullException ( nameof ( settingsUtils ) ) ;
if ( _settingsUtils . SettingsExists ( PowerToyName ) )
2020-08-18 01:00:56 +08:00
{
2020-10-22 03:32:53 +08:00
try
{
2021-01-14 20:14:29 +08:00
Settings = _settingsUtils . GetSettingsOrDefault < KeyboardManagerSettings > ( PowerToyName ) ;
2020-10-22 03:32:53 +08:00
}
catch ( Exception e )
{
Logger . LogError ( $"Exception encountered while reading {PowerToyName} settings." , e ) ;
#if DEBUG
if ( e is ArgumentException | | e is ArgumentNullException | | e is PathTooLongException )
{
2020-10-23 00:45:48 +08:00
throw ;
2020-10-22 03:32:53 +08:00
}
#endif
}
2020-08-18 01:00:56 +08:00
// Load profile.
if ( ! LoadProfile ( ) )
{
2020-08-20 06:59:10 +08:00
_profile = new KeyboardManagerProfile ( ) ;
2020-08-18 01:00:56 +08:00
}
}
else
{
2020-09-24 04:20:32 +08:00
Settings = new KeyboardManagerSettings ( ) ;
2020-09-22 01:14:44 +08:00
_settingsUtils . SaveSettings ( Settings . ToJsonString ( ) , PowerToyName ) ;
2020-08-18 01:00:56 +08:00
}
}
public bool Enabled
{
get
{
2020-09-24 04:20:32 +08:00
return GeneralSettingsConfig . Enabled . KeyboardManager ;
2020-08-18 01:00:56 +08:00
}
set
{
2020-09-24 04:20:32 +08:00
if ( GeneralSettingsConfig . Enabled . KeyboardManager ! = value )
2020-08-18 01:00:56 +08:00
{
2020-09-24 04:20:32 +08:00
GeneralSettingsConfig . Enabled . KeyboardManager = value ;
2020-08-18 01:00:56 +08:00
OnPropertyChanged ( nameof ( Enabled ) ) ;
2021-04-27 03:01:38 +08:00
if ( ! Enabled & & editor ! = null )
{
editor . CloseMainWindow ( ) ;
}
2020-09-24 04:20:32 +08:00
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings ( GeneralSettingsConfig ) ;
2020-08-18 01:00:56 +08:00
2020-08-18 06:00:19 +08:00
SendConfigMSG ( outgoing . ToString ( ) ) ;
2020-08-18 01:00:56 +08:00
}
}
}
// store remappings
public List < KeysDataModel > RemapKeys
{
get
{
2020-08-20 06:59:10 +08:00
if ( _profile ! = null )
2020-08-18 01:00:56 +08:00
{
2020-08-20 06:59:10 +08:00
return _profile . RemapKeys . InProcessRemapKeys ;
2020-08-18 01:00:56 +08:00
}
else
{
return new List < KeysDataModel > ( ) ;
}
}
}
public static List < AppSpecificKeysDataModel > CombineShortcutLists ( List < KeysDataModel > globalShortcutList , List < AppSpecificKeysDataModel > appSpecificShortcutList )
{
2020-10-20 04:32:05 +08:00
if ( globalShortcutList = = null & & appSpecificShortcutList = = null )
{
return new List < AppSpecificKeysDataModel > ( ) ;
}
else if ( globalShortcutList = = null )
{
return appSpecificShortcutList ;
}
else if ( appSpecificShortcutList = = null )
{
return globalShortcutList . ConvertAll ( x = > new AppSpecificKeysDataModel { OriginalKeys = x . OriginalKeys , NewRemapKeys = x . NewRemapKeys , TargetApp = "All Apps" } ) . ToList ( ) ;
}
else
{
return globalShortcutList . ConvertAll ( x = > new AppSpecificKeysDataModel { OriginalKeys = x . OriginalKeys , NewRemapKeys = x . NewRemapKeys , TargetApp = "All Apps" } ) . Concat ( appSpecificShortcutList ) . ToList ( ) ;
}
2020-08-18 01:00:56 +08:00
}
public List < AppSpecificKeysDataModel > RemapShortcuts
{
get
{
2020-08-20 06:59:10 +08:00
if ( _profile ! = null )
2020-08-18 01:00:56 +08:00
{
2020-08-20 06:59:10 +08:00
return CombineShortcutLists ( _profile . RemapShortcuts . GlobalRemapShortcuts , _profile . RemapShortcuts . AppSpecificRemapShortcuts ) ;
2020-08-18 01:00:56 +08:00
}
else
{
return new List < AppSpecificKeysDataModel > ( ) ;
}
}
}
2020-08-20 06:59:10 +08:00
public ICommand RemapKeyboardCommand = > _remapKeyboardCommand ? ? ( _remapKeyboardCommand = new RelayCommand ( OnRemapKeyboard ) ) ;
2020-08-18 01:00:56 +08:00
2020-08-20 06:59:10 +08:00
public ICommand EditShortcutCommand = > _editShortcutCommand ? ? ( _editShortcutCommand = new RelayCommand ( OnEditShortcut ) ) ;
2020-08-18 01:00:56 +08:00
2021-04-27 03:01:38 +08:00
private void OnRemapKeyboard ( )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
OpenEditor ( ( int ) KeyboardManagerEditorType . KeyEditor ) ;
2020-08-18 01:00:56 +08:00
}
2021-04-27 03:01:38 +08:00
private void OnEditShortcut ( )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
OpenEditor ( ( int ) KeyboardManagerEditorType . ShortcutEditor ) ;
2020-08-18 01:00:56 +08:00
}
2021-04-27 03:01:38 +08:00
private static void BringProcessToFront ( Process process )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
if ( process = = null )
{
return ;
}
IntPtr handle = process . MainWindowHandle ;
2021-04-27 07:14:59 +08:00
if ( NativeMethods . IsIconic ( handle ) )
2021-04-27 03:01:38 +08:00
{
2021-04-27 07:14:59 +08:00
NativeMethods . ShowWindow ( handle , NativeMethods . SWRESTORE ) ;
2021-04-27 03:01:38 +08:00
}
2021-04-27 07:14:59 +08:00
NativeMethods . SetForegroundWindow ( handle ) ;
2020-08-18 01:00:56 +08:00
}
2021-04-27 03:01:38 +08:00
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions here (especially mutex errors) should not halt app execution, but they will be logged.")]
private void OpenEditor ( int type )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
try
{
if ( editor ! = null & & editor . HasExited )
{
Logger . LogInfo ( $"Previous instance of {PowerToyName} editor exited" ) ;
editor = null ;
}
if ( editor ! = null )
{
Logger . LogInfo ( $"The {PowerToyName} editor instance {editor.Id} exists. Bringing the process to the front" ) ;
BringProcessToFront ( editor ) ;
return ;
}
string path = Path . Combine ( Environment . CurrentDirectory , KeyboardManagerEditorPath ) ;
Logger . LogInfo ( $"Starting {PowerToyName} editor from {path}" ) ;
// InvariantCulture: type represents the KeyboardManagerEditorType enum value
2022-04-20 04:00:28 +08:00
editor = Process . Start ( path , $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}" ) ;
2021-04-27 03:01:38 +08:00
}
catch ( Exception e )
{
Logger . LogError ( $"Exception encountered when opening an {PowerToyName} editor" , e ) ;
}
2020-08-18 01:00:56 +08:00
}
2020-08-18 06:00:19 +08:00
public void NotifyFileChanged ( )
2020-08-18 01:00:56 +08:00
{
2020-08-18 06:00:19 +08:00
OnPropertyChanged ( nameof ( RemapKeys ) ) ;
OnPropertyChanged ( nameof ( RemapShortcuts ) ) ;
2020-08-18 01:00:56 +08:00
}
2020-10-22 03:32:53 +08:00
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions here (especially mutex errors) should not halt app execution, but they will be logged.")]
2020-08-18 06:00:19 +08:00
public bool LoadProfile ( )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
// The KBM process out of runner creates the default.json file if it does not exist.
2020-08-18 01:00:56 +08:00
var success = true ;
2021-04-27 03:01:38 +08:00
var readSuccessfully = false ;
string fileName = Settings . Properties . ActiveConfiguration . Value + JsonFileType ;
2020-08-18 01:00:56 +08:00
try
{
2021-04-27 03:01:38 +08:00
// retry loop for reading
CancellationTokenSource ts = new CancellationTokenSource ( ) ;
Task t = Task . Run ( ( ) = >
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
while ( ! readSuccessfully & & ! ts . IsCancellationRequested )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
if ( _settingsUtils . SettingsExists ( PowerToyName , fileName ) )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
try
2020-09-24 04:20:32 +08:00
{
2021-01-14 20:14:29 +08:00
_profile = _settingsUtils . GetSettingsOrDefault < KeyboardManagerProfile > ( PowerToyName , fileName ) ;
2021-04-27 03:01:38 +08:00
readSuccessfully = true ;
2020-09-24 04:20:32 +08:00
}
2021-04-27 03:01:38 +08:00
catch ( Exception e )
2020-09-24 04:20:32 +08:00
{
2021-04-27 03:01:38 +08:00
Logger . LogError ( $"Exception encountered when reading {PowerToyName} settings" , e ) ;
2020-09-24 04:20:32 +08:00
}
2020-08-18 01:00:56 +08:00
}
2021-04-27 03:01:38 +08:00
if ( ! readSuccessfully )
2020-08-18 01:00:56 +08:00
{
2021-04-27 03:01:38 +08:00
Task . Delay ( 500 ) . Wait ( ) ;
2020-08-18 01:00:56 +08:00
}
}
2021-04-27 03:01:38 +08:00
} ) ;
2022-03-11 01:31:16 +08:00
var completedInTime = t . Wait ( 3000 , ts . Token ) ;
2021-04-27 03:01:38 +08:00
ts . Cancel ( ) ;
ts . Dispose ( ) ;
2022-03-11 01:31:16 +08:00
if ( readSuccessfully )
{
FilterRemapKeysList ( _profile ? . RemapKeys ? . InProcessRemapKeys ) ;
}
else
2021-04-27 03:01:38 +08:00
{
success = false ;
2020-08-18 01:00:56 +08:00
}
2021-04-27 03:01:38 +08:00
2022-03-11 01:31:16 +08:00
if ( ! completedInTime )
{
Logger . LogError ( $"Timeout encountered when loading {PowerToyName} profile" ) ;
}
2020-08-18 01:00:56 +08:00
}
2020-10-22 03:32:53 +08:00
catch ( Exception e )
2020-08-18 01:00:56 +08:00
{
// Failed to load the configuration.
2020-10-22 03:32:53 +08:00
Logger . LogError ( $"Exception encountered when loading {PowerToyName} profile" , e ) ;
2020-08-18 01:00:56 +08:00
success = false ;
}
2022-03-11 01:31:16 +08:00
if ( ! success )
{
Logger . LogError ( $"Couldn't load {PowerToyName} profile" ) ;
}
2020-08-18 01:00:56 +08:00
return success ;
}
}
}