Click-to-Call-Tray/Program.cs
krjan02 19e74f9e29
All checks were successful
Build and Relase / build-release (push) Successful in 1m9s
Build and Relase / create-release (push) Successful in 13s
Initial release
2025-10-14 18:13:17 +02:00

306 lines
11 KiB
C#

using Click_to_Call_Tray;
using Click_to_Call_Tray.Properties;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
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;
// Stores the Virtual Key Code for the current hotkey (e.g., 0x7A for F11)
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)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FreeConsole();
[STAThread]
static void Main()
{
// 1. Load configuration and set initial hotkey
string initialHotkeyName = IniConfig.ReadHotkey();
CurrentHotkeyVkCode = GetVkCodeFromKeyName(initialHotkeyName);
s_hookID = SetHook(s_hookProc);
// 2. Fetch version and check for updates
var currentTag = UpdateCheck.GetCurrentVersion();
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);
FreeConsole();
}
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc,
GetModuleHandle(curModule.ModuleName), 0);
}
}
private delegate IntPtr LowLevelKeyboardProc(
int nCode, IntPtr wParam, IntPtr lParam);
private static IntPtr HookCallback(
int nCode, IntPtr wParam, IntPtr lParam)
{
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(() =>
{
try
{
string clipboardText = Clipboard.GetText();
if (!string.IsNullOrEmpty(clipboardText))
{
string cleanedNumber = CleanNumber(clipboardText);
if (IsValidNumber(cleanedNumber))
{
var url = $"tel:{cleanedNumber}";
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
Console.WriteLine($"Initiated call to: {cleanedNumber}");
}
else
{
Console.WriteLine($"Invalid number format: {cleanedNumber}");
}
}
}
catch (ExternalException e)
{
Console.WriteLine($"Error accessing clipboard: {e.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join(500);
}
private static string CleanNumber(string input)
{
// 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;
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook,
LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[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
// =========================================================================================
public class CallTrayContext : ApplicationContext
{
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AllocConsole();
private NotifyIcon _trayIcon;
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);
contextMenu.Items.Add(new ToolStripSeparator());
// 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,
ContextMenuStrip = contextMenu,
Visible = true,
Text = $"Click-to-Call-Tray {currentTag}"
};
}
private void AddHotkeyMenuItems(ToolStripMenuItem parentMenu)
{
// Create F1 through F12 options
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);
}
// Initially check the currently configured hotkey
CheckCurrentHotkey(parentMenu);
}
private void CheckCurrentHotkey(ToolStripMenuItem parentMenu)
{
string currentKeyName = IniConfig.ReadHotkey();
foreach (ToolStripMenuItem item in parentMenu.DropDownItems)
{
item.Checked = item.Tag.ToString() == currentKeyName;
}
}
void SetHotkey_Click(object sender, EventArgs e)
{
ToolStripMenuItem clickedItem = (ToolStripMenuItem)sender;
string newKeyName = clickedItem.Tag.ToString();
// 1. Update the configuration and reset the hook in Program
Program.SetNewHotkey(newKeyName);
// 2. Update the checkmarks in the UI
ToolStripMenuItem parentMenu = (ToolStripMenuItem)clickedItem.OwnerItem;
CheckCurrentHotkey(parentMenu);
MessageBox.Show($"Hotkey successfully changed to {newKeyName}. Please ensure you re-select the phone number text before pressing {newKeyName}.", "Hotkey Updated");
}
void DisplayConsole(object sender, EventArgs e)
{
AllocConsole();
}
void CheckForUpdates(object sender, EventArgs e)
{
if (!UpdateCheck.CheckUpdate())
{
MessageBox.Show("No update available.", "Update Status");
}
}
void Exit(object sender, EventArgs e)
{
_trayIcon.Visible = false;
_trayIcon.Dispose();
Application.Exit();
}
protected override void Dispose(bool disposing)
{
if (disposing && _trayIcon != null)
{
_trayIcon.Dispose();
}
base.Dispose(disposing);
}
}
}