PowerControl: Update and expose UserProfiles that can persist per-game settings

This commit is contained in:
Kamil Trzciński 2023-01-05 23:35:31 +01:00
parent 3252e799cb
commit 2d5f8c498f
11 changed files with 436 additions and 200 deletions

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ build-Debug/
.vscode/
scripts/Redist/
SteamDeckTools_Setup*.exe
UserProfiles/

View file

@ -31,6 +31,11 @@ namespace CommonHelpers
this.SettingChanged += delegate { };
}
public bool Exists
{
get { return File.Exists(this.ConfigFile); }
}
public override string ToString()
{
return "";
@ -138,6 +143,27 @@ namespace CommonHelpers
}
}
public void TouchFile()
{
lock (this)
{
if (Exists)
return;
using (File.Create(ConfigFile)) { }
}
}
public void DeleteFile()
{
lock (this)
{
cachedValues.Clear();
try { File.Delete(ConfigFile); }
catch (DirectoryNotFoundException) { }
}
}
[DllImport("kernel32.dll")]
static extern bool WritePrivateProfileString(string lpAppName, string? lpKeyName, string? lpString, string lpFileName);

View file

@ -1,4 +1,5 @@
using RTSSSharedMemoryNET;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace CommonHelpers
@ -15,32 +16,46 @@ namespace CommonHelpers
return IsOSDForeground(out processId, out _);
}
public static bool IsOSDForeground(out int processId, out string? applicationName)
public static bool IsOSDForeground(out int processId, out string processName)
{
applicationName = null;
return new Applications().FindForeground(out processId, out processName);
}
try
public struct Applications
{
public IDictionary<int, String> IDs { get; } = new Dictionary<int, String>();
public Applications()
{
RTSSSharedMemoryNET.AppEntry[] appEntries;
try { appEntries = OSD.GetAppEntries(AppFlags.MASK); }
catch { return; }
foreach (var app in appEntries)
IDs.TryAdd(app.ProcessId, Path.GetFileNameWithoutExtension(app.Name));
}
public bool FindForeground(out int processId, out string processName)
{
processId = 0;
processName = "";
var id = GetTopLevelProcessId();
processId = (int)id.GetValueOrDefault(0);
if (id is null)
return false;
foreach (var app in OSD.GetAppEntries(AppFlags.MASK))
{
if (app.ProcessId == processId)
{
applicationName = ExtractAppName(app.Name);
return true;
}
}
if (!IDs.TryGetValue(id.Value, out var name))
return false;
return false;
processId = id.Value;
processName = name;
return true;
}
catch
public bool IsRunning(int processId)
{
processId = 0;
return false;
return IDs.ContainsKey(processId);
}
}
@ -86,13 +101,6 @@ namespace CommonHelpers
}
}
public static List<string> GetCurrentApps()
{
var apps = OSD.GetAppEntries(AppFlags.MASK).Select(e => ExtractAppName(e.Name)).ToList();
return apps;
}
public static uint EnableFlag(uint flag, bool status)
{
var current = SetFlags(~flag, status ? flag : 0);
@ -142,24 +150,12 @@ namespace CommonHelpers
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
private static string ExtractAppName(string fullName)
{
string res = fullName.Split('\\').Last();
if (res.ToLower().Contains(".exe"))
{
return res[..^4];
}
return res;
}
private static uint? GetTopLevelProcessId()
private static int? GetTopLevelProcessId()
{
var hWnd = GetForegroundWindow();
var result = GetWindowThreadProcessId(hWnd, out uint processId);
if (result != 0)
return processId;
return (int)processId;
return null;
}

View file

@ -33,7 +33,7 @@ namespace PowerControl
DateTime? neptuneDeviceNextKey;
System.Windows.Forms.Timer neptuneTimer;
ProfilesController profilesController;
ProfilesController? profilesController;
SharedData<PowerControlSetting> sharedData = SharedData<PowerControlSetting>.CreateNew();
@ -113,7 +113,6 @@ namespace PowerControl
osdTimer.Enabled = true;
profilesController = new ProfilesController();
profilesController.Initialize();
GlobalHotKey.RegisterHotKey(Settings.Default.MenuUpKey, () =>
{
@ -222,6 +221,10 @@ namespace PowerControl
notifyIcon.Icon = Resources.traffic_light_outline_red;
}
var watchedProfiles = profilesController?.WatchedProfiles ?? new string[0];
if (watchedProfiles.Any())
notifyIcon.Text += ". Profile: " + string.Join(", ", watchedProfiles);
updateOSD();
}
@ -381,6 +384,7 @@ namespace PowerControl
public void Dispose()
{
using (profilesController) { }
components.Dispose();
osdClose();
}

View file

@ -11,43 +11,46 @@ namespace PowerControl.Helper
{
public class ProfileSettings : BaseSettings
{
private static string profilesPath = Path.Combine(Directory.GetCurrentDirectory(), "Profiles");
static ProfileSettings()
public static String UserProfilesPath
{
Directory.CreateDirectory(profilesPath);
get
{
var exePath = System.Reflection.Assembly.GetExecutingAssembly().Location;
var exeFolder = Path.GetDirectoryName(exePath) ?? Directory.GetCurrentDirectory();
var exeUserProfiles = Path.Combine(exeFolder, "UserProfiles");
if (!Directory.Exists(exeUserProfiles))
Directory.CreateDirectory(exeUserProfiles);
return exeUserProfiles;
}
}
public ProfileSettings(string profileName) : base("Profile")
public String ProfileName { get; }
public ProfileSettings(string profileName) : base("PersistentSettings")
{
this.TouchSettings = true;
this.ConfigFile = Path.Combine(profilesPath, profileName + ".ini");
this.ProfileName = profileName;
this.ConfigFile = Path.Combine(UserProfilesPath, String.Format("PowerControl.Process.{0}.ini", profileName));
this.SettingChanging += delegate { };
this.SettingChanged += delegate { };
}
public T Get<T>(string key, T defaultValue)
public String? GetValue(string key)
{
var result = base.Get(key, String.Empty);
if (result == String.Empty)
return null;
return result;
}
public int GetInt(string key, int defaultValue)
{
return base.Get(key, defaultValue);
}
public new bool Set<T>(string key, T value)
public void SetValue(string key, string value)
{
return base.Set(key, value);
}
public static bool CheckIfExists(string profileName)
{
foreach (FileInfo fi in Directory.CreateDirectory(profilesPath).GetFiles())
{
if (fi.Name[^4..].Equals(".ini") && fi.Name[..^4].Equals(profileName))
{
return true;
}
}
return false;
base.Set(key, value);
}
}
}

View file

@ -1,137 +0,0 @@
using CommonHelpers;
using PowerControl.Helper;
using PowerControl.Menu;
namespace PowerControl.Helpers
{
public class ProfilesController
{
private const string IsTroubledKey = "IsTroubled";
private const string DefaultName = "Default";
private string CurrentGame = string.Empty;
private ProfileSettings DefaultSettings = new ProfileSettings(DefaultName);
private ProfileSettings? CurrentSettings;
private static string[] troubledGames = { "dragonageinquisition" };
private System.Windows.Forms.Timer? timer;
public ProfilesController()
{
timer = new System.Windows.Forms.Timer();
}
public void Initialize()
{
MenuStack.Root.ValueChanged += OnOptionValueChange;
timer.Interval = 1000;
timer.Tick += (_, _) =>
{
timer.Stop();
RefreshProfiles();
timer.Start();
};
timer.Start();
}
private void RefreshProfiles()
{
if (!DeviceManager.IsDeckOnlyDisplay())
{
CurrentGame = string.Empty;
return;
}
string? gameName;
RTSS.IsOSDForeground(out _, out gameName);
// If there's no foreground games keep current profile if possible
if (gameName == null && RTSS.GetCurrentApps().Contains(CurrentGame))
{
gameName = CurrentGame;
}
if (gameName == null && CurrentGame != DefaultName)
{
CurrentGame = DefaultName;
CurrentSettings = null;
ApplyProfile();
}
if (gameName != null && CurrentGame != gameName)
{
CurrentGame = gameName;
CurrentSettings = ProfileSettings.CheckIfExists(CurrentGame) ?
new ProfileSettings(CurrentGame) : null;
ApplyProfile();
}
}
private void ApplyProfile()
{
int delay = GetBoolValue(IsTroubledKey) ? 5200 : 0;
var options = MenuStack.Root.Items.Where(o => o is MenuItemWithOptions).Select(o => (MenuItemWithOptions)o).ToList();
foreach (var option in options)
{
string? key = option.PersistentKey;
if (key != null)
{
option.Set(GetValue(option), delay, true);
}
}
}
private void OnOptionValueChange(MenuItemWithOptions options, string? oldValue, string newValue)
{
string? key = options.PersistentKey;
if (key != null)
{
SetValue(key, newValue);
}
}
private void SetBoolValue(string key, bool value)
{
var settings = CurrentSettings ?? DefaultSettings;
settings.Set(key, value);
}
private bool GetBoolValue(string key)
{
var settings = CurrentSettings ?? DefaultSettings;
return settings.Get(key, false);
}
private void SetValue(string key, string value)
{
var settings = CurrentSettings ?? DefaultSettings;
settings.Set(key, value);
}
private string GetValue(MenuItemWithOptions option)
{
if (CurrentSettings == null)
{
return GetDefaultValue(option);
}
return CurrentSettings.Get(option.PersistentKey, GetDefaultValue(option));
}
private string GetDefaultValue(MenuItemWithOptions option)
{
return DefaultSettings.Get(option.PersistentKey, option.ResetValue?.Invoke() ?? string.Empty);
}
}
}

View file

@ -5,6 +5,7 @@ namespace PowerControl.Menu
public IList<string> Options { get; set; } = new List<string>();
public string? SelectedOption { get; private set; }
public string? ActiveOption { get; set; }
public string? ProfileOption { get; set; }
public int ApplyDelay { get; set; }
public bool CycleOptions { get; set; } = true;
public string? PersistentKey;
@ -175,6 +176,14 @@ namespace PowerControl.Menu
if (SelectedOption != null && ActiveOption != SelectedOption)
output += " (active: " + optionText(ActiveOption) + ")";
if (ProfileOption != null)
{
if (ProfileOption != ActiveOption && ProfileOption != SelectedOption)
output += " (profile: " + optionText(ProfileOption) + ")";
else
output += " [P]";
}
return output;
}

