diff --git a/WinJump/Config.cs b/WinJump/Config.cs new file mode 100644 index 0000000..189e002 --- /dev/null +++ b/WinJump/Config.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows.Forms; +using Newtonsoft.Json; + +namespace WinJump { + internal sealed class Config { + [JsonProperty("toggle-groups")] + public List ToggleGroups { get; set; } + + [JsonProperty("jump-to")] + public List JumpTo { get; set; } + + public static Config FromFile(string path) { + try { + string content = File.ReadAllText(path); + + var config = JsonConvert.DeserializeObject(content); + + // 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"); + } + } + } + + // 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"); + } + } + } + + return config; + } catch (Exception) { + return Default(); + } + } + + private static Config Default() { + var jumpTo = new List(); + + for (var k = Keys.D0; k <= Keys.D9; k++) { + jumpTo.Add(new JumpTo() { + Shortcut = new Shortcut() { + ModifierKeys = ModifierKeys.Win, + Keys = k + } + }); + } + + return new Config { + JumpTo = jumpTo, + ToggleGroups = new List() + }; + } + } + + public sealed class ToggleGroup { + [JsonConverter(typeof(ShortcutConverter))] + public Shortcut Shortcut { get; set; } + + public List Desktops { get; set; } + + public bool IsEqual(ToggleGroup other) { + return Shortcut.IsEqual(other.Shortcut); + } + } + + public sealed class JumpTo { + [JsonConverter(typeof(ShortcutConverter))] + public Shortcut Shortcut { get; set; } + + public int Desktop { get; set; } + + public bool IsEqual(JumpTo other) { + return Shortcut.IsEqual(other.Shortcut); + } + } + + public sealed class Shortcut { + public ModifierKeys ModifierKeys { get; set; } + public Keys Keys { get; set; } + + public bool IsEqual(Shortcut other) { + return ModifierKeys == other.ModifierKeys && Keys == other.Keys; + } + } + + public sealed class ShortcutConverter : JsonConverter { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) { + string expression = reader.Value?.ToString() ?? throw new Exception("Invalid shortcut"); + + var stack = new Queue(expression.Split('+')); + + ModifierKeys modifiers = 0; + + var lookup = new Dictionary { + {"ctrl", ModifierKeys.Control}, + {"alt", ModifierKeys.Alt}, + {"shift", ModifierKeys.Shift}, + {"win", ModifierKeys.Win} + }; + + while (stack.Count > 0) { + string token = stack.Dequeue(); + + if (lookup.ContainsKey(token)) { + modifiers |= lookup[token]; + } else { + return new Shortcut { + ModifierKeys = modifiers, + Keys = (Keys) Enum.Parse(typeof(Keys), token, true) + }; + } + } + + throw new Exception($"Invalid shortcut: {expression}"); + } + + public override bool CanConvert(Type objectType) { + return objectType == typeof(string); + } + } +} \ No newline at end of file diff --git a/WinJump/KeyboardHook.cs b/WinJump/KeyboardHook.cs index a381fb4..e92c723 100644 --- a/WinJump/KeyboardHook.cs +++ b/WinJump/KeyboardHook.cs @@ -39,8 +39,7 @@ protected override void WndProc(ref Message m) { ModifierKeys modifier = (ModifierKeys) ((int) m.LParam & 0xFFFF); // invoke the event to notify the parent. - if (KeyPressed != null) - KeyPressed(this, new KeyPressedEventArgs(modifier, key)); + KeyPressed?.Invoke(this, new KeyPressedEventArgs(modifier, key)); } } @@ -75,7 +74,7 @@ public void RegisterHotKey(ModifierKeys modifier, Keys key) { _currentId++; // register the hot key. - if (!RegisterHotKey(_window.Handle, _currentId, (uint) (modifier | ModifierKeys.NoRepeat), (uint) key)) + if (!RegisterHotKey(_window.Handle, _currentId, (uint)(modifier | ModifierKeys.NoRepeat), (uint) key)) throw new InvalidOperationException("Could not register the hot key."); } diff --git a/WinJump/Program.cs b/WinJump/Program.cs index fcee889..92eefeb 100644 --- a/WinJump/Program.cs +++ b/WinJump/Program.cs @@ -1,61 +1,85 @@ using System; using System.Diagnostics; -using System.Runtime.InteropServices; +using System.IO; +using System.Linq; using System.Threading; using System.Windows.Forms; +using Microsoft.Win32; using VirtualDesktop.VirtualDesktop; namespace WinJump { internal static class Program { - [DllImport("user32.dll")] - private static extern bool SetForegroundWindow(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - private static extern bool IsWindow(IntPtr hWnd); - [STAThread] public static void Main() { + // Register win jump to launch at startup + // The path to the key where Windows looks for startup applications + RegistryKey startupApp = Registry.CurrentUser.OpenSubKey( + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true); + + //Path to launch shortcut + string startPath = Environment.GetFolderPath(Environment.SpecialFolder.Programs) + + @"\WinJump\WinJump.appref-ms"; + + startupApp?.SetValue("WinJump", startPath); + + // Load config file + var config = Config.FromFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".winjump")); + // Because windows_key + number is already a bulit in Windows shortcut, we need to kill explorer // (explorer is the process that registers the shortcut) so that it releases the shortcut. // Then, we can register it and restart explorer. var killExplorer = Process.Start("cmd.exe", "/c taskkill /f /im explorer.exe"); killExplorer?.WaitForExit(); - + var thread = new STAThread(); // Start a thread that can handle UI requests KeyboardHook hook = new KeyboardHook(); - + hook.KeyPressed += (sender, args) => { - if (args.Key < Keys.D0 || args.Key > Keys.D9 || args.Modifier != ModifierKeys.Win) return; + Shortcut pressed = new Shortcut { + ModifierKeys = args.Modifier, + Keys = args.Key + }; + + // First, scan for jump to shortcuts + JumpTo jumpTo = config.JumpTo.FirstOrDefault(x => x.Shortcut.IsEqual(pressed)); + + if (jumpTo != null) { + thread.JumpTo(jumpTo.Desktop - 1); + return; + } - int index = args.Key == Keys.D0 ? 10 : (args.Key - Keys.D1); - thread.JumpTo(index); + ToggleGroup toggleGroup = config.ToggleGroups.FirstOrDefault(x => x.Shortcut.IsEqual(pressed)); + + if (toggleGroup != null) { + thread.JumpToNext(toggleGroup.Desktops.Select(x => x - 1).ToArray()); + } }; + + // Register the shortcuts + foreach (var shortcut in config.JumpTo.Select(t => t.Shortcut)) { + hook.RegisterHotKey(shortcut.ModifierKeys, shortcut.Keys); + } - for(var key = Keys.D0; key <= Keys.D9; key++) { - hook.RegisterHotKey(ModifierKeys.Win, key); + // Register the toggle groups + foreach (var shortcut in config.ToggleGroups.Select(t => t.Shortcut)) { + hook.RegisterHotKey(shortcut.ModifierKeys, shortcut.Keys); } Process.Start(Environment.SystemDirectory + "\\..\\explorer.exe"); - - AppDomain.CurrentDomain.ProcessExit += (s, e) => - { - hook.Dispose(); - }; - + + AppDomain.CurrentDomain.ProcessExit += (s, e) => { hook.Dispose(); }; + Application.Run(); } - + // Credit https://stackoverflow.com/a/21684059/4779937 private sealed class STAThread : IDisposable { - private IntPtr[] LastActiveWindow = new IntPtr[10]; private readonly VirtualDesktopWrapper vdw = VirtualDesktopManager.Create(); - + public STAThread() { using (mre = new ManualResetEvent(false)) { var thread = new Thread(() => { @@ -69,47 +93,52 @@ public STAThread() { mre.WaitOne(); } } + public void BeginInvoke(Delegate dlg, params Object[] args) { if (ctx == null) throw new ObjectDisposedException("STAThread"); - ctx.Post((_) => dlg.DynamicInvoke(args), null); + ctx.Post(_ => dlg.DynamicInvoke(args), null); } + + // index must be 0-indexed public void JumpTo(int index) { if (ctx == null) throw new ObjectDisposedException("STAThread"); - + ctx.Send((_) => { - // Before we go to a new Window, save the foreground Window - LastActiveWindow[vdw.GetDesktop()] = GetForegroundWindow(); - vdw.JumpTo(index); - // Give it just a little time to let the desktop settle - Thread.Sleep(50); - - if(LastActiveWindow[index] != IntPtr.Zero) { - // Check if the window still exists (it might have been closed) - if (IsWindow(LastActiveWindow[index])) { - SetForegroundWindow(LastActiveWindow[index]); - } else { - LastActiveWindow[index] = IntPtr.Zero; - } - } }, null); } + // desktops must be 0-indexed + public void JumpToNext(int[] desktops) { + + if (ctx == null) throw new ObjectDisposedException("STAThread"); + + ctx.Send(_ => { + int index = Array.FindIndex(desktops, x => x == vdw.GetDesktop()); + if (index < 0) index = 0; + int next = desktops[(index + 1) % desktops.Length]; + + vdw.JumpTo(next); + + }, null); + + } + private void Initialize(object sender, EventArgs e) { ctx = SynchronizationContext.Current; mre.Set(); Application.Idle -= Initialize; } + public void Dispose() { if (ctx == null) return; - - ctx.Send((_) => Application.ExitThread(), null); + + ctx.Send(_ => Application.ExitThread(), null); ctx = null; } private SynchronizationContext ctx; private readonly ManualResetEvent mre; } - } -} +} \ No newline at end of file diff --git a/WinJump/Properties/AssemblyInfo.cs b/WinJump/Properties/AssemblyInfo.cs index f3c4652..7af7a78 100644 --- a/WinJump/Properties/AssemblyInfo.cs +++ b/WinJump/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.4.0")] +[assembly: AssemblyFileVersion("1.0.4.0")] \ No newline at end of file diff --git a/WinJump/WinJump.csproj b/WinJump/WinJump.csproj index 02196ca..759c21d 100644 --- a/WinJump/WinJump.csproj +++ b/WinJump/WinJump.csproj @@ -12,6 +12,7 @@ 512 true true + false publish\ true Disk @@ -22,9 +23,8 @@ false false true - 0 + 2 1.0.0.%2a - false false true true @@ -61,6 +61,9 @@ true + + ..\packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll + @@ -74,6 +77,7 @@ + @@ -86,6 +90,7 @@ True Resources.resx + SettingsSingleFileGenerator Settings.Designer.cs