diff --git a/ClipForge/App.xaml b/ClipForge/App.xaml
index b3f8d56..d741abd 100644
--- a/ClipForge/App.xaml
+++ b/ClipForge/App.xaml
@@ -8,9 +8,44 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ClipForge/ClipForge.csproj b/ClipForge/ClipForge.csproj
index af21841..41618fa 100644
--- a/ClipForge/ClipForge.csproj
+++ b/ClipForge/ClipForge.csproj
@@ -18,12 +18,12 @@
4A55954F2A73A9D620442C7DFBFC7C95A71D8D24
SHA256
True
- C:\Users\Blade\Desktop\Clipforge Packaged\V1\
+ C:\Users\Blade\Desktop\Clipforge Packaged\V0.1\
False
True
Auto
x64
- J:\Projects\ClipForge\ClipForge\bin\x64\Release\net8.0-windows10.0.19041.0
+ C:\Users\Blade\Desktop\Clipforge Packaged
0
diff --git a/ClipForge/GlobalHotkeyService.cs b/ClipForge/GlobalHotkeyService.cs
index d786b51..551cb5e 100644
--- a/ClipForge/GlobalHotkeyService.cs
+++ b/ClipForge/GlobalHotkeyService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Runtime.InteropServices;
using Microsoft.UI.Xaml;
@@ -6,39 +6,49 @@ namespace ClipForge
{
public class GlobalHotkeyService
{
- // These two lines talk directly to Windows to register/unregister hotkeys
[DllImport("user32.dll")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
- // This is the ID we'll use to identify our clip hotkey
private const int HOTKEY_CLIP = 1;
+ private const uint MOD_NOREPEAT = 0x4000; // don't fire on key repeat
- // Alt key modifier
- private const uint MOD_ALT = 0x0001;
-
- // F9 key
- private const uint VK_F9 = 0x78;
-
- // This is the "event" that fires when the hotkey is pressed
public event Action? ClipRequested;
private IntPtr _hwnd;
+ private uint _modifiers;
+ private uint _vk;
- public void Initialize(IntPtr hwnd)
+ /// Register the clip hotkey. Use settings values (e.g. HotkeyModifiers, HotkeyVirtualKey).
+ public void Initialize(IntPtr hwnd, uint modifiers, uint vk)
{
_hwnd = hwnd;
- RegisterHotKey(_hwnd, HOTKEY_CLIP, MOD_ALT, VK_F9);
+ _modifiers = modifiers;
+ _vk = vk;
+ TryRegister();
+ }
+
+ /// Update the hotkey at runtime (e.g. after user remaps). Unregisters old and registers new.
+ public bool UpdateHotkey(uint modifiers, uint vk)
+ {
+ UnregisterHotKey(_hwnd, HOTKEY_CLIP);
+ _modifiers = modifiers;
+ _vk = vk;
+ return TryRegister();
+ }
+
+ private bool TryRegister()
+ {
+ uint mod = _modifiers | MOD_NOREPEAT;
+ return RegisterHotKey(_hwnd, HOTKEY_CLIP, mod, _vk);
}
public void ProcessHotkey(int id)
{
if (id == HOTKEY_CLIP)
- {
ClipRequested?.Invoke();
- }
}
public void Cleanup()
diff --git a/ClipForge/HotkeyHelper.cs b/ClipForge/HotkeyHelper.cs
new file mode 100644
index 0000000..8b88a08
--- /dev/null
+++ b/ClipForge/HotkeyHelper.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using Windows.System;
+
+namespace ClipForge
+{
+ /// Converts between hotkey modifier/vk and display strings.
+ public static class HotkeyHelper
+ {
+ public const uint MOD_ALT = 0x0001;
+ public const uint MOD_CONTROL = 0x0002;
+ public const uint MOD_SHIFT = 0x0004;
+ public const uint MOD_WIN = 0x0008;
+ public const uint MOD_NOREPEAT = 0x4000;
+
+ // Built in static ctor with TryAdd so duplicate VirtualKey enum values (e.g. Kana/Hangul = 0x15) don't throw.
+ private static readonly Dictionary KeyNames = new();
+
+ static HotkeyHelper()
+ {
+ TryAdd(VirtualKey.LeftButton, "LMB");
+ TryAdd(VirtualKey.RightButton, "RMB");
+ TryAdd(VirtualKey.Cancel, "Cancel");
+ TryAdd(VirtualKey.Back, "Backspace");
+ TryAdd(VirtualKey.Tab, "Tab");
+ TryAdd(VirtualKey.Clear, "Clear");
+ TryAdd(VirtualKey.Enter, "Enter");
+ TryAdd(VirtualKey.Shift, "Shift");
+ TryAdd(VirtualKey.Control, "Ctrl");
+ TryAdd(VirtualKey.Menu, "Alt");
+ TryAdd(VirtualKey.Pause, "Pause");
+ TryAdd(VirtualKey.CapitalLock, "Caps Lock");
+ TryAdd(VirtualKey.Kana, "Kana");
+ TryAdd(VirtualKey.Hangul, "Hangul"); // same value as Kana on some SDKs; TryAdd skips duplicate
+ TryAdd(VirtualKey.Junja, "Junja");
+ TryAdd(VirtualKey.Final, "Final");
+ TryAdd(VirtualKey.Hanja, "Hanja");
+ TryAdd(VirtualKey.Kanji, "Kanji");
+ TryAdd(VirtualKey.Escape, "Escape");
+ TryAdd(VirtualKey.Convert, "Convert");
+ TryAdd(VirtualKey.NonConvert, "NonConvert");
+ TryAdd(VirtualKey.Accept, "Accept");
+ TryAdd(VirtualKey.ModeChange, "ModeChange");
+ TryAdd(VirtualKey.Space, "Space");
+ TryAdd(VirtualKey.PageUp, "PgUp");
+ TryAdd(VirtualKey.PageDown, "PgDn");
+ TryAdd(VirtualKey.End, "End");
+ TryAdd(VirtualKey.Home, "Home");
+ TryAdd(VirtualKey.Left, "Left");
+ TryAdd(VirtualKey.Up, "Up");
+ TryAdd(VirtualKey.Right, "Right");
+ TryAdd(VirtualKey.Down, "Down");
+ TryAdd(VirtualKey.Select, "Select");
+ TryAdd(VirtualKey.Print, "Print");
+ TryAdd(VirtualKey.Execute, "Execute");
+ TryAdd(VirtualKey.Snapshot, "Print Screen");
+ TryAdd(VirtualKey.Insert, "Insert");
+ TryAdd(VirtualKey.Delete, "Delete");
+ TryAdd(VirtualKey.Help, "Help");
+ TryAdd(VirtualKey.Number0, "0");
+ TryAdd(VirtualKey.Number1, "1");
+ TryAdd(VirtualKey.Number2, "2");
+ TryAdd(VirtualKey.Number3, "3");
+ TryAdd(VirtualKey.Number4, "4");
+ TryAdd(VirtualKey.Number5, "5");
+ TryAdd(VirtualKey.Number6, "6");
+ TryAdd(VirtualKey.Number7, "7");
+ TryAdd(VirtualKey.Number8, "8");
+ TryAdd(VirtualKey.Number9, "9");
+ TryAdd(VirtualKey.A, "A");
+ TryAdd(VirtualKey.B, "B");
+ TryAdd(VirtualKey.C, "C");
+ TryAdd(VirtualKey.D, "D");
+ TryAdd(VirtualKey.E, "E");
+ TryAdd(VirtualKey.F, "F");
+ TryAdd(VirtualKey.G, "G");
+ TryAdd(VirtualKey.H, "H");
+ TryAdd(VirtualKey.I, "I");
+ TryAdd(VirtualKey.J, "J");
+ TryAdd(VirtualKey.K, "K");
+ TryAdd(VirtualKey.L, "L");
+ TryAdd(VirtualKey.M, "M");
+ TryAdd(VirtualKey.N, "N");
+ TryAdd(VirtualKey.O, "O");
+ TryAdd(VirtualKey.P, "P");
+ TryAdd(VirtualKey.Q, "Q");
+ TryAdd(VirtualKey.R, "R");
+ TryAdd(VirtualKey.S, "S");
+ TryAdd(VirtualKey.T, "T");
+ TryAdd(VirtualKey.U, "U");
+ TryAdd(VirtualKey.V, "V");
+ TryAdd(VirtualKey.W, "W");
+ TryAdd(VirtualKey.X, "X");
+ TryAdd(VirtualKey.Y, "Y");
+ TryAdd(VirtualKey.Z, "Z");
+ TryAdd(VirtualKey.LeftWindows, "Win");
+ TryAdd(VirtualKey.RightWindows, "Win");
+ TryAdd(VirtualKey.Application, "App");
+ TryAdd(VirtualKey.Sleep, "Sleep");
+ TryAdd(VirtualKey.NumberPad0, "Num 0");
+ TryAdd(VirtualKey.NumberPad1, "Num 1");
+ TryAdd(VirtualKey.NumberPad2, "Num 2");
+ TryAdd(VirtualKey.NumberPad3, "Num 3");
+ TryAdd(VirtualKey.NumberPad4, "Num 4");
+ TryAdd(VirtualKey.NumberPad5, "Num 5");
+ TryAdd(VirtualKey.NumberPad6, "Num 6");
+ TryAdd(VirtualKey.NumberPad7, "Num 7");
+ TryAdd(VirtualKey.NumberPad8, "Num 8");
+ TryAdd(VirtualKey.NumberPad9, "Num 9");
+ TryAdd(VirtualKey.Multiply, "Num *");
+ TryAdd(VirtualKey.Add, "Num +");
+ TryAdd(VirtualKey.Separator, "Num ,");
+ TryAdd(VirtualKey.Subtract, "Num -");
+ TryAdd(VirtualKey.Decimal, "Num .");
+ TryAdd(VirtualKey.Divide, "Num /");
+ TryAdd(VirtualKey.F1, "F1");
+ TryAdd(VirtualKey.F2, "F2");
+ TryAdd(VirtualKey.F3, "F3");
+ TryAdd(VirtualKey.F4, "F4");
+ TryAdd(VirtualKey.F5, "F5");
+ TryAdd(VirtualKey.F6, "F6");
+ TryAdd(VirtualKey.F7, "F7");
+ TryAdd(VirtualKey.F8, "F8");
+ TryAdd(VirtualKey.F9, "F9");
+ TryAdd(VirtualKey.F10, "F10");
+ TryAdd(VirtualKey.F11, "F11");
+ TryAdd(VirtualKey.F12, "F12");
+ TryAdd(VirtualKey.F13, "F13");
+ TryAdd(VirtualKey.F14, "F14");
+ TryAdd(VirtualKey.F15, "F15");
+ TryAdd(VirtualKey.F16, "F16");
+ TryAdd(VirtualKey.F17, "F17");
+ TryAdd(VirtualKey.F18, "F18");
+ TryAdd(VirtualKey.F19, "F19");
+ TryAdd(VirtualKey.F20, "F20");
+ TryAdd(VirtualKey.F21, "F21");
+ TryAdd(VirtualKey.F22, "F22");
+ TryAdd(VirtualKey.F23, "F23");
+ TryAdd(VirtualKey.F24, "F24");
+ TryAdd(VirtualKey.NumberKeyLock, "NumLock");
+ TryAdd(VirtualKey.Scroll, "Scroll Lock");
+ TryAdd(VirtualKey.LeftShift, "Shift");
+ TryAdd(VirtualKey.RightShift, "Shift");
+ TryAdd(VirtualKey.LeftControl, "Ctrl");
+ TryAdd(VirtualKey.RightControl, "Ctrl");
+ TryAdd(VirtualKey.LeftMenu, "Alt");
+ TryAdd(VirtualKey.RightMenu, "Alt");
+ }
+
+ private static void TryAdd(VirtualKey key, string name)
+ {
+ KeyNames.TryAdd(key, name);
+ }
+
+ /// Format modifier flags + virtual key as display string (e.g. "Alt + PgUp").
+ public static string ToDisplayString(uint modifiers, uint vk)
+ {
+ var parts = new List();
+ if ((modifiers & MOD_WIN) != 0) parts.Add("Win");
+ if ((modifiers & MOD_CONTROL) != 0) parts.Add("Ctrl");
+ if ((modifiers & MOD_ALT) != 0) parts.Add("Alt");
+ if ((modifiers & MOD_SHIFT) != 0) parts.Add("Shift");
+ var keyName = GetKeyName((VirtualKey)vk);
+ if (!string.IsNullOrEmpty(keyName))
+ parts.Add(keyName);
+ return parts.Count > 0 ? string.Join(" + ", parts) : "None";
+ }
+
+ public static string GetKeyName(VirtualKey key)
+ {
+ return KeyNames.TryGetValue(key, out var name) ? name : key.ToString();
+ }
+
+ /// True if the key is typically used only as a modifier (don't use as sole key).
+ public static bool IsModifierKey(VirtualKey key)
+ {
+ return key == VirtualKey.LeftWindows || key == VirtualKey.RightWindows
+ || key == VirtualKey.Control || key == VirtualKey.LeftControl || key == VirtualKey.RightControl
+ || key == VirtualKey.Menu || key == VirtualKey.LeftMenu || key == VirtualKey.RightMenu
+ || key == VirtualKey.Shift || key == VirtualKey.LeftShift || key == VirtualKey.RightShift;
+ }
+ }
+}
diff --git a/ClipForge/MainWindow.xaml b/ClipForge/MainWindow.xaml
index 3e3959c..bf013f1 100644
--- a/ClipForge/MainWindow.xaml
+++ b/ClipForge/MainWindow.xaml
@@ -13,56 +13,71 @@
-
+
-
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
@@ -102,37 +123,44 @@
-
+
+ FontSize="26"
+ FontWeight="Bold"
+ Foreground="{StaticResource ClipForgeTextPrimaryBrush}"/>
+ Margin="0,0,0,4"/>
-
+
-
+
@@ -143,11 +171,11 @@
+ CornerRadius="10">
@@ -156,8 +184,8 @@
+ Background="{StaticResource ClipForgeSidebarBrush}"
+ CornerRadius="10,10,0,0">
+ Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
-
-
+
+
+ FontSize="26"
+ FontWeight="Bold"
+ Foreground="{StaticResource ClipForgeTextPrimaryBrush}"/>
-
+ Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
@@ -289,25 +330,27 @@
-
+ Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
@@ -327,25 +370,27 @@
-
+ Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
@@ -355,61 +400,66 @@
-
-
-
+
-
-
-
+ MinWidth="140"
+ FontFamily="Consolas"
+ FontSize="14"
+ FontWeight="Bold"
+ Foreground="{StaticResource ClipForgeAccentBrush}"
+ Content="Alt + F9"/>
-
+ Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
diff --git a/ClipForge/MainWindow.xaml.cs b/ClipForge/MainWindow.xaml.cs
index 4320aad..4a82457 100644
--- a/ClipForge/MainWindow.xaml.cs
+++ b/ClipForge/MainWindow.xaml.cs
@@ -1,10 +1,12 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.Win32;
using System;
using System.Linq;
using System.Runtime.InteropServices;
+using Windows.System;
using WinRT.Interop;
namespace ClipForge
@@ -16,6 +18,7 @@ namespace ClipForge
private ClipLibraryService _clipLibrary;
private SettingsService _settingsService;
private ThumbnailService _thumbnailService;
+ private bool _isRecordingHotkey;
private delegate IntPtr WinProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
private WinProc _newWndProc;
@@ -41,6 +44,13 @@ namespace ClipForge
private static extern IntPtr SendMessage(IntPtr hWnd, uint msg,
IntPtr wParam, IntPtr lParam);
+ [DllImport("user32.dll")]
+ private static extern short GetAsyncKeyState(int vk);
+
+ private const int VK_SHIFT = 0x10;
+ private const int VK_CONTROL = 0x11;
+ private const int VK_MENU = 0x12; // Alt
+ private const int VK_LWIN = 0x5B;
private const int GWLP_WNDPROC = -4;
private const uint WM_HOTKEY = 0x0312;
private const int GWL_EXSTYLE = -20;
@@ -78,7 +88,10 @@ namespace ClipForge
_hotkeyService = new GlobalHotkeyService();
_hotkeyService.ClipRequested += OnClipRequested;
- _hotkeyService.Initialize(hwnd);
+ var mod = _settingsService.Settings.HotkeyModifiers;
+ var vk = _settingsService.Settings.HotkeyVirtualKey;
+ if (mod == 0 && vk == 0) { mod = 1; vk = 0x78; }
+ _hotkeyService.Initialize(hwnd, (uint)mod, (uint)vk);
ClipGrid.ItemsSource = _clipLibrary.Clips;
UpdateClipCount();
@@ -169,6 +182,8 @@ namespace ClipForge
QualityLabel.Text = $"{s.VideoQuality}%";
FramerateCombo.SelectedIndex = s.Framerate == 30 ? 0 : 1;
StartupToggle.IsOn = IsStartupEnabled();
+ if (HotkeyRecorderButton != null)
+ HotkeyRecorderButton.Content = HotkeyHelper.ToDisplayString((uint)s.HotkeyModifiers, (uint)s.HotkeyVirtualKey);
}
// --- STARTUP WITH WINDOWS ---
@@ -235,17 +250,104 @@ namespace ClipForge
_ = ShowToastAsync("✅ Settings saved!");
}
+ // --- CUSTOM HOTKEY ---
+ private void HotkeyRecorderButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_isRecordingHotkey) return;
+ _isRecordingHotkey = true;
+ HotkeyRecorderButton.Content = "Press any key... (Esc to cancel)";
+ HotkeyRecorderButton.Focus(FocusState.Programmatic);
+ HotkeyRecorderButton.KeyDown += OnHotkeyCaptureKeyDown;
+ }
+
+ private void OnHotkeyCaptureKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (!_isRecordingHotkey) return;
+ e.Handled = true;
+
+ try
+ {
+ var key = e.Key;
+ // Escape cancels without changing the hotkey
+ if (key == VirtualKey.Escape)
+ {
+ StopRecordingHotkey(restoreDisplay: true);
+ return;
+ }
+ if (HotkeyHelper.IsModifierKey(key))
+ return; // wait for a non-modifier key
+ if (key == VirtualKey.None)
+ return;
+
+ uint mod = 0;
+ if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) mod |= HotkeyHelper.MOD_CONTROL;
+ if ((GetAsyncKeyState(VK_MENU) & 0x8000) != 0) mod |= HotkeyHelper.MOD_ALT;
+ if ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0) mod |= HotkeyHelper.MOD_SHIFT;
+ if ((GetAsyncKeyState(VK_LWIN) & 0x8000) != 0) mod |= HotkeyHelper.MOD_WIN;
+
+ uint vk = (uint)key;
+ _settingsService.Settings.HotkeyModifiers = (int)mod;
+ _settingsService.Settings.HotkeyVirtualKey = (int)vk;
+
+ var display = HotkeyHelper.ToDisplayString(mod, vk);
+ StopRecordingHotkey(restoreDisplay: false);
+ HotkeyRecorderButton.Content = display;
+
+ // Defer registration so the key is released first; otherwise the new hotkey can fire
+ // immediately and re-enter (e.g. OnClipRequested) and crash.
+ _ = DelayedHotkeyRegister(mod, vk, display);
+ }
+ catch (Exception ex)
+ {
+ StopRecordingHotkey(restoreDisplay: true);
+ _ = ShowToastAsync("⚠ " + ex.Message);
+ }
+ }
+
+ private void StopRecordingHotkey(bool restoreDisplay)
+ {
+ HotkeyRecorderButton.KeyDown -= OnHotkeyCaptureKeyDown;
+ _isRecordingHotkey = false;
+ if (restoreDisplay)
+ {
+ var s = _settingsService.Settings;
+ HotkeyRecorderButton.Content = HotkeyHelper.ToDisplayString((uint)s.HotkeyModifiers, (uint)s.HotkeyVirtualKey);
+ }
+ }
+
+ private async System.Threading.Tasks.Task DelayedHotkeyRegister(uint mod, uint vk, string display)
+ {
+ await System.Threading.Tasks.Task.Delay(300);
+ App.MainQueue?.TryEnqueue(() =>
+ {
+ try
+ {
+ var ok = _hotkeyService.UpdateHotkey(mod, vk);
+ HotkeyRecorderButton.Content = ok ? display : display + " (in use?)";
+ }
+ catch (Exception ex)
+ {
+ HotkeyRecorderButton.Content = display;
+ _ = ShowToastAsync("⚠ " + ex.Message);
+ }
+ });
+ }
+
// --- NAV ---
private void NavClips_Click(object sender, RoutedEventArgs e)
{
ClipsPage.Visibility = Visibility.Visible;
SettingsPage.Visibility = Visibility.Collapsed;
+ NavClips.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonSelectedStyle"];
+ NavSettings.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonStyle"];
}
private void NavSettings_Click(object sender, RoutedEventArgs e)
{
ClipsPage.Visibility = Visibility.Collapsed;
SettingsPage.Visibility = Visibility.Visible;
+ NavSettings.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonSelectedStyle"];
+ NavClips.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonStyle"];
}
private void RecordButton_Click(object sender, RoutedEventArgs e)
diff --git a/ClipForge/Package.appxmanifest b/ClipForge/Package.appxmanifest
index ca70092..c877d90 100644
--- a/ClipForge/Package.appxmanifest
+++ b/ClipForge/Package.appxmanifest
@@ -11,7 +11,7 @@
+ Version="0.1.0.0" />
diff --git a/ClipForge/SettingsService.cs b/ClipForge/SettingsService.cs
index 929eb3c..080aeea 100644
--- a/ClipForge/SettingsService.cs
+++ b/ClipForge/SettingsService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Text.Json;
@@ -9,7 +9,10 @@ namespace ClipForge
public int ClipLengthSeconds { get; set; } = 30;
public int VideoQuality { get; set; } = 70;
public int Framerate { get; set; } = 60;
- public string HotkeyDisplay { get; set; } = "Alt + F9";
+ /// Windows modifier flags: Alt=1, Ctrl=2, Shift=4, Win=8.
+ public int HotkeyModifiers { get; set; } = 1; // MOD_ALT
+ /// Windows virtual key code (e.g. VK_F9=0x78, VK_PRIOR=0x21 for PgUp).
+ public int HotkeyVirtualKey { get; set; } = 0x78; // VK_F9
}
public class SettingsService