Skip to content

Commit

Permalink
- Added .winjump config file support that now allows users to customi…
Browse files Browse the repository at this point in the history
…ze the jump shortcuts as well as create toggle groups as discussed in #1

- Cleanup setup process to automatically register as a startup application
- Seemingly fixed the weird glitch that would steal focus when moving between desktops?
  • Loading branch information
widavies committed Feb 17, 2023
1 parent aa1f81a commit c0c47f1
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 53 deletions.
137 changes: 137 additions & 0 deletions WinJump/Config.cs
Original file line number Diff line number Diff line change
@@ -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<ToggleGroup> ToggleGroups { get; set; }

[JsonProperty("jump-to")]
public List<JumpTo> JumpTo { get; set; }

public static Config FromFile(string path) {
try {
string content = File.ReadAllText(path);

var config = JsonConvert.DeserializeObject<Config>(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<JumpTo>();

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<ToggleGroup>()
};
}
}

public sealed class ToggleGroup {
[JsonConverter(typeof(ShortcutConverter))]
public Shortcut Shortcut { get; set; }

public List<int> 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<string>(expression.Split('+'));

ModifierKeys modifiers = 0;

var lookup = new Dictionary<string, ModifierKeys> {
{"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);
}
}
}
5 changes: 2 additions & 3 deletions WinJump/KeyboardHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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.");
}

Expand Down
121 changes: 75 additions & 46 deletions WinJump/Program.cs
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand All @@ -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;
}

}
}
}
4 changes: 2 additions & 2 deletions WinJump/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
[assembly: AssemblyVersion("1.0.4.0")]
[assembly: AssemblyFileVersion("1.0.4.0")]
9 changes: 7 additions & 2 deletions WinJump/WinJump.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
Expand All @@ -22,9 +23,8 @@
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationRevision>2</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<PublishWizardCompleted>true</PublishWizardCompleted>
<BootstrapperEnabled>true</BootstrapperEnabled>
Expand Down Expand Up @@ -61,6 +61,9 @@
<SignManifests>true</SignManifests>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
Expand All @@ -74,6 +77,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Config.cs" />
<Compile Include="KeyboardHook.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
Expand All @@ -86,6 +90,7 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
Expand Down

0 comments on commit c0c47f1

Please sign in to comment.