Public Access
244 lines
7.4 KiB
C#
244 lines
7.4 KiB
C#
using System.Drawing;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows.Forms;
|
|
using MeetingAssistant.LaunchProfiles;
|
|
using MeetingAssistant.Recording;
|
|
|
|
namespace MeetingAssistant.Taskbar;
|
|
|
|
public sealed class WindowsTaskbarIconService : IHostedService, IDisposable
|
|
{
|
|
private readonly MeetingRecordingCoordinator coordinator;
|
|
private readonly ILaunchProfileOptionsProvider launchProfiles;
|
|
private readonly ILogger<WindowsTaskbarIconService> logger;
|
|
private readonly TaskCompletionSource started = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
private Thread? uiThread;
|
|
private SynchronizationContext? uiContext;
|
|
private NotifyIcon? notifyIcon;
|
|
private ContextMenuStrip? contextMenu;
|
|
private System.Windows.Forms.Timer? refreshTimer;
|
|
private Icon? currentIcon;
|
|
private RecordingProcessState? currentState;
|
|
|
|
public WindowsTaskbarIconService(
|
|
MeetingRecordingCoordinator coordinator,
|
|
ILaunchProfileOptionsProvider launchProfiles,
|
|
ILogger<WindowsTaskbarIconService> logger)
|
|
{
|
|
this.coordinator = coordinator;
|
|
this.launchProfiles = launchProfiles;
|
|
this.logger = logger;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!OperatingSystem.IsWindows())
|
|
{
|
|
logger.LogInformation("Taskbar icon is only supported on Windows");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
uiThread = new Thread(RunMessageLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = "Meeting Assistant Taskbar Icon"
|
|
};
|
|
uiThread.SetApartmentState(ApartmentState.STA);
|
|
uiThread.Start();
|
|
|
|
return started.Task.WaitAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
var context = uiContext;
|
|
if (context is not null)
|
|
{
|
|
context.Post(_ => Application.ExitThread(), null);
|
|
}
|
|
|
|
if (uiThread is not null)
|
|
{
|
|
while (uiThread.IsAlive && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(50, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
currentIcon?.Dispose();
|
|
contextMenu?.Dispose();
|
|
notifyIcon?.Dispose();
|
|
refreshTimer?.Dispose();
|
|
}
|
|
|
|
private void RunMessageLoop()
|
|
{
|
|
try
|
|
{
|
|
Application.EnableVisualStyles();
|
|
Application.SetCompatibleTextRenderingDefault(false);
|
|
uiContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
|
|
SynchronizationContext.SetSynchronizationContext(uiContext);
|
|
|
|
contextMenu = new ContextMenuStrip();
|
|
contextMenu.Opening += (_, _) => RebuildMenu();
|
|
notifyIcon = new NotifyIcon
|
|
{
|
|
ContextMenuStrip = contextMenu,
|
|
Visible = true
|
|
};
|
|
refreshTimer = new System.Windows.Forms.Timer
|
|
{
|
|
Interval = 1000
|
|
};
|
|
refreshTimer.Tick += (_, _) => RefreshVisualState();
|
|
refreshTimer.Start();
|
|
RefreshVisualState();
|
|
started.TrySetResult();
|
|
|
|
Application.Run();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
started.TrySetException(exception);
|
|
logger.LogError(exception, "Taskbar icon service failed");
|
|
}
|
|
finally
|
|
{
|
|
refreshTimer?.Stop();
|
|
if (notifyIcon is not null)
|
|
{
|
|
notifyIcon.Visible = false;
|
|
}
|
|
|
|
Dispose();
|
|
}
|
|
}
|
|
|
|
private void RefreshVisualState()
|
|
{
|
|
if (notifyIcon is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var menu = BuildMenu();
|
|
if (currentState != menu.State)
|
|
{
|
|
var nextIcon = CreateIcon(menu.State);
|
|
notifyIcon.Icon = nextIcon;
|
|
currentIcon?.Dispose();
|
|
currentIcon = nextIcon;
|
|
currentState = menu.State;
|
|
}
|
|
|
|
notifyIcon.Text = TruncateTooltip(menu.Tooltip);
|
|
}
|
|
|
|
private void RebuildMenu()
|
|
{
|
|
if (contextMenu is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var menu = BuildMenu();
|
|
contextMenu.Items.Clear();
|
|
foreach (var menuItem in menu.Items)
|
|
{
|
|
var item = new ToolStripMenuItem(menuItem.Text)
|
|
{
|
|
Tag = menuItem
|
|
};
|
|
item.Click += (_, _) => _ = ExecuteMenuItemAsync(menuItem);
|
|
contextMenu.Items.Add(item);
|
|
}
|
|
}
|
|
|
|
private MeetingTaskbarMenu BuildMenu()
|
|
{
|
|
return MeetingTaskbarMenuBuilder.Build(
|
|
coordinator.CurrentStatus,
|
|
launchProfiles.GetProfiles());
|
|
}
|
|
|
|
private async Task ExecuteMenuItemAsync(MeetingTaskbarMenuItem item)
|
|
{
|
|
try
|
|
{
|
|
switch (item.Action)
|
|
{
|
|
case MeetingTaskbarAction.StartRecording:
|
|
await coordinator.StartAsync(item.ProfileName, CancellationToken.None);
|
|
break;
|
|
case MeetingTaskbarAction.StopRecording:
|
|
await coordinator.StopAsync(CancellationToken.None);
|
|
break;
|
|
case MeetingTaskbarAction.AbortRecording:
|
|
await coordinator.AbortAsync(CancellationToken.None);
|
|
break;
|
|
case MeetingTaskbarAction.SwitchProfile:
|
|
await coordinator.ToggleAsync(item.ProfileName, CancellationToken.None);
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.LogError(
|
|
exception,
|
|
"Taskbar menu action {Action} failed for launch profile {LaunchProfile}",
|
|
item.Action,
|
|
item.ProfileName);
|
|
}
|
|
}
|
|
|
|
private static string TruncateTooltip(string tooltip)
|
|
{
|
|
return tooltip.Length <= 63 ? tooltip : tooltip[..63];
|
|
}
|
|
|
|
private static Icon CreateIcon(RecordingProcessState state)
|
|
{
|
|
var (color, text) = state switch
|
|
{
|
|
RecordingProcessState.Recording => (Color.FromArgb(220, 38, 38), "R"),
|
|
RecordingProcessState.Summarizing => (Color.FromArgb(37, 99, 235), "S"),
|
|
_ => (Color.FromArgb(75, 85, 99), "I")
|
|
};
|
|
|
|
using var bitmap = new Bitmap(16, 16);
|
|
using (var graphics = Graphics.FromImage(bitmap))
|
|
{
|
|
graphics.Clear(Color.Transparent);
|
|
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
using var brush = new SolidBrush(color);
|
|
graphics.FillEllipse(brush, 1, 1, 14, 14);
|
|
using var font = new Font(FontFamily.GenericSansSerif, 8, FontStyle.Bold, GraphicsUnit.Pixel);
|
|
using var textBrush = new SolidBrush(Color.White);
|
|
var size = graphics.MeasureString(text, font);
|
|
graphics.DrawString(
|
|
text,
|
|
font,
|
|
textBrush,
|
|
(16 - size.Width) / 2,
|
|
(16 - size.Height) / 2);
|
|
}
|
|
|
|
var handle = bitmap.GetHicon();
|
|
try
|
|
{
|
|
return (Icon)Icon.FromHandle(handle).Clone();
|
|
}
|
|
finally
|
|
{
|
|
DestroyIcon(handle);
|
|
}
|
|
}
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern bool DestroyIcon(IntPtr hIcon);
|
|
}
|