View file

@ -7,6 +7,8 @@ namespace PowerControl
Name = String.Format("\r\n\r\nPower Control v{0}\r\n", Application.ProductVersion.ToString()),
Items =
{
Options.Profiles.Instance,
new Menu.MenuItemSeparator(),
Options.Brightness.Instance,
Options.Volume.Instance,
new Menu.MenuItemSeparator(),

View file

@ -0,0 +1,71 @@
namespace PowerControl.Options
{
public static class Profiles
{
public static ProfilesController? Controller;
public static Menu.MenuItemWithOptions Instance = new Menu.MenuItemWithOptions()
{
Name = "Profiles",
OptionsValues = delegate ()
{
var currentProfileSettings = Controller?.CurrentProfileSettings;
if (currentProfileSettings == null)
return null;
if (currentProfileSettings.Exists)
{
return new string[] {
currentProfileSettings.ProfileName,
"Save All",
"Delete"
};
}
else
{
return new string[] {
"None",
"Create New",
"Save All"
};
}
},
CycleOptions = false,
CurrentValue = delegate ()
{
var currentProfileSettings = Controller?.CurrentProfileSettings;
if (currentProfileSettings == null)
return null;
if (currentProfileSettings.Exists)
return currentProfileSettings.ProfileName;
else
return "None";
},
ApplyValue = (selected) =>
{
switch (selected)
{
case "Delete":
Controller?.DeleteProfile();
return "None";
case "Create New":
Controller?.CreateProfile(false);
return Controller?.CurrentProfileSettings?.ProfileName;
case "Save All":
Controller?.CreateProfile(true);
return Controller?.CurrentProfileSettings?.ProfileName;
default:
return selected;
}
},
AfterApply = () =>
{
Instance?.Update();
}
};
}
}

View file

@ -25,7 +25,7 @@ namespace PowerControl.Options
ApplyValue = (selected) =>
{
DisplayResolutionController.SetRefreshRate(int.Parse(selected));
return DisplayResolutionController.GetRefreshRate().ToString();
},
AfterApply = () =>

View file

@ -0,0 +1,261 @@
using System.Diagnostics;
using CommonHelpers;
using ExternalHelpers;
using PowerControl.Helper;
using PowerControl.Menu;
namespace PowerControl
{
public class ProfilesController : IDisposable
{
public const bool AutoCreateProfiles = true;
private Dictionary<int, PowerControl.Helper.ProfileSettings> watchedProcesses = new Dictionary<int, PowerControl.Helper.ProfileSettings>();
private Dictionary<MenuItemWithOptions, String>? changedSettings;
private System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer()
{
Interval = 1000
};
public IEnumerable<String> WatchedProfiles
{
get
{
foreach (var process in watchedProcesses)
yield return process.Value.ProfileName;
}
}
public ProfileSettings? CurrentProfileSettings { get; private set; }
public ProfilesController()
{
PowerControl.Options.Profiles.Controller = this;
MenuStack.Root.ValueChanged += Root_OnOptionValueChange;
timer.Start();
timer.Tick += Timer_Tick;
}
~ProfilesController()
{
Dispose();
}
public void Dispose()
{
GC.SuppressFinalize(this);
PowerControl.Options.Profiles.Controller = null;
MenuStack.Root.ValueChanged -= Root_OnOptionValueChange;
timer.Stop();
}
private void Timer_Tick(object? sender, EventArgs e)
{
timer.Enabled = false;
try { RefreshProfiles(); }
finally { timer.Enabled = true; }
}
private void RefreshProfiles()
{
if (DisplayConfig.IsInternalConnected != true)
{
foreach (var process in watchedProcesses)
RemoveProcess(process.Key);
return;
}
var applications = new RTSS.Applications();
if (applications.FindForeground(out var processId, out var processName))
{
if (!BringUpProcess(processId))
AddProcess(processId, processName);
}
foreach (var process in watchedProcesses)
{
if (applications.IsRunning(process.Key))
continue;
RemoveProcess(process.Key);
}
}
private bool BringUpProcess(int processId)
{
if (!watchedProcesses.TryGetValue(processId, out var profileSettings))
return false;
if (CurrentProfileSettings != profileSettings)
{
Log.TraceLine("ProfilesController: Foreground changed: {0} => {1}",
CurrentProfileSettings?.ProfileName, profileSettings.ProfileName);
CurrentProfileSettings = profileSettings;
ProfileChanged();
}
return true;
}
private void AddProcess(int processId, string processName)
{
Log.TraceLine("ProfilesController: New Process: {0}/{1}", processId, processName);
if (changedSettings == null)
changedSettings = new Dictionary<MenuItemWithOptions, string>();
var profileSettings = new ProfileSettings(processName);
watchedProcesses.Add(processId, profileSettings);
ApplyProfile(profileSettings);
}
private void RemoveProcess(int processId)
{
if (!watchedProcesses.Remove(processId, out var profileSettings))
return;
if (CurrentProfileSettings == profileSettings)
CurrentProfileSettings = null;
Log.TraceLine("ProfilesController: Removed Process: {0}", processId);
if (watchedProcesses.Any())
return;
ResetProfile();
}
private void Root_OnOptionValueChange(MenuItemWithOptions options, string? oldValue, string newValue)
{
if (options.PersistentKey is null)
return;
if (oldValue is not null)
{
if (changedSettings?.TryAdd(options, oldValue) == true)
{
Log.TraceLine("ProfilesController: Saved change: {0} from {1}", options.PersistentKey, oldValue);
}
}
// If profile exists persist value
if (CurrentProfileSettings != null && (CurrentProfileSettings.Exists || AutoCreateProfiles))
{
CurrentProfileSettings.SetValue(options.PersistentKey, newValue);
options.ProfileOption = newValue;
Log.TraceLine("ProfilesController: Stored: {0} {1} = {2}",
CurrentProfileSettings.ProfileName, options.PersistentKey, newValue);
}
}
private void ProfileChanged()
{
foreach (var menuItem in MenuStack.Root.AllMenuItemOptions())
{
if (menuItem.PersistentKey is null)
continue;
menuItem.ProfileOption = CurrentProfileSettings?.GetValue(menuItem.PersistentKey);
}
}
public void CreateProfile(bool saveAll = true)
{
var profileSettings = CurrentProfileSettings;
profileSettings?.TouchFile();
Log.TraceLine("ProfilesController: Created Profile: {0}, SaveAll={1}",
profileSettings?.ProfileName, saveAll);
if (!saveAll)
return;
foreach (var menuItem in MenuStack.Root.AllMenuItemOptions())
{
if (menuItem.PersistentKey is null || menuItem.ActiveOption is null)
continue;
profileSettings?.SetValue(menuItem.PersistentKey, menuItem.ActiveOption);
}
ProfileChanged();
}
public void DeleteProfile()
{
CurrentProfileSettings?.DeleteFile();
ProfileChanged();
Log.TraceLine("ProfilesController: Deleted Profile: {0}", CurrentProfileSettings?.ProfileName);
}
private void ApplyProfile(ProfileSettings profileSettings)
{
CurrentProfileSettings = profileSettings;
ProfileChanged();
if (CurrentProfileSettings is null || CurrentProfileSettings?.Exists != true)
return;
int delay = CurrentProfileSettings.GetInt("ApplyDelay", -1);
foreach (var menuItem in MenuStack.Root.AllMenuItemOptions())
{
if (menuItem.PersistentKey is null)
continue;
var persistedValue = CurrentProfileSettings.GetValue(menuItem.PersistentKey);
if (persistedValue is null)
continue;
try
{
menuItem.Set(persistedValue, delay, true);
Log.TraceLine("ProfilesController: Applied from Profile: {0}: {1} = {2}",
CurrentProfileSettings.ProfileName, menuItem.PersistentKey, persistedValue);
}
catch (Exception e)
{
Log.TraceLine("ProfilesController: Exception Profile: {0}: {1} = {2} => {3}",
CurrentProfileSettings.ProfileName, menuItem.PersistentKey, persistedValue, e);
CurrentProfileSettings.DeleteKey(menuItem.PersistentKey);
menuItem.ProfileOption = null;
}
}
}
private void ResetProfile()
{
CurrentProfileSettings = null;
ProfileChanged();
if (changedSettings is null)
return;
// Revert all changes made to original value
var appliedSettings = changedSettings;
changedSettings = null;
foreach (var setting in appliedSettings)
{
try
{
setting.Key.Set(setting.Value);
Log.TraceLine("ProfilesController: Reset: {0} = {1} => {2}",
setting.Key.PersistentKey, setting.Value);
}
catch (Exception e)
{
Log.TraceLine("ProfilesController: Reset Exception: {0} = {1} => {2}",
setting.Key.PersistentKey, setting.Value, e);
}
}
}
}
}