1.0.2
All checks were successful
Build and Relase / build-release (push) Successful in 1m37s
Build and Relase / create-release (push) Successful in 1m8s

This commit is contained in:
krjan02 2025-11-13 21:34:07 +01:00
parent 8f198362de
commit b5df3cd83e
5 changed files with 4618 additions and 181 deletions

View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.14.36401.2 d17.14 VisualStudioVersion = 17.14.36401.2
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Click-to-Call-Tray", "Click-to-Call-Tray.csproj", "{FDD892DF-B784-42F6-AA74-6401D2627451}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Click-to-Call-Tray", "Click-to-Call-Tray.csproj", "{FDD892DF-B784-42F6-AA74-6401D2627451}"
EndProject EndProject

88
HotkeyRecorderForm.Designer.cs generated Normal file
View File

@ -0,0 +1,88 @@
namespace Click_to_Call_Tray
{
partial class HotkeyRecorderForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
private System.Windows.Forms.Label labelInstruction;
private System.Windows.Forms.TextBox textBoxHotkey;
private System.Windows.Forms.Button buttonOk;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(HotkeyRecorderForm));
labelInstruction = new Label();
textBoxHotkey = new TextBox();
buttonOk = new Button();
SuspendLayout();
//
// labelInstruction
//
labelInstruction.AutoSize = true;
labelInstruction.Location = new Point(18, 17);
labelInstruction.Margin = new Padding(4, 0, 4, 0);
labelInstruction.Name = "labelInstruction";
labelInstruction.Size = new Size(229, 15);
labelInstruction.TabIndex = 0;
labelInstruction.Text = "Press the keys you want to set as a hotkey:";
//
// textBoxHotkey
//
textBoxHotkey.Location = new Point(18, 46);
textBoxHotkey.Margin = new Padding(4, 3, 4, 3);
textBoxHotkey.Name = "textBoxHotkey";
textBoxHotkey.ReadOnly = true;
textBoxHotkey.Size = new Size(291, 23);
textBoxHotkey.TabIndex = 1;
//
// buttonOk
//
buttonOk.Location = new Point(315, 44);
buttonOk.Margin = new Padding(4, 3, 4, 3);
buttonOk.Name = "buttonOk";
buttonOk.Size = new Size(88, 27);
buttonOk.TabIndex = 2;
buttonOk.Text = "OK";
buttonOk.UseVisualStyleBackColor = true;
buttonOk.Click += ButtonOk_Click;
//
// HotkeyRecorderForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(420, 92);
Controls.Add(labelInstruction);
Controls.Add(textBoxHotkey);
Controls.Add(buttonOk);
Icon = (Icon)resources.GetObject("$this.Icon");
Margin = new Padding(4, 3, 4, 3);
Name = "HotkeyRecorderForm";
Text = "Hotkey Recorder";
ResumeLayout(false);
PerformLayout();
}
#endregion
}
}

42
HotkeyRecorderForm.cs Normal file
View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Click_to_Call_Tray
{
public partial class HotkeyRecorderForm : Form
{
public Keys RecordedKeys { get; private set; }
public HotkeyRecorderForm()
{
InitializeComponent();
this.KeyPreview = true;
this.KeyDown += HotkeyRecorderForm_KeyDown;
}
private void HotkeyRecorderForm_KeyDown(object sender, KeyEventArgs e)
{
StringBuilder keysBuilder = new StringBuilder();
if (e.Control) keysBuilder.Append("Ctrl+");
if (e.Shift) keysBuilder.Append("Shift+");
if (e.Alt) keysBuilder.Append("Alt+");
keysBuilder.Append(e.KeyCode.ToString());
textBoxHotkey.Text = keysBuilder.ToString();
RecordedKeys = e.KeyData;
}
private void ButtonOk_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.OK;
this.Close();
}
}
}

