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
|
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
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
639
Program.cs
639
Program.cs
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user