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:
Generated
+38
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PrintServiceTray;
|
||||
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user