4028
HotkeyRecorderForm.resx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,31 @@
using Click_to_Call_Tray; using Click_to_Call_Tray;
using Click_to_Call_Tray.Properties; using Click_to_Call_Tray.Properties;
using Microsoft.VisualBasic.ApplicationServices;
using Microsoft.Win32;
using System; using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Policy;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Forms.Design.Behavior;
namespace Click_to_Call_Tray namespace Click_to_Call_Tray
{ {
internal static class Program internal static class Program
{ {
// Global keyboard hook state private static HotkeyManager _hotkeyManager;
private static LowLevelKeyboardProc s_hookProc = HookCallback; private static ClipboardWorker _clipboardWorker;
private static IntPtr s_hookID = IntPtr.Zero;
// Stores the Virtual Key Code for the current hotkey (e.g., 0x7A for F11) internal static Keys CurrentHotkey { get; private set; }
internal static int CurrentHotkeyVkCode { get; private set; }
// Converts a key name (e.g., "F11") to its VK code (e.g., 0x7A)
internal static int GetVkCodeFromKeyName(string keyName)
{
if (keyName.StartsWith("F") && int.TryParse(keyName.Substring(1), out int fNumber))
{
if (fNumber >= 1 && fNumber <= 12)
{
// VK_F1 is 0x70. VK_F2 is 0x71, etc.
return 0x70 + (fNumber - 1);
}
}
// Fallback to F11 (0x7A) if parsing fails
return 0x7A;
}
// Sets the new hotkey and updates the hook
internal static void SetNewHotkey(string newKeyName)
{
UnhookWindowsHookEx(s_hookID);
IniConfig.WriteHotkey(newKeyName);
CurrentHotkeyVkCode = GetVkCodeFromKeyName(newKeyName);
s_hookID = SetHook(s_hookProc);
}
// P/Invoke for WinAPI
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FreeConsole(); internal static extern bool FreeConsole();
@ -52,138 +33,366 @@ namespace Click_to_Call_Tray
[STAThread] [STAThread]
static void Main() static void Main()
{ {
// 1. Load configuration and set initial hotkey // Load hotkey from config; be robust with parsing; fall back to F11.
string initialHotkeyName = IniConfig.ReadHotkey(); var initialHotkey = HotkeyStringConverter.TryParse(IniConfig.ReadHotkey(), out var parsed)
CurrentHotkeyVkCode = GetVkCodeFromKeyName(initialHotkeyName); ? parsed
s_hookID = SetHook(s_hookProc); : Keys.F11;
CurrentHotkey = initialHotkey;
// Start the clipboard STA worker
_clipboardWorker = new ClipboardWorker();
// Start the low-level keyboard hook manager
_hotkeyManager = new HotkeyManager(CurrentHotkey);
_hotkeyManager.HotkeyPressed += OnHotkeyPressed;
// 2. Fetch version and check for updates
var currentTag = UpdateCheck.GetCurrentVersion(); var currentTag = UpdateCheck.GetCurrentVersion();
UpdateCheck.CheckUpdate(); // Optional: asynchronous update check
Task.Run(() => UpdateCheck.CheckUpdate());
// 3. Initialize Application and start the Tray Icon
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();
Application.Run(new CallTrayContext(currentTag)); Application.Run(new CallTrayContext(currentTag));
// 4. Clean up upon exit _hotkeyManager?.Dispose();
UnhookWindowsHookEx(s_hookID); _clipboardWorker?.Dispose();
FreeConsole(); FreeConsole();
} }
private static IntPtr SetHook(LowLevelKeyboardProc proc) internal static void SetNewHotkey(Keys newHotkey)
{ {
using (Process curProcess = Process.GetCurrentProcess()) CurrentHotkey = newHotkey;
using (ProcessModule curModule = curProcess.MainModule) IniConfig.WriteHotkey(HotkeyStringConverter.ToString(newHotkey));
if (_hotkeyManager != null)
{ {
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, _hotkeyManager.CurrentHotkey = newHotkey;
GetModuleHandle(curModule.ModuleName), 0);
} }
} }
private delegate IntPtr LowLevelKeyboardProc( private static void OnHotkeyPressed()
int nCode, IntPtr wParam, IntPtr lParam);
private static IntPtr HookCallback(
int nCode, IntPtr wParam, IntPtr lParam)
{ {
const int WM_KEYDOWN = 0x0100; // Run outside the hook thread, quickly
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) Task.Run(async () =>
{
int vkCode = Marshal.ReadInt32(lParam);
// Check if the pressed key matches the current configured hotkey VK code
if (vkCode == CurrentHotkeyVkCode)
{
// Copy selected text (Ctrl+C)
SendKeys.SendWait("^c");
Thread.Sleep(100);
ProcessClipboard();
return (IntPtr)1; // Consume the keypress
}
}
return CallNextHookEx(s_hookID, nCode, wParam, lParam);
}
private static void ProcessClipboard()
{
// The clipboard must be accessed on an STA thread.
Thread staThread = new Thread(() =>
{ {
try try
{ {
string clipboardText = Clipboard.GetText(); // Copy selection
SendKeys.SendWait("^c");
if (!string.IsNullOrEmpty(clipboardText)) // Give the clipboard a brief moment to update and then try to read it with retries
await Task.Delay(80).ConfigureAwait(false);
string text = await _clipboardWorker.GetTextWithRetryAsync(retryCount: 10, delayMs: 50).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(text))
return;
string cleaned = PhoneNumberUtil.CleanNumber(text);
if (!PhoneNumberUtil.IsValidNumber(cleaned))
{ {
string cleanedNumber = CleanNumber(clipboardText); // Optional: Console feedback
Console.WriteLine($"Invalid number format: {cleaned}");
if (IsValidNumber(cleanedNumber)) return;
{ }
var url = $"tel:{cleanedNumber}";
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); var url = $"tel:{cleaned}";
Console.WriteLine($"Initiated call to: {cleanedNumber}"); try
} {
else Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
{ Console.WriteLine($"Initiated call to: {cleaned}");
Console.WriteLine($"Invalid number format: {cleanedNumber}"); }
} catch (Win32Exception wex)
{
Console.WriteLine($"Failed to start tel: handler. {wex.Message}");
} }
}
catch (ExternalException e)
{
Console.WriteLine($"Error accessing clipboard: {e.Message}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"An unexpected error occurred: {ex.Message}"); Console.WriteLine($"Unexpected error handling hotkey: {ex.Message}");
} }
}); });
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join(500);
} }
}
private static string CleanNumber(string input) internal sealed class HotkeyManager : IDisposable
{ {
// Remove common separators and trim leading zero (if not part of an international code)
return input
.Replace(" ", "")
.Replace(".", "")
.Replace("/", "")
.Replace("-", "")
.Replace("(0)", "")
.TrimStart('0');
}
private static bool IsValidNumber(string number)
{
// Simple validation: accepts international format, digits, and optional extensions/separators
return Regex.IsMatch(number, @"^(\+?[1-9][0-9]{0,2})?(\s?\(\d+\)\s?)?\d+(-\d+)?$");
}
#region WinAPI
private const int WH_KEYBOARD_LL = 13; private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
private readonly LowLevelKeyboardProc _proc;
private IntPtr _hookId = IntPtr.Zero;
public event Action HotkeyPressed;
public Keys CurrentHotkey { get; set; }
public HotkeyManager(Keys initialHotkey)
{
CurrentHotkey = initialHotkey;
_proc = HookCallback;
_hookId = SetHook(_proc);
}
public void Dispose()
{
if (_hookId != IntPtr.Zero)
{
UnhookWindowsHookEx(_hookId);
_hookId = IntPtr.Zero;
}
}
private IntPtr SetHook(LowLevelKeyboardProc proc)
{
// For low-level hooks, it's okay to pass hMod = IntPtr.Zero
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, IntPtr.Zero, 0);
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
{
var kbData = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
var key = (Keys)kbData.vkCode;
// Build the composite key including modifiers
Keys composite = Keys.None;
if (Control.ModifierKeys.HasFlag(Keys.Control)) composite |= Keys.Control;
if (Control.ModifierKeys.HasFlag(Keys.Shift)) composite |= Keys.Shift;
if (Control.ModifierKeys.HasFlag(Keys.Alt)) composite |= Keys.Alt;
composite |= key;
if (AreHotkeysEqual(composite, CurrentHotkey))
{
// Fire on background to avoid blocking hook thread
Task.Run(() => HotkeyPressed?.Invoke());
// Swallow the key to prevent it from reaching the foreground app
return (IntPtr)1;
}
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
private static bool AreHotkeysEqual(Keys a, Keys b)
{
// Normalize to compare modifiers + key
var norm = new Func<Keys, Keys>(k =>
{
var mods = k & (Keys.Control | Keys.Shift | Keys.Alt);
var keyOnly = k & Keys.KeyCode;
return mods | keyOnly;
});
return norm(a) == norm(b);
}
#region Interop
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk); private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
#endregion #endregion
} }
// ========================================================================================= internal sealed class ClipboardWorker : IDisposable
// TRAY ICON CONTEXT {
// ========================================================================================= private readonly Thread _thread;
private readonly BlockingCollection<Func<string>> _requests = new();
private readonly BlockingCollection<string> _responses = new();
private volatile bool _running = true;
public ClipboardWorker()
{
_thread = new Thread(Worker) { IsBackground = true };
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
}
private void Worker()
{
try
{
while (_running)
{
var work = _requests.Take();
try
{
var result = work?.Invoke();
_responses.Add(result);
}
catch
{
_responses.Add(null);
}
}
}
catch
{
// Exit thread
}
}
public Task<string> GetTextWithRetryAsync(int retryCount = 8, int delayMs = 40)
{
return Task.Run(async () =>
{
for (int i = 0; i < retryCount; i++)
{
string text = GetTextOnce();
if (!string.IsNullOrWhiteSpace(text))
return text;
await Task.Delay(delayMs).ConfigureAwait(false);
}
return string.Empty;
});
}
private string GetTextOnce()
{
if (!_running) return string.Empty;
_requests.Add(() =>
{
try
{
return Clipboard.ContainsText() ? Clipboard.GetText() : string.Empty;
}
catch (ExternalException)
{
return string.Empty;
}
catch
{
return string.Empty;
}
});
return _responses.Take();
}
public void Dispose()
{
_running = false;
try { _requests.CompleteAdding(); } catch { }
try { _responses.CompleteAdding(); } catch { }
}
}
internal static class PhoneNumberUtil
{
// Remove anything except digits and '+'.
public static string CleanNumber(string input)
{
if (string.IsNullOrWhiteSpace(input))
return string.Empty;
// Normalize unicode spaces etc.
var normalized = input.Trim();
// Remove specific common optional notation "(0)"
normalized = normalized.Replace("(0)", "", StringComparison.Ordinal);
var sb = new StringBuilder(normalized.Length);
foreach (var ch in normalized)
{
if ((ch >= '0' && ch <= '9') || ch == '+')
sb.Append(ch);
}
// Prevent multiple '+' or '+' not at start
string result = sb.ToString();
if (result.IndexOf('+') > 0) result = result.Replace("+", "");
if (result.Count(c => c == '+') > 1) result = result.TrimStart('+').Insert(0, "+");
return result;
}
// Simple and robust: allow + and 6-20 digits
public static bool IsValidNumber(string number)
{
if (string.IsNullOrWhiteSpace(number))
return false;
return Regex.IsMatch(number, @"^\+?\d{6,20}$");
}
private static int Count(this string s, Func<char, bool> predicate)
{
int c = 0;
foreach (var ch in s)
if (predicate(ch)) c++;
return c;
}
private static int Count(this string s, char match)
{
int c = 0;
foreach (var ch in s)
if (ch == match) c++;
return c;
}
}
internal static class HotkeyStringConverter
{
public static bool TryParse(string input, out Keys keys)
{
keys = Keys.None;
if (string.IsNullOrWhiteSpace(input))
return false;
// Try enum parse which supports comma-separated flags (e.g., "Control, Shift, H")
if (Enum.TryParse(typeof(Keys), input, true, out var value))
{
keys = (Keys)value;
return true;
}
// Also accept forms like "Ctrl+Shift+H"
var cleaned = input.Replace("Ctrl", "Control", StringComparison.OrdinalIgnoreCase)
.Replace('+', ',');
if (Enum.TryParse(typeof(Keys), cleaned, true, out value))
{
keys = (Keys)value;
return true;
}
// Function keys shortcut e.g., "F1"
if (input.StartsWith("F", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(input.AsSpan(1), out int f) && f >= 1 && f <= 24)
{
keys = Keys.F1 + (f - 1);
return true;
}
return false;
}
public static string ToString(Keys keys)
{
return keys.ToString();
}
}
public class CallTrayContext : ApplicationContext public class CallTrayContext : ApplicationContext
{ {
@ -191,94 +400,164 @@ namespace Click_to_Call_Tray
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
static extern bool AllocConsole(); static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool FreeConsole();
private NotifyIcon _trayIcon; private NotifyIcon _trayIcon;
private bool _console = false;
public CallTrayContext(string currentTag) public CallTrayContext(string currentTag)
{ {
ContextMenuStrip contextMenu = new ContextMenuStrip(); var contextMenu = new ContextMenuStrip();
contextMenu.Items.Add(CreateDisabledMenuItem($"krjan02 ©{DateTime.Now.Year}"));
ToolStripMenuItem creditMenuItem = new ToolStripMenuItem(String.Format("krjan02 ©{0}", DateTime.Now.Year)); contextMenu.Items.Add(CreateMenuItem("Debug Console", DisplayConsole));
creditMenuItem.Enabled = false; contextMenu.Items.Add(CreateHotkeyMenu());
contextMenu.Items.Add(creditMenuItem); contextMenu.Items.Add(CreateMenuItem("Check for Updates", CheckForUpdates));
// 1. Debug Console
ToolStripMenuItem logMenuItem = new ToolStripMenuItem("Debug Console");
logMenuItem.Click += DisplayConsole;
contextMenu.Items.Add(logMenuItem);
// 2. Hotkey Settings Submenu
ToolStripMenuItem hotkeyMenu = new ToolStripMenuItem("Set Hotkey");
AddHotkeyMenuItems(hotkeyMenu);
contextMenu.Items.Add(hotkeyMenu);
// 3. Update Check
ToolStripMenuItem updateMenuItem = new ToolStripMenuItem("Check for Updates");
updateMenuItem.Click += CheckForUpdates;
contextMenu.Items.Add(updateMenuItem);
contextMenu.Items.Add(new ToolStripSeparator()); contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add(CreateMenuItem("Exit", Exit));
// 4. Exit
ToolStripMenuItem exitMenuItem = new ToolStripMenuItem("Exit");
exitMenuItem.Click += Exit;
contextMenu.Items.Add(exitMenuItem);
// Initialize Tray Icon
_trayIcon = new NotifyIcon() _trayIcon = new NotifyIcon()
{ {
Icon = Click_to_Call_Tray.Properties.Resources.TrayIcon, Icon = Resources.TrayIcon,
ContextMenuStrip = contextMenu, ContextMenuStrip = contextMenu,
Visible = true, Visible = true,
Text = $"Click-to-Call-Tray {currentTag}" Text = $"Click-to-Call-Tray {currentTag}"
}; };
} }
private void AddHotkeyMenuItems(ToolStripMenuItem parentMenu) private ToolStripMenuItem CreateHotkeyMenu()
{ {
// Create F1 through F12 options var hotkeyMenu = new ToolStripMenuItem("Set Hotkey");
// F1..F12
for (int i = 1; i <= 12; i++) for (int i = 1; i <= 12; i++)
{ {
string keyName = $"F{i}"; var key = Keys.F1 + (i - 1);
ToolStripMenuItem keyItem = new ToolStripMenuItem(keyName); string text = $"F{i}";
keyItem.Tag = keyName; var item = CreateMenuItem(text, SetHotkey_Click, key);
keyItem.Click += SetHotkey_Click; hotkeyMenu.DropDownItems.Add(item);
parentMenu.DropDownItems.Add(keyItem);
} }
// Initially check the currently configured hotkey // Custom
CheckCurrentHotkey(parentMenu); var customKeyItem = new ToolStripMenuItem();
customKeyItem.Click += SetCustomHotkey_Click;
UpdateCustomHotkeyMenuItem(customKeyItem);
hotkeyMenu.DropDownItems.Add(customKeyItem);
// Mark current selection
CheckCurrentHotkey(hotkeyMenu);
return hotkeyMenu;
}
private void UpdateCustomHotkeyMenuItem(ToolStripMenuItem customKeyItem)
{
var raw = IniConfig.ReadHotkey();
Keys current = HotkeyStringConverter.TryParse(raw, out var parsed) ? parsed : Keys.F11;
bool isDefaultFn = IsDefaultFunctionKey(current);
customKeyItem.Text = isDefaultFn
? "Custom Hotkey [None]"
: $"Custom Hotkey [{HotkeyStringConverter.ToString(current)}]";
customKeyItem.Tag = current;
customKeyItem.Checked = !isDefaultFn;
if (customKeyItem.OwnerItem is ToolStripMenuItem parentItem && !isDefaultFn)
{
foreach (ToolStripItem item in parentItem.DropDownItems)
{
if (item is ToolStripMenuItem menuSubItem && menuSubItem.Tag is Keys kTag)
{
menuSubItem.Checked = kTag == current;
}
}
}
}
private ToolStripMenuItem CreateDisabledMenuItem(string text)
{
return new ToolStripMenuItem(text) { Enabled = false };
}
private ToolStripMenuItem CreateMenuItem(string text, EventHandler handler, object tag = null)
{
var item = new ToolStripMenuItem(text);
item.Click += handler;
item.Tag = tag;
return item;
} }
private void CheckCurrentHotkey(ToolStripMenuItem parentMenu) private void CheckCurrentHotkey(ToolStripMenuItem parentMenu)
{ {
string currentKeyName = IniConfig.ReadHotkey(); var raw = IniConfig.ReadHotkey();
foreach (ToolStripMenuItem item in parentMenu.DropDownItems) Keys current = HotkeyStringConverter.TryParse(raw, out var parsed) ? parsed : Keys.F11;
foreach (ToolStripItem item in parentMenu.DropDownItems)
{ {
item.Checked = item.Tag.ToString() == currentKeyName; if (item is ToolStripMenuItem menuSubItem && menuSubItem.Tag is Keys k)
{
menuSubItem.Checked = k == current;
}
} }
} }
void SetHotkey_Click(object sender, EventArgs e) private void SetHotkey_Click(object sender, EventArgs e)
{ {
ToolStripMenuItem clickedItem = (ToolStripMenuItem)sender; var clickedItem = (ToolStripMenuItem)sender;
string newKeyName = clickedItem.Tag.ToString(); if (clickedItem.Tag is not Keys newHotkey)
return;
// 1. Update the configuration and reset the hook in Program Program.SetNewHotkey(newHotkey);
Program.SetNewHotkey(newKeyName);
// 2. Update the checkmarks in the UI var parentMenu = (ToolStripMenuItem)clickedItem.OwnerItem;
ToolStripMenuItem parentMenu = (ToolStripMenuItem)clickedItem.OwnerItem;
CheckCurrentHotkey(parentMenu); CheckCurrentHotkey(parentMenu);
UpdateCustomHotkeyMenuItem(parentMenu.DropDownItems[^1] as ToolStripMenuItem);
MessageBox.Show($"Hotkey successfully changed to {newKeyName}. Please ensure you re-select the phone number text before pressing {newKeyName}.", "Hotkey Updated"); MessageBox.Show($"Hotkey successfully changed to {newHotkey}. Please re-select the phone number text before pressing {newHotkey}.", "Hotkey Updated");
} }
void DisplayConsole(object sender, EventArgs e) private void SetCustomHotkey_Click(object sender, EventArgs e)
{ {
AllocConsole(); using (HotkeyRecorderForm recorder = new HotkeyRecorderForm())
{
if (recorder.ShowDialog() == DialogResult.OK)
{
Keys newHotkey = recorder.RecordedKeys;
Program.SetNewHotkey(newHotkey);
if (sender is ToolStripMenuItem customItem)
{
UpdateCustomHotkeyMenuItem(customItem);
// Also uncheck function key items
if (customItem.OwnerItem is ToolStripMenuItem parent)
foreach (ToolStripItem it in parent.DropDownItems)
if (it is ToolStripMenuItem mi && mi.Tag is Keys k && IsDefaultFunctionKey(k))
mi.Checked = false;
}
MessageBox.Show($"Hotkey successfully changed to {newHotkey}. Please re-select the phone number text before pressing the new hotkey.", "Hotkey Updated");
}
}
} }
void CheckForUpdates(object sender, EventArgs e) private bool IsDefaultFunctionKey(Keys keys)
{
return keys >= Keys.F1 && keys <= Keys.F24;
}
private void DisplayConsole(object sender, EventArgs e)
{
// Show console if not already allocated
if (_console)
FreeConsole();
else
AllocConsole();
_console = !_console;
}
private void CheckForUpdates(object sender, EventArgs e)
{ {
if (!UpdateCheck.CheckUpdate()) if (!UpdateCheck.CheckUpdate())
{ {
@ -286,7 +565,7 @@ namespace Click_to_Call_Tray
} }
} }
void Exit(object sender, EventArgs e) private void Exit(object sender, EventArgs e)
{ {
_trayIcon.Visible = false; _trayIcon.Visible = false;
_trayIcon.Dispose(); _trayIcon.Dispose();
@ -295,11 +574,11 @@ namespace Click_to_Call_Tray
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing && _trayIcon != null) if (disposing)
{ {
_trayIcon.Dispose(); _trayIcon?.Dispose();
} }
base.Dispose(disposing); base.Dispose(disposing);
} }
} }
} }