diff --git a/WinJump/Core/Config.cs b/WinJump/Core/Config.cs index 033eb25..e755254 100644 --- a/WinJump/Core/Config.cs +++ b/WinJump/Core/Config.cs @@ -1,21 +1,24 @@ -using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows.Forms; -using Newtonsoft.Json; namespace WinJump.Core; /// /// Handles loading the configuration file /// -internal sealed class Config { +public sealed class Config { public static readonly int MAX_STICKY_DESKTOPS = 10; public static readonly string LOCATION = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".winjump"); + public static Config Current { get; private set; } = null!; + [JsonProperty("move-window-to")] public required List MoveWindowTo { get; set; } @@ -30,76 +33,106 @@ internal sealed class Config { [JsonProperty("change-desktops-with-scroll")] public required bool ChangeDesktopsWithScroll { get; set; } - + + [JsonProperty("scroll-invert-direction")] + public required bool ScrollInvertDirection { get; set; } + [JsonProperty("sticky-desktops")] public int StickyDesktops { get; set; } - public static Config Load() { - try { + public static Config Load() + { + try + { EnsureCreated(); string content = File.ReadAllText(LOCATION); - var config = JsonConvert.DeserializeObject(content); + var config = JsonConvert.DeserializeObject(content) ?? throw new Exception("Failed to deserialize config file"); - if(config == null) { - throw new Exception("Failed to deserialize config file"); - } + Validate(config); - // Check for jump tos with duplicate shortcuts - for(int i = 0; i < config.JumpTo.Count; i++) { - var shortcut = config.JumpTo[i].Shortcut; - for(int j = i + 1; j < config.JumpTo.Count; j++) { - if(config.JumpTo[j].Shortcut.IsEqual(shortcut)) { - throw new Exception("Duplicate jump to shortcut"); - } - - if(config.JumpTo[i].Desktop <= 0) { - throw new Exception("Invalid desktop number"); - } - } + Current = config; + return config; + } + catch + { + Current = Default(); + return Current; + } + } + private static void Validate(Config config) + { + // JumpTo duplicates + for (int i = 0; i < config.JumpTo.Count; i++) { + var shortcut = config.JumpTo[i].Shortcut; + + if (config.JumpTo[i].Desktop <= 0) + throw new Exception("Invalid desktop number"); + + for (int j = i + 1; j < config.JumpTo.Count; j++) { + if (config.JumpTo[j].Shortcut.IsEqual(shortcut)) + throw new Exception("Duplicate jump to shortcut"); } + } - // Check for toggle groups with duplicate shortcuts - for(int i = 0; i < config.ToggleGroups.Count; i++) { - var shortcut = config.ToggleGroups[i].Shortcut; - for(int j = i + 1; j < config.ToggleGroups.Count; j++) { - if(config.ToggleGroups[j].Shortcut.IsEqual(shortcut)) { - throw new Exception("Duplicate toggle group shortcut"); - } - - if(config.ToggleGroups[i].Desktops.Any((d) => d <= 0)) { - throw new Exception("Invalid desktop number"); - } - } - } + // ToggleGroups + for (int i = 0; i < config.ToggleGroups.Count; i++) { + var shortcut = config.ToggleGroups[i].Shortcut; + + if (config.ToggleGroups[i].Desktops.Any(d => d <= 0)) + throw new Exception("Invalid desktop number"); - // Check for move windows with duplicate shortcuts - for(int i = 0; i < config.MoveWindowTo.Count; i++) { - var shortcut = config.MoveWindowTo[i].Shortcut; - for(int j = i + 1; j < config.MoveWindowTo.Count; j++) { - if(config.MoveWindowTo[j].Shortcut.IsEqual(shortcut)) { - throw new Exception("Duplicate move window shortcut"); - } - - if(config.MoveWindowTo[i].Desktop <= 0) { - throw new Exception("Invalid desktop number"); - } - } + for (int j = i + 1; j < config.ToggleGroups.Count; j++) { + if (config.ToggleGroups[j].Shortcut.IsEqual(shortcut)) + throw new Exception("Duplicate toggle group shortcut"); } + } - return config; - } catch(Exception) { - return Default(); + // MoveWindowTo + for (int i = 0; i < config.MoveWindowTo.Count; i++) { + var shortcut = config.MoveWindowTo[i].Shortcut; + + if (config.MoveWindowTo[i].Desktop <= 0) + throw new Exception("Invalid desktop number"); + + for (int j = i + 1; j < config.MoveWindowTo.Count; j++) { + if (config.MoveWindowTo[j].Shortcut.IsEqual(shortcut)) + throw new Exception("Duplicate move window shortcut"); + } } } public static void EnsureCreated() { - if(File.Exists(LOCATION)) return; + var defaults = JObject.FromObject(Default()); + + // Create a file if it's not there + if (!File.Exists(LOCATION)) + { + File.WriteAllText(LOCATION, defaults.ToString(Formatting.Indented)); + return; + } - var config = Default(); - string content = JsonConvert.SerializeObject(config, Formatting.Indented); - File.WriteAllText(LOCATION, content); + //Update a file with new options + var existing = JObject.Parse(File.ReadAllText(LOCATION)); + bool changed = false; + + foreach (var prop in defaults.Properties()) + { + if (existing[prop.Name] == null) + { + existing[prop.Name] = prop.Value; + changed = true; + } + } + + if (changed) + { + File.WriteAllText( + LOCATION, + existing.ToString(Formatting.Indented) + ); + } } /// @@ -107,6 +140,7 @@ public static void EnsureCreated() { /// /// Default configuration private static Config Default() { + var jumpTo = new List(); uint desktop = 1; @@ -133,6 +167,7 @@ private static Config Default() { ToggleGroups = [], JumpCurrentGoesToLast = true, ChangeDesktopsWithScroll = false, + ScrollInvertDirection = false, StickyDesktops = 0 }; } diff --git a/WinJump/Core/WinJumpManager.cs b/WinJump/Core/WinJumpManager.cs index f4d37b1..4474b03 100644 --- a/WinJump/Core/WinJumpManager.cs +++ b/WinJump/Core/WinJumpManager.cs @@ -1,9 +1,11 @@ -using System; +using Microsoft.Win32; +using System; +using System.Runtime.InteropServices; +using System.Text; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -15,6 +17,7 @@ namespace WinJump.Core; public delegate void DesktopChanged(bool lightMode, uint desktopNum); + /// /// Ties everything together. /// @@ -24,26 +27,83 @@ namespace WinJump.Core; /// and that color scheme / taskbar created events are handled. /// public class WinJumpManager : IDisposable { + + // Structs for the Windows Taskbar position - Win32 Shell API. + public struct RECT + { + public int left, top, right, bottom; + } + public struct APPBARDATA + { + public int cbSize; + public IntPtr hWnd; + public uint uCallbackMessage; + public uint uEdge; + public RECT rc; + public int lParam; + } + /* * Internal fields */ - private readonly STAThread? _thread; // tied exactly to the lifecycle of explorer.exe private readonly ExplorerMonitor _explorerMonitor = new(); private readonly KeyboardHook _keyboardHook = new(); private readonly MouseHook _mouseHook = new(); + + #region P/Invoke Signatures + + [DllImport("user32.dll")] + static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + static extern IntPtr GetShellWindow(); + + [DllImport("user32.dll")] + static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll")] + static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("shell32.dll")] + private static extern uint SHAppBarMessage(uint dwMessage, ref APPBARDATA pData); + + #endregion + + private const uint ABM_GETSTATE = 0x00000004; + private const uint ABM_GETTASKBARPOS = 0x00000005; + + const int GWL_STYLE = -16; + const uint WS_CAPTION = 0x00C00000; // Title bar style + private bool _lightMode { get; set; } private uint _currentDesktop { get; set; } private uint? _lastDesktop { get; set; } + private const string RUN_KEY = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string APP_NAME = "WinJump"; + private uint? taskbarState = null; + private static string AppPath => + Process.GetCurrentProcess().MainModule!.FileName!; + public static bool LastLoadRequiredExplorerRestart; - public WinJumpManager(DesktopChanged desktopChanged) { - // Load config file - var config = Config.Load(); + public WinJumpManager(Config config, DesktopChanged desktopChanged) { + //// Load config file + //var config = Config.Load(); - // Attempt to register the shortcuts. + if (IsStartupEnabled()) + EnableStartup(); + else + DisableStartup(); + // Attempt to register the shortcuts. var registerShortcuts = new List> { () => { return config.JumpTo.Select(t => t.Shortcut).All(shortcut => @@ -76,6 +136,7 @@ public WinJumpManager(DesktopChanged desktopChanged) { if(config.ChangeDesktopsWithScroll) { _mouseHook.Register(); + taskbarState = GetTaskbarState(); } // Add handler for hotkey press events @@ -123,19 +184,26 @@ public WinJumpManager(DesktopChanged desktopChanged) { } }; + // Add a handler for mouse scroll events _mouseHook.MouseScrolled += (_, args) => { - // Check to make sure the mouse event happened over the taskbar - if(args.y < SystemParameters.PrimaryScreenHeight - 40) return; + //Check if the mouse event happened over the taskbar + if (args.y < SystemParameters.PrimaryScreenHeight - 40) return; + + // Check the focused window + bool bIsIt = IsActiveWindowAGame(); + System.Diagnostics.Debug.WriteLine("IsActiveWindowAGame: {0}", bIsIt); + if (bIsIt) return; int? current = _thread?.GetCurrentDesktop(); if(current != null) { - if(args.up) { - _thread?.JumpToNoHack((uint) (current.Value + 1)); - } else { - _thread?.JumpToNoHack((uint) (current.Value - 1)); - } + int delta = args.up ? 1 : -1; + + if (config.ScrollInvertDirection) + delta = -delta; + + _thread?.JumpToNoHack((uint)(current.Value + delta)); } }; @@ -176,6 +244,72 @@ public WinJumpManager(DesktopChanged desktopChanged) { }; } + public bool IsActiveWindowAGame() + { + IntPtr hWnd = GetForegroundWindow(); + + if (hWnd == IntPtr.Zero) return false; + + // edge case, if the focused window is the desktop or taskbar, it's not a game + if (hWnd == GetShellWindow() || hWnd == GetDesktopWindow()) return false; + + // edge case, Windows uses 'WorkerW' or 'Progman' for the wallpaper/icons + // These always report as full-screen size, so we must filter them by class name. + StringBuilder className = new StringBuilder(256); + GetClassName(hWnd, className, 256); + string cName = className.ToString(); + if (cName == "WorkerW" || cName == "Progman") return false; + + // Check if it covers the screen (maximised & borderless) + GetWindowRect(hWnd, out RECT rect); + var screen = System.Windows.Forms.Screen.FromHandle(hWnd); + bool isFullScreenSize = (rect.right - rect.left >= screen.Bounds.Width && + rect.bottom - rect.top >= screen.Bounds.Height); + + if (!isFullScreenSize) return false; + + // Goofy check: Check window style since games apparently lack a Title Bar WS_CAPTION + long style = GetWindowLong(hWnd, GWL_STYLE); + bool hasNoTitleBar = (style & WS_CAPTION) == 0; + + // If it fills the screen and has no title bar, its a game or a full video + return hasNoTitleBar; + } + + + /* + * Returns 1 if taskbar auto-hidden, 0 otherwise. + */ + private static uint? GetTaskbarState() + { + var data = new APPBARDATA + { + cbSize = Marshal.SizeOf() + }; + + var appBarMessage = SHAppBarMessage(ABM_GETSTATE, ref data); + System.Diagnostics.Debug.WriteLine("appBarState, state: {0}", appBarMessage); + return appBarMessage; + } + + public static void EnableStartup() + { + using var key = Registry.CurrentUser.OpenSubKey(RUN_KEY, true); + key!.SetValue(APP_NAME, $"\"{AppPath}\""); + } + + public static void DisableStartup() + { + using var key = Registry.CurrentUser.OpenSubKey(RUN_KEY, true); + key!.DeleteValue(APP_NAME, false); + } + + public static bool IsStartupEnabled() + { + using var key = Registry.CurrentUser.OpenSubKey(RUN_KEY, false); + return key?.GetValue(APP_NAME) != null; + } + public void Dispose() { _thread?.Dispose(); _explorerMonitor.Dispose(); diff --git a/WinJump/UI/App.xaml.cs b/WinJump/UI/App.xaml.cs index 79ca527..b8cf70d 100644 --- a/WinJump/UI/App.xaml.cs +++ b/WinJump/UI/App.xaml.cs @@ -25,7 +25,9 @@ protected override void OnStartup(StartupEventArgs e) { return; } - if(FindResource("NotifyIcon") is TaskbarIcon icon) { + var config = Config.Load(); // fix config.current + + if (FindResource("NotifyIcon") is TaskbarIcon icon) { notifyIcon = icon; } else { throw new Exception("Could not find NotifyIcon"); @@ -37,7 +39,7 @@ protected override void OnStartup(StartupEventArgs e) { var lightIcons = new Dictionary(); var darkIcons = new Dictionary(); - for(uint i = 1; i <= 16; i++) { + for (uint i = 1; i <= 16; i++) { string fileName = i > 15 ? "15+.ico" : $"{i}.ico"; var lightFileInfo = embeddedProvider.GetFileInfo("UI/Icons/Light/" + fileName); @@ -50,7 +52,7 @@ protected override void OnStartup(StartupEventArgs e) { darkIcons.Add(i, new Icon(darkStream)); } - manager = new WinJumpManager( + manager = new WinJumpManager(config, (lightMode, desktopIcon) => { notifyIcon.Icon = lightMode ? darkIcons[desktopIcon + 1] : lightIcons[desktopIcon + 1]; }); diff --git a/WinJump/UI/Icons/app.ico b/WinJump/UI/Icons/app.ico new file mode 100644 index 0000000..8640ad0 Binary files /dev/null and b/WinJump/UI/Icons/app.ico differ diff --git a/WinJump/UI/TrayModel.cs b/WinJump/UI/TrayModel.cs index 57e1eb3..c005567 100644 --- a/WinJump/UI/TrayModel.cs +++ b/WinJump/UI/TrayModel.cs @@ -1,14 +1,43 @@ -using System; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; +using System; +using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Reflection; using System.Windows; using System.Windows.Input; -using Microsoft.Win32; using WinJump.Core; namespace WinJump.UI; -public class TrayModel { +public class TrayModel : INotifyPropertyChanged{ + + public event PropertyChangedEventHandler? PropertyChanged; + + public static bool StartWithWindows => WinJumpManager.IsStartupEnabled(); + + public ICommand ToggleStartWithWindows => new DelegateCommand + { + CanExecuteFunc = () => true, + CommandAction = () => + { + if (WinJumpManager.IsStartupEnabled()) + { + WinJumpManager.DisableStartup(); + } + else + { + WinJumpManager.EnableStartup(); + } + + PropertyChanged?.Invoke( + this, + new PropertyChangedEventArgs(nameof(StartWithWindows)) + ); + } + }; + public ICommand OnOpenConfig => new DelegateCommand { CanExecuteFunc = () => true, CommandAction = () => { @@ -52,6 +81,25 @@ public class TrayModel { } }; + public ICommand OnOpenLocation => new DelegateCommand + { + CanExecuteFunc = () => true, + CommandAction = () => { + Config.EnsureCreated(); + + if (!File.Exists(Config.LOCATION)) + return; + + Process.Start(new ProcessStartInfo + { + FileName = "explorer.exe", + Arguments = $"/select,\"{Config.LOCATION}\"", + UseShellExecute = true + }); + + } + }; + public ICommand Exit => new DelegateCommand { CanExecuteFunc = () => true, CommandAction = () => { @@ -61,7 +109,6 @@ public class TrayModel { var killExplorer = Process.Start("cmd.exe", "/c taskkill /f /im explorer.exe"); killExplorer.WaitForExit(); - Process.Start(Environment.SystemDirectory + "\\..\\explorer.exe"); } diff --git a/WinJump/UI/TrayResources.xaml b/WinJump/UI/TrayResources.xaml index fcbfd85..7822623 100644 --- a/WinJump/UI/TrayResources.xaml +++ b/WinJump/UI/TrayResources.xaml @@ -6,18 +6,26 @@ - + + + + + + \ No newline at end of file diff --git a/WinJump/WinJump.csproj b/WinJump/WinJump.csproj index ccaab58..7e44ec9 100644 --- a/WinJump/WinJump.csproj +++ b/WinJump/WinJump.csproj @@ -8,6 +8,7 @@ 2.0.15 2.0.15 2.0.15 + UI\Icons\app.ico @@ -114,6 +115,7 @@ +