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 @@
+