diff --git a/.gitignore b/.gitignore index 87222a1..5913fd0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build-Debug/ .vscode/ scripts/Redist/ SteamDeckTools_Setup*.exe +UserProfiles/ diff --git a/CommonHelpers/BaseSettings.cs b/CommonHelpers/BaseSettings.cs index 0ac0782..bd4ba57 100644 --- a/CommonHelpers/BaseSettings.cs +++ b/CommonHelpers/BaseSettings.cs @@ -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); diff --git a/CommonHelpers/RTSS.cs b/CommonHelpers/RTSS.cs index d133723..fa74ac1 100644 --- a/CommonHelpers/RTSS.cs +++ b/CommonHelpers/RTSS.cs @@ -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 IDs { get; } = new Dictionary(); + + 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 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; } diff --git a/PowerControl/Controller.cs b/PowerControl/Controller.cs index acd9ccb..ca8cc3f 100644 --- a/PowerControl/Controller.cs +++ b/PowerControl/Controller.cs @@ -33,7 +33,7 @@ namespace PowerControl DateTime? neptuneDeviceNextKey; System.Windows.Forms.Timer neptuneTimer; - ProfilesController profilesController; + ProfilesController? profilesController; SharedData sharedData = SharedData.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(); } diff --git a/PowerControl/Helpers/ProfileSettings.cs b/PowerControl/Helpers/ProfileSettings.cs index a064058..09bc137 100644 --- a/PowerControl/Helpers/ProfileSettings.cs +++ b/PowerControl/Helpers/ProfileSettings.cs @@ -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(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(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); } } } diff --git a/PowerControl/Helpers/ProfilesController.cs b/PowerControl/Helpers/ProfilesController.cs deleted file mode 100644 index 1231052..0000000 --- a/PowerControl/Helpers/ProfilesController.cs +++ /dev/null @@ -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); - } - } -} diff --git a/PowerControl/Menu/MenuItemWithOptions.cs b/PowerControl/Menu/MenuItemWithOptions.cs index a25c34a..3d1a4bd 100644 --- a/PowerControl/Menu/MenuItemWithOptions.cs +++ b/PowerControl/Menu/MenuItemWithOptions.cs @@ -5,6 +5,7 @@ namespace PowerControl.Menu public IList Options { get; set; } = new List(); 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; } diff --git a/PowerControl/MenuStack.cs b/PowerControl/MenuStack.cs index 22de21b..09fb916 100644 --- a/PowerControl/MenuStack.cs +++ b/PowerControl/MenuStack.cs @@ -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(), diff --git a/PowerControl/Options/Profiles.cs b/PowerControl/Options/Profiles.cs new file mode 100644 index 0000000..4591101 --- /dev/null +++ b/PowerControl/Options/Profiles.cs @@ -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(); + } + }; + } +} \ No newline at end of file diff --git a/PowerControl/Options/RefreshRate.cs b/PowerControl/Options/RefreshRate.cs index fe52519..bb29a81 100644 --- a/PowerControl/Options/RefreshRate.cs +++ b/PowerControl/Options/RefreshRate.cs @@ -25,7 +25,7 @@ namespace PowerControl.Options ApplyValue = (selected) => { DisplayResolutionController.SetRefreshRate(int.Parse(selected)); - + return DisplayResolutionController.GetRefreshRate().ToString(); }, AfterApply = () => diff --git a/PowerControl/ProfilesController.cs b/PowerControl/ProfilesController.cs new file mode 100644 index 0000000..58ba262 --- /dev/null +++ b/PowerControl/ProfilesController.cs @@ -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 watchedProcesses = new Dictionary(); + private Dictionary? changedSettings; + + private System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer() + { + Interval = 1000 + }; + + public IEnumerable 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(); + + 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); + } + } + } + } +}