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 ;
2020-10-22 03:32:53 +08:00
using System.Diagnostics.CodeAnalysis ;
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-08-18 06:00:19 +08:00
using Microsoft.PowerToys.Settings.UI.Lib.Helpers ;
2020-09-24 04:20:32 +08:00
using Microsoft.PowerToys.Settings.UI.Lib.Interface ;
2020-08-18 01:00:56 +08:00
using Microsoft.PowerToys.Settings.UI.Lib.Utilities ;
2020-08-18 06:00:19 +08:00
using Microsoft.PowerToys.Settings.UI.Lib.ViewModels.Commands ;
2020-08-18 01:00:56 +08:00
2020-08-18 06:00:19 +08:00
namespace Microsoft.PowerToys.Settings.UI.Lib.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 RemapKeyboardActionName = "RemapKeyboard" ;
private const string RemapKeyboardActionValue = "Open Remap Keyboard Window" ;
private const string EditShortcutActionName = "EditShortcut" ;
private const string EditShortcutActionValue = "Open Edit Shortcut Window" ;
private const string JsonFileType = ".json" ;
private const string ProfileFileMutexName = "PowerToys.KeyboardManager.ConfigMutex" ;
private const int ProfileFileMutexWaitTimeoutMilliseconds = 1000 ;
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-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
{
Settings = _settingsUtils . GetSettings < KeyboardManagerSettings > ( PowerToyName ) ;
}
catch ( Exception e )
{
Logger . LogError ( $"Exception encountered while reading {PowerToyName} settings." , e ) ;
#if DEBUG
if ( e is ArgumentException | | e is ArgumentNullException | | e is PathTooLongException )
{
throw e ;
}
#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 ) ) ;
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
2020-10-20 04:32:05 +08:00
// Note: FxCop suggests calling ConfigureAwait() for the following methods,
// and calling ConfigureAwait(true) has the same behavior as not explicitly
// calling it (continuations are scheduled on the task-creating thread)
2020-08-18 01:00:56 +08:00
private async void OnRemapKeyboard ( )
{
2020-10-20 04:32:05 +08:00
await Task . Run ( ( ) = > OnRemapKeyboardBackground ( ) ) . ConfigureAwait ( true ) ;
2020-08-18 01:00:56 +08:00
}
private async void OnEditShortcut ( )
{
2020-10-20 04:32:05 +08:00
await Task . Run ( ( ) = > OnEditShortcutBackground ( ) ) . ConfigureAwait ( true ) ;
2020-08-18 01:00:56 +08:00
}
private async Task OnRemapKeyboardBackground ( )
{
Helper . AllowRunnerToForeground ( ) ;
2020-08-18 06:00:19 +08:00
SendConfigMSG ( Helper . GetSerializedCustomAction ( PowerToyName , RemapKeyboardActionName , RemapKeyboardActionValue ) ) ;
2020-10-20 04:32:05 +08:00
await Task . CompletedTask . ConfigureAwait ( true ) ;
2020-08-18 01:00:56 +08:00
}
private async Task OnEditShortcutBackground ( )
{
Helper . AllowRunnerToForeground ( ) ;
2020-08-18 06:00:19 +08:00
SendConfigMSG ( Helper . GetSerializedCustomAction ( PowerToyName , EditShortcutActionName , EditShortcutActionValue ) ) ;
2020-10-20 04:32:05 +08:00
await Task . CompletedTask . ConfigureAwait ( true ) ;
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
{
var success = true ;
try
{
using ( var profileFileMutex = Mutex . OpenExisting ( ProfileFileMutexName ) )
{
if ( profileFileMutex . WaitOne ( ProfileFileMutexWaitTimeoutMilliseconds ) )
{
// update the UI element here.
try
{
2020-09-24 04:20:32 +08:00
string fileName = Settings . Properties . ActiveConfiguration . Value + JsonFileType ;
if ( _settingsUtils . SettingsExists ( PowerToyName , fileName ) )
{
_profile = _settingsUtils . GetSettings < KeyboardManagerProfile > ( PowerToyName , fileName ) ;
}
else
{
// The KBM process out of runner creates the default.json file if it does not exist.
success = false ;
}
FilterRemapKeysList ( _profile ? . RemapKeys ? . InProcessRemapKeys ) ;
2020-08-18 01:00:56 +08:00
}
finally
{
// Make sure to release the mutex.
profileFileMutex . ReleaseMutex ( ) ;
}
}
else
{
success = false ;
}
}
}
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 ;
}
return success ;
}
}
}