feat: Add Print Service Tray application with configuration and queue management

- Implemented PrintQueueService with job counting and snapshot retrieval.
- Created Form1 as the main entry point for the tray application.
- Developed ConfigurationForm for managing printer configurations and document types.
- Added models for printer configuration, queue status requests, and responses.
- Established ConfigurationService for loading and saving printer configurations.
- Introduced ServiceClient for IPC communication with the print service.
- Built TrayApplicationContext for managing the system tray icon and status updates.
- Added TODO.md for tracking remaining work and future enhancements.
This commit is contained in:
Jason
2026-05-29 10:43:48 -05:00
parent cce9248089
commit a08da0217d
17 changed files with 1541 additions and 4 deletions
+38
View File
@@ -0,0 +1,38 @@
namespace PrintServiceTray;
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <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()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
}
#endregion
}
+9
View File
@@ -0,0 +1,9 @@
namespace PrintServiceTray;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
+515
View File
@@ -0,0 +1,515 @@
using PrintServiceTray.Models;
using PrintServiceTray.Services;
namespace PrintServiceTray.Forms;
public partial class ConfigurationForm : Form
{
private readonly ConfigurationService _configService;
private PrinterConfiguration _config;
private string? _selectedDocType;
private List<string> _installedPrinters;
// UI Controls
private ListBox _docTypeListBox = null!;
private Button _newDocTypeButton = null!;
private Button _deleteDocTypeButton = null!;
private Panel _pageConfigPanel = null!;
private RadioButton _saveGlobalRadio = null!;
private RadioButton _saveLocalRadio = null!;
private Button _saveButton = null!;
private Button _cancelButton = null!;
private Button _addPageButton = null!;
private Button _removePageButton = null!;
private Label _configLabel = null!;
private List<PageConfigControl> _pageControls = new();
public ConfigurationForm()
{
_configService = new ConfigurationService();
_config = _configService.Load();
_installedPrinters = _configService.GetInstalledPrinters();
InitializeComponent();
LoadDocumentTypes();
}
private void InitializeComponent()
{
Text = "LAAPC Printer Configuration";
Size = new Size(900, 600);
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
// Left panel - Document Types
var leftPanel = new Panel
{
Location = new Point(10, 10),
Size = new Size(250, 540),
BorderStyle = BorderStyle.FixedSingle
};
var docTypeLabel = new Label
{
Text = "Document Types:",
Location = new Point(10, 10),
Size = new Size(230, 20),
Font = new Font(Font, FontStyle.Bold)
};
_docTypeListBox = new ListBox
{
Location = new Point(10, 35),
Size = new Size(230, 430)
};
_docTypeListBox.SelectedIndexChanged += DocTypeListBox_SelectedIndexChanged;
_newDocTypeButton = new Button
{
Text = "New",
Location = new Point(10, 475),
Size = new Size(110, 30)
};
_newDocTypeButton.Click += NewDocType_Click;
_deleteDocTypeButton = new Button
{
Text = "Delete",
Location = new Point(130, 475),
Size = new Size(110, 30)
};
_deleteDocTypeButton.Click += DeleteDocType_Click;
leftPanel.Controls.AddRange(new Control[] {
docTypeLabel, _docTypeListBox, _newDocTypeButton, _deleteDocTypeButton
});
// Right panel - Page Configuration
var rightPanel = new Panel
{
Location = new Point(270, 10),
Size = new Size(610, 540),
BorderStyle = BorderStyle.FixedSingle
};
_configLabel = new Label
{
Text = "Configuration for: (Select a document type)",
Location = new Point(10, 10),
Size = new Size(590, 20),
Font = new Font(Font, FontStyle.Bold)
};
var pageConfigLabel = new Label
{
Text = "Page Configuration:",
Location = new Point(10, 40),
Size = new Size(590, 20)
};
_pageConfigPanel = new Panel
{
Location = new Point(10, 65),
Size = new Size(590, 350),
AutoScroll = true,
BorderStyle = BorderStyle.FixedSingle
};
_addPageButton = new Button
{
Text = "+ Add Page",
Location = new Point(10, 425),
Size = new Size(120, 30)
};
_addPageButton.Click += AddPage_Click;
_removePageButton = new Button
{
Text = "- Remove Last",
Location = new Point(140, 425),
Size = new Size(120, 30)
};
_removePageButton.Click += RemovePage_Click;
// Save options
var saveToLabel = new Label
{
Text = "Save to:",
Location = new Point(10, 465),
Size = new Size(60, 20)
};
_saveGlobalRadio = new RadioButton
{
Text = "Global (All Users)",
Location = new Point(80, 465),
Size = new Size(150, 20)
};
_saveLocalRadio = new RadioButton
{
Text = "Local (My Computer)",
Location = new Point(240, 465),
Size = new Size(170, 20),
Checked = true
};
_saveButton = new Button
{
Text = "Save",
Location = new Point(430, 495),
Size = new Size(80, 35)
};
_saveButton.Click += Save_Click;
_cancelButton = new Button
{
Text = "Cancel",
Location = new Point(520, 495),
Size = new Size(80, 35)
};
_cancelButton.Click += (s, e) => Close();
rightPanel.Controls.AddRange(new Control[] {
_configLabel, pageConfigLabel, _pageConfigPanel,
_addPageButton, _removePageButton,
saveToLabel, _saveGlobalRadio, _saveLocalRadio,
_saveButton, _cancelButton
});
Controls.AddRange(new Control[] { leftPanel, rightPanel });
}
private void LoadDocumentTypes()
{
_docTypeListBox.Items.Clear();
foreach (var docType in _config.DocumentTypes.Keys.OrderBy(k => k))
{
_docTypeListBox.Items.Add(docType);
}
}
private void DocTypeListBox_SelectedIndexChanged(object? sender, EventArgs e)
{
if (_docTypeListBox.SelectedItem is string docType)
{
_selectedDocType = docType;
LoadPageConfiguration(docType);
}
}
private void LoadPageConfiguration(string docType)
{
_configLabel.Text = $"Configuration for: {docType}";
_pageConfigPanel.Controls.Clear();
_pageControls.Clear();
if (!_config.DocumentTypes.TryGetValue(docType, out var config))
{
config = new DocumentTypeConfig { Name = docType };
_config.DocumentTypes[docType] = config;
}
int yPos = 10;
foreach (var page in config.Pages.OrderBy(p => p.PageNumber))
{
var pageControl = new PageConfigControl(page.PageNumber, _installedPrinters, _configService);
pageControl.Location = new Point(10, yPos);
pageControl.LoadPage(page);
_pageConfigPanel.Controls.Add(pageControl);
_pageControls.Add(pageControl);
yPos += pageControl.Height + 10;
}
if (config.Pages.Count == 0)
{
// Add first page by default
AddPage_Click(null, EventArgs.Empty);
}
}
private void AddPage_Click(object? sender, EventArgs e)
{
if (_selectedDocType == null) return;
var config = _config.DocumentTypes[_selectedDocType];
int nextPageNum = config.Pages.Count > 0 ? config.Pages.Max(p => p.PageNumber) + 1 : 1;
var newPage = new PageConfig
{
PageNumber = nextPageNum,
PrinterName = _installedPrinters.FirstOrDefault() ?? "",
TrayNumber = 1
};
config.Pages.Add(newPage);
LoadPageConfiguration(_selectedDocType);
}
private void RemovePage_Click(object? sender, EventArgs e)
{
if (_selectedDocType == null) return;
var config = _config.DocumentTypes[_selectedDocType];
if (config.Pages.Count > 0)
{
var lastPage = config.Pages.OrderByDescending(p => p.PageNumber).First();
config.Pages.Remove(lastPage);
LoadPageConfiguration(_selectedDocType);
}
}
private void NewDocType_Click(object? sender, EventArgs e)
{
var dialog = new TextInputDialog("New Document Type", "Enter document type name:");
if (dialog.ShowDialog() == DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.InputText))
{
var docType = dialog.InputText.Trim();
if (!_config.DocumentTypes.ContainsKey(docType))
{
_config.DocumentTypes[docType] = new DocumentTypeConfig { Name = docType };
LoadDocumentTypes();
_docTypeListBox.SelectedItem = docType;
}
else
{
MessageBox.Show($"Document type '{docType}' already exists.", "Duplicate", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
}
private void DeleteDocType_Click(object? sender, EventArgs e)
{
if (_selectedDocType == null) return;
var result = MessageBox.Show(
$"Are you sure you want to delete '{_selectedDocType}'?",
"Confirm Delete",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
_config.DocumentTypes.Remove(_selectedDocType);
_selectedDocType = null;
LoadDocumentTypes();
_pageConfigPanel.Controls.Clear();
_configLabel.Text = "Configuration for: (Select a document type)";
}
}
private void Save_Click(object? sender, EventArgs e)
{
try
{
// Save current page configurations
if (_selectedDocType != null && _config.DocumentTypes.TryGetValue(_selectedDocType, out var config))
{
config.Pages.Clear();
foreach (var pageControl in _pageControls)
{
config.Pages.Add(pageControl.GetPageConfig());
}
}
bool saveGlobal = _saveGlobalRadio.Checked;
_configService.Save(_config, saveGlobal);
MessageBox.Show(
$"Configuration saved to {(saveGlobal ? "global" : "local")} file successfully!",
"Success",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
MessageBox.Show(
$"Failed to save configuration: {ex.Message}",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
}
// Custom control for each page configuration
public class PageConfigControl : UserControl
{
private readonly int _pageNumber;
private readonly List<string> _printers;
private readonly ConfigurationService _configService;
private ComboBox _printerCombo = null!;
private ComboBox _trayCombo = null!;
public PageConfigControl(int pageNumber, List<string> printers, ConfigurationService configService)
{
_pageNumber = pageNumber;
_printers = printers;
_configService = configService;
InitializeComponent();
}
private void InitializeComponent()
{
Size = new Size(560, 60);
BorderStyle = BorderStyle.FixedSingle;
var pageLabel = new Label
{
Text = $"Page {_pageNumber}:",
Location = new Point(10, 10),
Size = new Size(70, 20),
Font = new Font(Font, FontStyle.Bold)
};
var printerLabel = new Label
{
Text = "Printer:",
Location = new Point(10, 32),
Size = new Size(60, 20)
};
_printerCombo = new ComboBox
{
Location = new Point(75, 30),
Size = new Size(250, 25),
DropDownStyle = ComboBoxStyle.DropDownList
};
_printerCombo.Items.AddRange(_printers.ToArray());
_printerCombo.SelectedIndexChanged += PrinterCombo_SelectedIndexChanged;
var trayLabel = new Label
{
Text = "Tray:",
Location = new Point(340, 32),
Size = new Size(40, 20)
};
_trayCombo = new ComboBox
{
Location = new Point(385, 30),
Size = new Size(165, 25),
DropDownStyle = ComboBoxStyle.DropDownList
};
Controls.AddRange(new Control[] {
pageLabel, printerLabel, _printerCombo, trayLabel, _trayCombo
});
}
public void LoadPage(PageConfig page)
{
_printerCombo.SelectedItem = page.PrinterName;
LoadTrays(page.PrinterName);
// Try to find and select the tray
for (int i = 0; i < _trayCombo.Items.Count; i++)
{
if (_trayCombo.Items[i] is TrayItem item && item.TrayNumber == page.TrayNumber)
{
_trayCombo.SelectedIndex = i;
break;
}
}
}
private void PrinterCombo_SelectedIndexChanged(object? sender, EventArgs e)
{
if (_printerCombo.SelectedItem is string printer)
{
LoadTrays(printer);
}
}
private void LoadTrays(string printerName)
{
_trayCombo.Items.Clear();
var trays = _configService.GetPrinterTrays(printerName);
foreach (var (number, name) in trays)
{
_trayCombo.Items.Add(new TrayItem { TrayNumber = number, TrayName = name });
}
if (_trayCombo.Items.Count > 0)
{
_trayCombo.SelectedIndex = 0;
}
}
public PageConfig GetPageConfig()
{
var selectedTray = _trayCombo.SelectedItem as TrayItem;
return new PageConfig
{
PageNumber = _pageNumber,
PrinterName = _printerCombo.SelectedItem?.ToString() ?? "",
TrayNumber = selectedTray?.TrayNumber ?? 1,
TrayLabel = selectedTray?.TrayName
};
}
private class TrayItem
{
public int TrayNumber { get; set; }
public string TrayName { get; set; } = "";
public override string ToString() => $"Tray {TrayNumber} - {TrayName}";
}
}
// Simple text input dialog
public class TextInputDialog : Form
{
private TextBox _textBox = null!;
public string InputText => _textBox.Text;
public TextInputDialog(string title, string prompt)
{
Text = title;
Size = new Size(400, 150);
StartPosition = FormStartPosition.CenterParent;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
var promptLabel = new Label
{
Text = prompt,
Location = new Point(20, 20),
Size = new Size(360, 20)
};
_textBox = new TextBox
{
Location = new Point(20, 50),
Size = new Size(360, 25)
};
var okButton = new Button
{
Text = "OK",
DialogResult = DialogResult.OK,
Location = new Point(220, 85),
Size = new Size(75, 30)
};
var cancelButton = new Button
{
Text = "Cancel",
DialogResult = DialogResult.Cancel,
Location = new Point(305, 85),
Size = new Size(75, 30)
};
AcceptButton = okButton;
CancelButton = cancelButton;
Controls.AddRange(new Control[] { promptLabel, _textBox, okButton, cancelButton });
}
}
+29
View File
@@ -0,0 +1,29 @@
namespace PrintServiceTray.Models;
/// <summary>
/// Configuration for all document types
/// </summary>
public class PrinterConfiguration
{
public Dictionary<string, DocumentTypeConfig> DocumentTypes { get; set; } = new();
}
/// <summary>
/// Configuration for a single document type
/// </summary>
public class DocumentTypeConfig
{
public string Name { get; set; } = string.Empty;
public List<PageConfig> Pages { get; set; } = new();
}
/// <summary>
/// Configuration for a single page
/// </summary>
public class PageConfig
{
public int PageNumber { get; set; }
public string PrinterName { get; set; } = string.Empty;
public int TrayNumber { get; set; }
public string? TrayLabel { get; set; } // Optional: "Pink Paper", "Green Paper", etc.
}
@@ -0,0 +1,6 @@
namespace PrintServiceTray.Models;
public class QueueStatusRequest
{
public string Command { get; set; } = "GetQueueStatus";
}
@@ -0,0 +1,19 @@
namespace PrintServiceTray.Models;
public class QueueStatusResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public List<JobStatus> Jobs { get; set; } = new();
}
public class JobStatus
{
public string Id { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string? PrinterName { get; set; }
public int? CurrentPage { get; set; }
public int? TotalPages { get; set; }
}
+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Drawing.EnableWindowsSupport" Value="true" />
</ItemGroup>
</Project>
+35
View File
@@ -0,0 +1,35 @@
namespace PrintServiceTray;
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
try
{
// Prevent multiple instances
bool createdNew;
using var mutex = new Mutex(true, "LAAPC_PrintServiceTray_Mutex", out createdNew);
if (!createdNew)
{
MessageBox.Show("LAAPC Print Service Tray is already running.", "Already Running",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
ApplicationConfiguration.Initialize();
Application.Run(new TrayApplicationContext());
GC.KeepAlive(mutex);
}
catch (Exception ex)
{
MessageBox.Show($"Fatal error: {ex.Message}\n\n{ex.StackTrace}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
@@ -0,0 +1,136 @@
using System.Text.Json;
using PrintServiceTray.Models;
namespace PrintServiceTray.Services;
public class ConfigurationService
{
private readonly string _globalConfigPath;
private readonly string _localConfigPath;
public ConfigurationService()
{
// Global config: next to CLI executable (C:\LAAPC or wherever installed)
var cliPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"LAAPC"
);
_globalConfigPath = Path.Combine(cliPath, "printer-config.json");
// Local config: user's AppData
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LAAPC"
);
Directory.CreateDirectory(appDataPath);
_localConfigPath = Path.Combine(appDataPath, "printer-config.json");
}
/// <summary>
/// Load configuration (local overrides global)
/// </summary>
public PrinterConfiguration Load()
{
var config = new PrinterConfiguration();
// Load global first
if (File.Exists(_globalConfigPath))
{
try
{
var json = File.ReadAllText(_globalConfigPath);
var globalConfig = JsonSerializer.Deserialize<PrinterConfiguration>(json);
if (globalConfig != null)
{
config = globalConfig;
}
}
catch { }
}
// Load local and merge/override
if (File.Exists(_localConfigPath))
{
try
{
var json = File.ReadAllText(_localConfigPath);
var localConfig = JsonSerializer.Deserialize<PrinterConfiguration>(json);
if (localConfig != null)
{
// Merge: local overrides global for matching document types
foreach (var kvp in localConfig.DocumentTypes)
{
config.DocumentTypes[kvp.Key] = kvp.Value;
}
}
}
catch { }
}
return config;
}
/// <summary>
/// Save configuration to global or local
/// </summary>
public void Save(PrinterConfiguration config, bool saveGlobal)
{
var path = saveGlobal ? _globalConfigPath : _localConfigPath;
// Ensure directory exists
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var json = JsonSerializer.Serialize(config, options);
File.WriteAllText(path, json);
}
/// <summary>
/// Get all installed printers
/// </summary>
public List<string> GetInstalledPrinters()
{
var printers = new List<string>();
try
{
foreach (string printerName in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
{
printers.Add(printerName);
}
}
catch { }
return printers;
}
/// <summary>
/// Get available trays for a printer
/// </summary>
public List<(int Number, string Name)> GetPrinterTrays(string printerName)
{
var trays = new List<(int, string)>();
try
{
var printerSettings = new System.Drawing.Printing.PrinterSettings
{
PrinterName = printerName
};
foreach (System.Drawing.Printing.PaperSource source in printerSettings.PaperSources)
{
trays.Add((source.RawKind, source.SourceName));
}
}
catch { }
return trays;
}
public string GlobalConfigPath => _globalConfigPath;
public string LocalConfigPath => _localConfigPath;
}
@@ -0,0 +1,62 @@
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using PrintServiceTray.Models;
namespace PrintServiceTray.Services;
public class ServiceClient
{
private readonly string _pipeName;
private readonly int _timeout;
public ServiceClient(string pipeName = "PrintServicePipe", int timeoutMs = 5000)
{
_pipeName = pipeName;
_timeout = timeoutMs;
}
public async Task<QueueStatusResponse> GetQueueStatusAsync()
{
try
{
using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut);
await pipe.ConnectAsync(_timeout);
var request = new QueueStatusRequest();
var requestJson = JsonSerializer.Serialize(request);
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
await pipe.WriteAsync(requestBytes);
await pipe.FlushAsync();
var buffer = new byte[4096];
var bytesRead = await pipe.ReadAsync(buffer);
var responseJson = Encoding.UTF8.GetString(buffer, 0, bytesRead);
var response = JsonSerializer.Deserialize<QueueStatusResponse>(responseJson);
return response ?? new QueueStatusResponse { Success = false, Message = "Invalid response" };
}
catch (TimeoutException)
{
return new QueueStatusResponse { Success = false, Message = "Service not responding" };
}
catch (Exception ex)
{
return new QueueStatusResponse { Success = false, Message = $"Error: {ex.Message}" };
}
}
public async Task<bool> IsServiceRunningAsync()
{
try
{
var response = await GetQueueStatusAsync();
return response.Success;
}
catch
{
return false;
}
}
}
+206
View File
@@ -0,0 +1,206 @@
using System.Diagnostics;
using PrintServiceTray.Services;
using PrintServiceTray.Models;
namespace PrintServiceTray;
public class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _trayIcon;
private readonly ServiceClient _serviceClient;
private readonly System.Windows.Forms.Timer _refreshTimer;
private readonly ToolStripMenuItem _queueStatusItem;
private readonly ToolStripMenuItem _serviceStatusItem;
public TrayApplicationContext()
{
try
{
_serviceClient = new ServiceClient();
// Create context menu
var contextMenu = new ContextMenuStrip();
// Service status (at top)
_serviceStatusItem = new ToolStripMenuItem("● Service Running")
{
Enabled = false,
Font = new Font(contextMenu.Font, FontStyle.Bold)
};
contextMenu.Items.Add(_serviceStatusItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Queue status section
_queueStatusItem = new ToolStripMenuItem("Queue Status")
{
Enabled = false
};
contextMenu.Items.Add(_queueStatusItem);
var viewQueueItem = new ToolStripMenuItem("View Full Queue...");
viewQueueItem.Click += ViewQueue_Click;
contextMenu.Items.Add(viewQueueItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Configuration
var configureItem = new ToolStripMenuItem("Configure Printers...");
configureItem.Click += Configure_Click;
contextMenu.Items.Add(configureItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Exit
var exitItem = new ToolStripMenuItem("Exit");
exitItem.Click += Exit_Click;
contextMenu.Items.Add(exitItem);
// Create tray icon
_trayIcon = new NotifyIcon
{
Icon = SystemIcons.Information,
ContextMenuStrip = contextMenu,
Visible = true,
Text = "LAAPC Print Service"
};
_trayIcon.DoubleClick += TrayIcon_DoubleClick;
// Show startup notification
_trayIcon.ShowBalloonTip(2000, "LAAPC Print Service", "Tray app started", ToolTipIcon.Info);
// Set up refresh timer (every 2 seconds)
_refreshTimer = new System.Windows.Forms.Timer
{
Interval = 2000
};
_refreshTimer.Tick += RefreshTimer_Tick;
_refreshTimer.Start();
// Initial refresh
_ = RefreshStatusAsync();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to initialize tray app: {ex.Message}\n\n{ex.StackTrace}",
"Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
throw;
}
}
private async void RefreshTimer_Tick(object? sender, EventArgs e)
{
await RefreshStatusAsync();
}
private async Task RefreshStatusAsync()
{
try
{
var status = await _serviceClient.GetQueueStatusAsync();
if (status.Success)
{
_serviceStatusItem.Text = "● Service Running";
_serviceStatusItem.ForeColor = Color.Green;
// Update queue status
if (status.Jobs.Count == 0)
{
_queueStatusItem.Text = "Queue Status: Empty";
_queueStatusItem.DropDownItems.Clear();
}
else
{
_queueStatusItem.Text = $"Queue Status ({status.Jobs.Count} {(status.Jobs.Count == 1 ? "job" : "jobs")})";
_queueStatusItem.Enabled = true;
// Update dropdown items with job list
_queueStatusItem.DropDownItems.Clear();
foreach (var job in status.Jobs.Take(10)) // Show max 10 in menu
{
var statusIcon = job.Status == "Processing" ? "●" : "○";
var jobText = $"{statusIcon} {job.Status}: {job.FileName}";
if (job.PrinterName != null)
{
jobText += $" ({job.PrinterName})";
}
if (job.CurrentPage.HasValue && job.TotalPages.HasValue)
{
jobText += $" - Page {job.CurrentPage}/{job.TotalPages}";
}
var jobItem = new ToolStripMenuItem(jobText) { Enabled = false };
_queueStatusItem.DropDownItems.Add(jobItem);
}
if (status.Jobs.Count > 10)
{
_queueStatusItem.DropDownItems.Add(new ToolStripSeparator());
var moreItem = new ToolStripMenuItem($"... and {status.Jobs.Count - 10} more")
{
Enabled = false
};
_queueStatusItem.DropDownItems.Add(moreItem);
}
}
// Update tooltip
_trayIcon.Text = status.Jobs.Count == 0
? "LAAPC Print Service - Queue empty"
: $"LAAPC Print Service - {status.Jobs.Count} job(s) in queue";
}
else
{
_serviceStatusItem.Text = "○ Service Not Running";
_serviceStatusItem.ForeColor = Color.Red;
_queueStatusItem.Text = "Queue Status: Unavailable";
_queueStatusItem.DropDownItems.Clear();
_trayIcon.Text = "LAAPC Print Service - Not running";
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error refreshing status: {ex.Message}");
}
}
private void TrayIcon_DoubleClick(object? sender, EventArgs e)
{
// Double-click opens full queue view
ViewQueue_Click(sender, e);
}
private void ViewQueue_Click(object? sender, EventArgs e)
{
// TODO: Open queue view window
MessageBox.Show("Queue view window coming soon!", "LAAPC Print Service", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void Configure_Click(object? sender, EventArgs e)
{
var configForm = new Forms.ConfigurationForm();
configForm.ShowDialog();
}
private void Exit_Click(object? sender, EventArgs e)
{
_refreshTimer.Stop();
_trayIcon.Visible = false;
Application.Exit();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_refreshTimer?.Stop();
_refreshTimer?.Dispose();
_trayIcon?.Dispose();
}
base.Dispose(disposing);
}
}