1.0.2
This commit is contained in:
parent
8f198362de
commit
b5df3cd83e
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36401.2 d17.14
|
||||
VisualStudioVersion = 17.14.36401.2
|
||||
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}"
|
||||
EndProject
|
||||
|
||||
88
HotkeyRecorderForm.Designer.cs
generated
Normal file
88
HotkeyRecorderForm.Designer.cs
generated
Normal 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
42
HotkeyRecorderForm.cs
Normal 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
4028
HotkeyRecorderForm.resx
Normal file
File diff suppressed because it is too large
Load Diff
627
Program.cs
627
Program.cs
@ -1,50 +1,31 @@
|
||||
using Click_to_Call_Tray;
|
||||
using Click_to_Call_Tray.Properties;
|
||||
using Microsoft.VisualBasic.ApplicationServices;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Policy;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Forms.Design.Behavior;
|
||||
|
||||
namespace Click_to_Call_Tray
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
// Global keyboard hook state
|
||||
private static LowLevelKeyboardProc s_hookProc = HookCallback;
|
||||
private static IntPtr s_hookID = IntPtr.Zero;
|
||||
private static HotkeyManager _hotkeyManager;
|
||||
private static ClipboardWorker _clipboardWorker;
|
||||
|
||||
// Stores the Virtual Key Code for the current hotkey (e.g., 0x7A for F11)
|
||||
internal static int CurrentHotkeyVkCode { get; private set; }
|
||||
internal static Keys CurrentHotkey { 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)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool FreeConsole();
|
||||
@ -52,138 +33,366 @@ namespace Click_to_Call_Tray
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
// 1. Load configuration and set initial hotkey
|
||||
string initialHotkeyName = IniConfig.ReadHotkey();
|
||||
CurrentHotkeyVkCode = GetVkCodeFromKeyName(initialHotkeyName);
|
||||
s_hookID = SetHook(s_hookProc);
|
||||
// Load hotkey from config; be robust with parsing; fall back to F11.
|
||||
var initialHotkey = HotkeyStringConverter.TryParse(IniConfig.ReadHotkey(), out var parsed)
|
||||
? parsed
|
||||
: 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();
|
||||
UpdateCheck.CheckUpdate();
|
||||
// Optional: asynchronous update check
|
||||
Task.Run(() => UpdateCheck.CheckUpdate());
|
||||
|
||||
// 3. Initialize Application and start the Tray Icon
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new CallTrayContext(currentTag));
|
||||
|
||||
// 4. Clean up upon exit
|
||||
UnhookWindowsHookEx(s_hookID);
|
||||
_hotkeyManager?.Dispose();
|
||||
_clipboardWorker?.Dispose();
|
||||
|
||||
FreeConsole();
|
||||
}
|
||||
|
||||
private static IntPtr SetHook(LowLevelKeyboardProc proc)
|
||||
internal static void SetNewHotkey(Keys newHotkey)
|
||||
{
|
||||
using (Process curProcess = Process.GetCurrentProcess())
|
||||
using (ProcessModule curModule = curProcess.MainModule)
|
||||
CurrentHotkey = newHotkey;
|
||||
IniConfig.WriteHotkey(HotkeyStringConverter.ToString(newHotkey));
|
||||
if (_hotkeyManager != null)
|
||||
{
|
||||
return SetWindowsHookEx(WH_KEYBOARD_LL, proc,
|
||||
GetModuleHandle(curModule.ModuleName), 0);
|
||||
_hotkeyManager.CurrentHotkey = newHotkey;
|
||||
}
|
||||
}
|
||||
|
||||
private delegate IntPtr LowLevelKeyboardProc(
|
||||
int nCode, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
private static IntPtr HookCallback(
|
||||
int nCode, IntPtr wParam, IntPtr lParam)
|
||||
private static void OnHotkeyPressed()
|
||||
{
|
||||
const int WM_KEYDOWN = 0x0100;
|
||||
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
|
||||
{
|
||||
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(() =>
|
||||
// Run outside the hook thread, quickly
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string clipboardText = Clipboard.GetText();
|
||||
// Copy selection
|
||||
SendKeys.SendWait("^c");
|
||||
|
||||
if (!string.IsNullOrEmpty(clipboardText))
|
||||
{
|
||||
string cleanedNumber = CleanNumber(clipboardText);
|
||||
// Give the clipboard a brief moment to update and then try to read it with retries
|
||||
await Task.Delay(80).ConfigureAwait(false);
|
||||
|
||||
if (IsValidNumber(cleanedNumber))
|
||||
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))
|
||||
{
|
||||
// Optional: Console feedback
|
||||
Console.WriteLine($"Invalid number format: {cleaned}");
|
||||
return;
|
||||
}
|
||||
|
||||
var url = $"tel:{cleaned}";
|
||||
try
|
||||
{
|
||||
var url = $"tel:{cleanedNumber}";
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
Console.WriteLine($"Initiated call to: {cleanedNumber}");
|
||||
Console.WriteLine($"Initiated call to: {cleaned}");
|
||||
}
|
||||
else
|
||||
catch (Win32Exception wex)
|
||||
{
|
||||
Console.WriteLine($"Invalid number format: {cleanedNumber}");
|
||||
Console.WriteLine($"Failed to start tel: handler. {wex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ExternalException e)
|
||||
{
|
||||
Console.WriteLine($"Error accessing clipboard: {e.Message}");
|
||||
}
|
||||
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 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)]
|
||||
private static extern IntPtr SetWindowsHookEx(int idHook,
|
||||
LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
|
||||
private static extern IntPtr SetWindowsHookEx(int idHook, 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)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
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
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// TRAY ICON CONTEXT
|
||||
// =========================================================================================
|
||||
internal sealed class ClipboardWorker : IDisposable
|
||||
{
|
||||
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
|
||||
{
|
||||
@ -191,94 +400,164 @@ namespace Click_to_Call_Tray
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
static extern bool FreeConsole();
|
||||
|
||||
private NotifyIcon _trayIcon;
|
||||
private bool _console = false;
|
||||
|
||||
public CallTrayContext(string currentTag)
|
||||
{
|
||||
ContextMenuStrip contextMenu = new ContextMenuStrip();
|
||||
|
||||
ToolStripMenuItem creditMenuItem = new ToolStripMenuItem(String.Format("krjan02 ©{0}", DateTime.Now.Year));
|
||||
creditMenuItem.Enabled = false;
|
||||
contextMenu.Items.Add(creditMenuItem);
|
||||
|
||||
// 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);
|
||||
|
||||
var contextMenu = new ContextMenuStrip();
|
||||
contextMenu.Items.Add(CreateDisabledMenuItem($"krjan02 ©{DateTime.Now.Year}"));
|
||||
contextMenu.Items.Add(CreateMenuItem("Debug Console", DisplayConsole));
|
||||
contextMenu.Items.Add(CreateHotkeyMenu());
|
||||
contextMenu.Items.Add(CreateMenuItem("Check for Updates", CheckForUpdates));
|
||||
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()
|
||||
{
|
||||
Icon = Click_to_Call_Tray.Properties.Resources.TrayIcon,
|
||||
Icon = Resources.TrayIcon,
|
||||
ContextMenuStrip = contextMenu,
|
||||
Visible = true,
|
||||
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++)
|
||||
{
|
||||
string keyName = $"F{i}";
|
||||
ToolStripMenuItem keyItem = new ToolStripMenuItem(keyName);
|
||||
keyItem.Tag = keyName;
|
||||
keyItem.Click += SetHotkey_Click;
|
||||
parentMenu.DropDownItems.Add(keyItem);
|
||||
var key = Keys.F1 + (i - 1);
|
||||
string text = $"F{i}";
|
||||
var item = CreateMenuItem(text, SetHotkey_Click, key);
|
||||
hotkeyMenu.DropDownItems.Add(item);
|
||||
}
|
||||
|
||||
// Initially check the currently configured hotkey
|
||||
CheckCurrentHotkey(parentMenu);
|
||||
// Custom
|
||||
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)
|
||||
{
|
||||
string currentKeyName = IniConfig.ReadHotkey();
|
||||
foreach (ToolStripMenuItem item in parentMenu.DropDownItems)
|
||||
var raw = IniConfig.ReadHotkey();
|
||||
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;
|
||||
string newKeyName = clickedItem.Tag.ToString();
|
||||
var clickedItem = (ToolStripMenuItem)sender;
|
||||
if (clickedItem.Tag is not Keys newHotkey)
|
||||
return;
|
||||
|
||||
// 1. Update the configuration and reset the hook in Program
|
||||
Program.SetNewHotkey(newKeyName);
|
||||
Program.SetNewHotkey(newHotkey);
|
||||
|
||||
// 2. Update the checkmarks in the UI
|
||||
ToolStripMenuItem parentMenu = (ToolStripMenuItem)clickedItem.OwnerItem;
|
||||
var parentMenu = (ToolStripMenuItem)clickedItem.OwnerItem;
|
||||
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;
|
||||
}
|
||||
|
||||
void CheckForUpdates(object sender, EventArgs e)
|
||||
MessageBox.Show($"Hotkey successfully changed to {newHotkey}. Please re-select the phone number text before pressing the new hotkey.", "Hotkey Updated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
@ -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.Dispose();
|
||||
@ -295,9 +574,9 @@ namespace Click_to_Call_Tray
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _trayIcon != null)
|
||||
if (disposing)
|
||||
{
|
||||
_trayIcon.Dispose();
|
||||
_trayIcon?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user