diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9e2c26b..58fa9ba 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -81,6 +81,23 @@ When adding new features: 5. **Update settings UI** - Add configuration options when appropriate 6. **Document in code** - Add XML comments for public APIs +## Icon Usage in WPF + +**IMPORTANT**: When adding icons or symbols to WPF UI: + +- **Always use Segoe MDL2 Assets font** instead of Unicode symbols +- Set `FontFamily="Segoe MDL2 Assets"` on TextBlock/TextBox controls +- Use hex codes like `` for chevron-right, `` for chevron-up, etc. +- **Never use plain Unicode characters** like ?, ?, ? - they display as `?` in the app +- See [Segoe MDL2 Assets icons list](https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-ui-symbol-font) + +Common icons used in GhostDraw: +- `` - ChevronRight (?) +- `` - ChevronUp (?) +- `` - ChevronDown (?) +- `` - Delete (??) +- `` - CheckMark (?) + ## Resources - [Low-Level Keyboard Hook Documentation](https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c53296..de0e1fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,8 @@ name: CI Build on: push: branches: [main] - pull_request: - branches: [main] + # pull_request: + # branches: [main] permissions: contents: write # Required for creating draft releases diff --git a/GhostDraw.sln b/GhostDraw.sln index 5b5b8ee..2ff61e2 100644 --- a/GhostDraw.sln +++ b/GhostDraw.sln @@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GhostDraw", "Src\GhostDraw\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GhostDraw.Tests", "Tests\GhostDraw.Tests\GhostDraw.Tests.csproj", "{2C665F98-F0C5-0A3F-9461-BB8A1A593CE2}" EndProject -Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "GhostDraw.Installer", "Installer\GhostDraw.Installer.wixproj", "{E1C8B89D-3F4E-4A6F-9C8E-5D7A8B9C0D1E}" +Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "GhostDraw.Installer", "Installer\GhostDraw.Installer.wixproj", "{E1C8B89D-3F4E-4A6F-9C8E-5D7A8B9C0D1E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" ProjectSection(SolutionItems) = preProject @@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{02EA EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{611A7057-6E33-46CA-BF31-21DA1B6765CF}" ProjectSection(SolutionItems) = preProject + docs\Settings.png = docs\Settings.png docs\TODO.md = docs\TODO.md EndProjectSection EndProject diff --git a/README.md b/README.md index 301f5ff..4761601 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,291 @@ -# GhostDraw +
-A lightweight Windows desktop application that allows you to draw directly on your screen using a simple keyboard hotkey and mouse input. +# 👻 GhostDraw -## Features +### Draw on Your Screen, Anywhere, Anytime -- **Global Hotkey**: Toggle drawing mode with `Ctrl+Alt+D` -- **Fullscreen Overlay**: Draw on top of any application -- **System Tray Integration**: Runs quietly in the background -- **Emergency Exit**: Press `ESC` to quickly hide the overlay -- **Transparent Drawing**: Overlay is completely transparent when not actively drawing +[![.NET 8](https://img.shields.io/badge/.NET-8.0-512BD4?logo=dotnet)](https://dotnet.microsoft.com/) +[![Windows](https://img.shields.io/badge/Windows-10%2B-0078D6?logo=windows)](https://www.microsoft.com/windows) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![GitHub release](https://img.shields.io/github/v/release/RuntimeRascal/ghost-draw)](https://github.com/RuntimeRascal/ghost-draw/releases) -## Requirements +**GhostDraw** is a lightweight, cyberpunk-themed Windows desktop application that lets you draw directly on your screen with a simple keyboard hotkey. Perfect for presentations, tutorials, collaboration, or just having fun! -- Windows 10 or later -- .NET 8 Runtime +[**Download Latest Release**](https://github.com/RuntimeRascal/ghost-draw/releases) | [**Report Bug**](https://github.com/RuntimeRascal/ghost-draw/issues) | [**Request Feature**](https://github.com/RuntimeRascal/ghost-draw/issues) -## Installation +![GhostDraw Demo](docs/Demo.gif) + -1. Download the latest release from the [Releases](https://github.com/RuntimeRascal/ghost-draw/releases) page -2. Extract the ZIP file to a folder of your choice -3. Run `GhostDraw.exe` -4. The application will start minimized to the system tray +
-## Usage +--- + +## ✨ Features + +### 🎨 **Drawing Tools** +- **Customizable Color Palette** - Create your own color collection and cycle through them while drawing +- **Variable Brush Thickness** - Adjust brush size from 1-100px with configurable min/max ranges +- **Smooth Drawing** - High-performance rendering for fluid strokes +- **Mouse Wheel Control** - Change brush thickness on-the-fly while drawing + +### ⌨️ **Hotkey System** +- **Global Hotkey** - Activate drawing mode from any application +- **Customizable Shortcuts** - Define your own key combinations +- **Two Draw Modes**: + - **Toggle Mode** - Press once to start, press again to stop + - **Hold Mode** - Draw only while holding the hotkey + +### 🖥️ **User Experience** +- **Transparent Overlay** - Draw on top of any application without blocking input +- **System Tray Integration** - Runs quietly in the background +- **Emergency Exit** - Press `ESC` to instantly hide the overlay +- **Right-Click Color Cycling** - Quickly switch between your palette colors +- **Position-Numbered Palette** - Easily organize and reorder your favorite colors + +### 🛡️ **Safety & Stability** +- **Fail-Safe Design** - Won't lock you out of your system if it crashes +- **Fast Input Processing** - All hooks complete in < 5ms for responsive system +- **Graceful Error Handling** - Protected critical paths ensure stability +- **Proper Resource Cleanup** - Memory and hooks always released properly + +--- + +## 📸 Screenshots + +### Settings Window +![Settings Window](docs/Settings.png) + +Customize every aspect of GhostDraw with the intuitive settings panel: +- Color palette management with add/remove/reorder +- Brush thickness range configuration +- Hotkey customization +- Drawing mode selection +- Logging level control + +### Active Drawing Mode +![Drawing Mode](docs/DrawingMode.png) + + +### Color Palette Cycling +![Color Cycling](docs/ColorCycling.png) + + +### System Tray Integration +![System Tray](docs/SystemTray.png) + + +--- + +## 🚀 Getting Started + +### Requirements + +- **Operating System**: Windows 10 or later +- **.NET Runtime**: [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) (automatically included in releases) + +### Installation + +1. **Download** the latest release from the [Releases page](https://github.com/RuntimeRascal/ghost-draw/releases) +2. **Extract** the ZIP file to your preferred location (e.g., `C:\Program Files\GhostDraw`) +3. **Run** `GhostDraw.exe` +4. The application will start and **minimize to the system tray** 👻 + +> **💡 Tip**: Create a shortcut in your Startup folder to launch GhostDraw automatically when Windows starts! + +--- + +## 🎯 How to Use + +### Basic Drawing + +1. **Activate Drawing Mode** + Press your configured hotkey (default: `Ctrl+Alt+D`) + +2. **Start Drawing** + Click and drag with your **left mouse button** to draw + +3. **Cycle Colors** + **Right-click** while drawing to switch to the next color in your palette + +4. **Adjust Thickness** + Scroll the **mouse wheel** while drawing to change brush size -1. **Start Drawing**: Press `Ctrl+Alt+D` to activate the drawing overlay -2. **Draw**: Click and drag with your mouse to draw on the screen -3. **Exit Drawing Mode**: Press `Ctrl+Alt+D` again or press `ESC` -4. **Exit Application**: Right-click the system tray icon and select "Exit" +5. **Exit Drawing Mode** + - Press the hotkey again (toggle mode) + - Press `ESC` for emergency exit + - Release the hotkey (hold mode) -## Building from Source +### Customizing Your Palette -### Prerequisites +1. **Open Settings** + Right-click the system tray icon → **Settings** -- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) -- Visual Studio 2022 (recommended) or any .NET-compatible IDE +2. **Edit Palette Colors** + Click to expand the color palette section -### Build Steps +3. **Add Colors** + Click **+ ADD COLOR** and choose your color -```bash -# Clone the repository -git clone https://github.com/RuntimeRascal/ghost-draw.git -cd ghost-draw +4. **Reorder Colors** + Use the **⬆ Up** and **⬇ Down** buttons to arrange colors + (Press ⬆ on first item to move it to end, ⬇ on last to move to start) -# Navigate to the source directory -cd src +5. **Remove Colors** + Click the **🗑️ Delete** button (minimum 1 color required) -# Restore dependencies -dotnet restore +6. **Select Active Color** + Click any color swatch to set it as your active brush color + (Indicated by pink border and ✓ checkmark) -# Build the project -dotnet build --configuration Release +### Configuring Hotkeys -# Run the application -dotnet run --configuration Release +1. **Open Settings** → Navigate to **HOTKEY** section +2. **Click** the hotkey input field +3. **Press** your desired key combination +4. **Save & Close** to apply changes + +> **⚠️ Note**: Some key combinations may conflict with system shortcuts or other applications. + +### Drawing Modes + +- **Lock Mode (Toggle)**: Press hotkey once to start drawing, press again to stop +- **Hold Mode**: Drawing is only active while the hotkey is held down + +Change modes in **Settings** → **MODE** section + +--- + +## ⚙️ Configuration + +All settings are automatically saved to: +`%LOCALAPPDATA%\GhostDraw\settings.json` + +### Default Settings + +```json +{ + "activeBrush": "#FFFFFF", + "brushThickness": 3, + "minBrushThickness": 1, + "maxBrushThickness": 20, + "hotkeyVirtualKeys": [162, 164, 68], + "lockDrawingMode": false, + "colorPalette": [ + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00", + "#FF00FF", + "#00FFFF", + "#FFFFFF", + "#000000", + "#FFA500", + "#800080" + ] +} ``` -## Architecture +--- + +## 🏗️ Architecture -GhostDraw is built using modern .NET practices: +GhostDraw is built with modern .NET practices and WPF: +### Technology Stack - **WPF** - UI framework and overlay rendering -- **Global Windows Hooks** - Keyboard and mouse input capture +- **Global Windows Hooks** - Low-level keyboard/mouse capture - **Dependency Injection** - Microsoft.Extensions.DependencyInjection - **Structured Logging** - Serilog + Microsoft.Extensions.Logging - **.NET 8** - Latest LTS framework -### Project Structure +### Design Principles -``` -ghost-draw/ -??? src/ # Main application source code -? ??? GhostDraw.csproj # Project file -? ??? ... # Application code -??? tests/ # Unit and integration tests -??? docs/ # Additional documentation -??? .github/ # GitHub configuration -? ??? copilot-instructions.md # AI assistant guidelines -??? README.md # This file -``` +✅ **Safety First** - User must never be locked out of their system +✅ **Fast Hooks** - All hook callbacks complete in < 5ms +✅ **Graceful Failure** - Exceptions are caught, logged, and handled +✅ **Clean Resources** - Hooks and resources always released on exit +✅ **Structured Logging** - Comprehensive diagnostics without performance impact -## Safety & Stability +--- -GhostDraw intercepts global keyboard and mouse input. The application is designed with safety as the top priority: +## 🔧 Troubleshooting -- **Fail-Safe Design**: Crashes won't lock you out of your system -- **Emergency Exit**: ESC key always hides the overlay -- **Fast Hook Processing**: All input hooks complete in < 5ms -- **Graceful Error Handling**: All critical paths are protected with try-catch blocks -- **Proper Cleanup**: Resources are always released on exit +### Drawing overlay doesn't appear +- Check if the hotkey is conflicting with another application +- Try changing the hotkey in Settings +- Ensure .NET 8 runtime is installed -## Contributing +### Application won't start +- Right-click `GhostDraw.exe` → **Run as administrator** +- Check Windows Event Viewer for crash logs +- Review logs in `%LOCALAPPDATA%\GhostDraw\logs\` -Contributions are welcome! Please read the [Copilot Instructions](.github/copilot-instructions.md) for detailed guidelines on: +### Drawing is laggy or slow +- Reduce brush thickness range (lower max value) +- Close other resource-intensive applications +- Check CPU usage in Task Manager -- Safety requirements -- Code style and conventions -- Architecture patterns -- Testing considerations +### Hotkey doesn't work +- Verify the key combination isn't used by Windows or other apps +- Try a different key combination +- Restart GhostDraw after changing hotkeys -### Development Guidelines +--- -1. **Safety First**: Never compromise user system stability -2. **Test Thoroughly**: Especially edge cases (multi-monitor, high DPI, rapid input) -3. **Log Appropriately**: Use structured logging with proper levels -4. **Handle Errors**: Catch and log exceptions gracefully -5. **Keep It Fast**: Hook callbacks must be lightning fast (< 5ms) +## 📋 Logging -## Roadmap +GhostDraw uses structured logging with configurable levels: -Future features under consideration: +- **Verbose** - Everything (very noisy) +- **Debug** - Detailed diagnostic information +- **Information** - General application flow (default) +- **Warning** - Unexpected but recoverable situations +- **Error** - Errors that don't crash the app +- **Fatal** - Critical errors requiring restart -- [ ] Brush customization (color, thickness, opacity) -- [ ] Stroke persistence (save/load drawings) -- [ ] Undo/redo functionality -- [ ] Screenshot integration -- [ ] Multiple drawing tools (pen, highlighter, eraser) -- [ ] Shape tools (line, rectangle, circle) +Logs are stored in: `%LOCALAPPDATA%\GhostDraw\logs\` -## Known Issues +Access logs via **Settings** → **LOGS** → **OPEN FOLDER** -- None currently reported +--- + +## 🤝 Contributing + +We welcome contributions! Whether it's: +- 🐛 Bug reports +- 💡 Feature suggestions +- 📝 Documentation improvements +- 💻 Code contributions + +Please check out our [Contributing Guidelines](CONTRIBUTING.md) to get started. + +--- -## License +## 📄 License -[Specify your license here - e.g., MIT, GPL, etc.] +This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. -## Acknowledgments +--- + +## 🙏 Acknowledgments -Built with: -- [WPF](https://github.com/dotnet/wpf) - UI framework -- [Serilog](https://serilog.net/) - Logging library -- [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection) - DI container +- **Segoe MDL2 Assets** - Microsoft's icon font +- **Serilog** - Flexible logging framework +- **WPF Community** - Inspiration and best practices -## Support +--- -If you encounter any issues or have questions: +## 📬 Contact -1. Check the [Issues](https://github.com/RuntimeRascal/ghost-draw/issues) page -2. Create a new issue with detailed information -3. Include logs from `%LOCALAPPDATA%\GhostDraw\logs\` if applicable +- **Issues**: [GitHub Issues](https://github.com/RuntimeRascal/ghost-draw/issues) +- **Discussions**: [GitHub Discussions](https://github.com/RuntimeRascal/ghost-draw/discussions) --- -**?? Important**: This application uses global keyboard and mouse hooks. Use responsibly and ensure you understand the [safety guidelines](.github/copilot-instructions.md) if modifying the code. +
+ +**Made with 💜 for creators, presenters, and anyone who loves drawing on their screen** + +⭐ **Star this repo if you find it useful!** ⭐ + +
diff --git a/Src/GhostDraw/App.xaml b/Src/GhostDraw/App.xaml index 241cb7e..7fe57f1 100644 --- a/Src/GhostDraw/App.xaml +++ b/Src/GhostDraw/App.xaml @@ -3,6 +3,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:GhostDraw"> - + + + + + diff --git a/Src/GhostDraw/App.xaml.cs b/Src/GhostDraw/App.xaml.cs index b402f62..7d05451 100644 --- a/Src/GhostDraw/App.xaml.cs +++ b/Src/GhostDraw/App.xaml.cs @@ -60,7 +60,7 @@ protected override void OnStartup(StartupEventArgs e) // Log loaded settings var settings = _appSettings.CurrentSettings; _logger.LogInformation("Loaded settings - Color: {Color}, Thickness: {Thickness}, Hotkey: {Hotkey}, LockMode: {LockMode}", - settings.BrushColor, settings.BrushThickness, + settings.ActiveBrush, settings.BrushThickness, settings.HotkeyDisplayName, settings.LockDrawingMode); @@ -115,9 +115,7 @@ protected override void OnStartup(StartupEventArgs e) try { _logger?.LogDebug("Opening settings window"); - var logger = _serviceProvider!.GetRequiredService>(); - var loggerFactory = _serviceProvider!.GetRequiredService(); - var settingsWindow = new SettingsWindow(_loggingSettings!, _appSettings!, logger, loggerFactory); + var settingsWindow = _serviceProvider!.GetRequiredService(); settingsWindow.ShowDialog(); } catch (Exception ex) @@ -185,11 +183,14 @@ protected override void OnStartup(StartupEventArgs e) } } - private void UpdateLogLevelMenuChecks(ToolStripMenuItem logLevelMenu, LogEventLevel selectedLevel) + private static void UpdateLogLevelMenuChecks(ToolStripMenuItem logLevelMenu, LogEventLevel selectedLevel) { foreach (ToolStripMenuItem item in logLevelMenu.DropDownItems) { - item.Checked = item.Text.StartsWith(selectedLevel.ToString()); + if (item.Text != null) + { + item.Checked = item.Text.StartsWith(selectedLevel.ToString()); + } } } diff --git a/Src/GhostDraw/Core/AppSettings.cs b/Src/GhostDraw/Core/AppSettings.cs index 8a35e98..4cb3fe1 100644 --- a/Src/GhostDraw/Core/AppSettings.cs +++ b/Src/GhostDraw/Core/AppSettings.cs @@ -8,10 +8,10 @@ namespace GhostDraw.Core; public class AppSettings { /// - /// Brush color in hex format (e.g., "#FF0000" for red) + /// Active brush color from the palette in hex format (e.g., "#FF0000" for red) /// - [JsonPropertyName("brushColor")] - public string BrushColor { get; set; } = "#FF0000"; + [JsonPropertyName("activeBrush")] + public string ActiveBrush { get; set; } = "#FF0000"; /// /// Brush thickness in pixels @@ -82,7 +82,7 @@ public AppSettings Clone() { return new AppSettings { - BrushColor = BrushColor, + ActiveBrush = ActiveBrush, BrushThickness = BrushThickness, MinBrushThickness = MinBrushThickness, MaxBrushThickness = MaxBrushThickness, diff --git a/Src/GhostDraw/Core/ServiceConfiguration.cs b/Src/GhostDraw/Core/ServiceConfiguration.cs index 73867f6..5e7fa7d 100644 --- a/Src/GhostDraw/Core/ServiceConfiguration.cs +++ b/Src/GhostDraw/Core/ServiceConfiguration.cs @@ -7,6 +7,7 @@ using GhostDraw.Managers; using GhostDraw.Helpers; using GhostDraw.Views; +using GhostDraw.ViewModels; namespace GhostDraw.Core; @@ -61,6 +62,10 @@ public static ServiceProvider ConfigureServices() // Register GlobalExceptionHandler AFTER its dependencies services.AddSingleton(); + // Register ViewModels and Views + services.AddTransient(); + services.AddTransient(); + _serviceProvider = services.BuildServiceProvider(); // Load saved log level from settings diff --git a/Src/GhostDraw/Services/AppSettingsService.cs b/Src/GhostDraw/Services/AppSettingsService.cs index f476575..b9b3629 100644 --- a/Src/GhostDraw/Services/AppSettingsService.cs +++ b/Src/GhostDraw/Services/AppSettingsService.cs @@ -20,6 +20,7 @@ public class AppSettingsService public event EventHandler? LockDrawingModeChanged; public event EventHandler<(double min, double max)>? BrushThicknessRangeChanged; public event EventHandler>? HotkeyChanged; + public event EventHandler>? ColorPaletteChanged; public AppSettingsService(ILogger logger) { @@ -53,13 +54,21 @@ private AppSettings LoadSettings() { _logger.LogInformation("Loading settings from {Path}", _settingsFilePath); string json = File.ReadAllText(_settingsFilePath); + + // Try to deserialize as current format var settings = JsonSerializer.Deserialize(json); if (settings != null) { + // Migrate old settings format if needed + settings = MigrateSettings(json, settings); + _logger.LogInformation("Settings loaded successfully"); - _logger.LogDebug("Brush Color: {Color}, Thickness: {Thickness}, Hotkey: {Hotkey}", - settings.BrushColor, settings.BrushThickness, settings.HotkeyDisplayName); + _logger.LogDebug("Active Brush: {Color}, Thickness: {Thickness}, Hotkey: {Hotkey}", + settings.ActiveBrush, settings.BrushThickness, settings.HotkeyDisplayName); + + // Save migrated settings to update file format + SaveSettings(settings); return settings; } } @@ -76,6 +85,40 @@ private AppSettings LoadSettings() } } + /// + /// Migrates settings from old format to new format + /// + private AppSettings MigrateSettings(string json, AppSettings settings) + { + try + { + // Parse as JsonDocument to check for old properties + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Migrate old "brushColor" to new "activeBrush" + if (root.TryGetProperty("brushColor", out var brushColorProp)) + { + var oldBrushColor = brushColorProp.GetString(); + if (!string.IsNullOrEmpty(oldBrushColor)) + { + settings.ActiveBrush = oldBrushColor; + _logger.LogInformation("Migrated 'brushColor' to 'activeBrush': {Color}", oldBrushColor); + } + } + + // Future migrations can be added here + // Example: if (root.TryGetProperty("oldProperty", out var oldProp)) { ... } + + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to migrate settings, using loaded values as-is"); + } + + return settings; + } + /// /// Saves settings to disk /// @@ -102,12 +145,12 @@ private void SaveSettings(AppSettings settings) } /// - /// Updates brush color and persists to disk + /// Updates active brush color and persists to disk /// - public void SetBrushColor(string colorHex) + public void SetActiveBrush(string colorHex) { - _logger.LogInformation("Setting brush color to {Color}", colorHex); - _currentSettings.BrushColor = colorHex; + _logger.LogInformation("Setting active brush to {Color}", colorHex); + _currentSettings.ActiveBrush = colorHex; SaveSettings(_currentSettings); // Raise event to notify UI @@ -195,14 +238,14 @@ public void SetHotkey(List virtualKeys) /// public string GetNextColor() { - var currentIndex = _currentSettings.ColorPalette.IndexOf(_currentSettings.BrushColor); + var currentIndex = _currentSettings.ColorPalette.IndexOf(_currentSettings.ActiveBrush); var nextIndex = (currentIndex + 1) % _currentSettings.ColorPalette.Count; var nextColor = _currentSettings.ColorPalette[nextIndex]; _logger.LogDebug("Cycling color from {CurrentColor} to {NextColor}", - _currentSettings.BrushColor, nextColor); + _currentSettings.ActiveBrush, nextColor); - SetBrushColor(nextColor); + SetActiveBrush(nextColor); return nextColor; } @@ -216,6 +259,69 @@ public void SetLogLevel(string logLevel) SaveSettings(_currentSettings); } + /// + /// Adds a color to the palette + /// + public void AddColorToPalette(string colorHex) + { + if (string.IsNullOrWhiteSpace(colorHex)) + { + _logger.LogWarning("Attempted to add invalid color to palette"); + return; + } + + if (_currentSettings.ColorPalette.Contains(colorHex)) + { + _logger.LogDebug("Color {Color} already exists in palette", colorHex); + return; + } + + _logger.LogInformation("Adding color {Color} to palette", colorHex); + _currentSettings.ColorPalette.Add(colorHex); + SaveSettings(_currentSettings); + ColorPaletteChanged?.Invoke(this, new List(_currentSettings.ColorPalette)); + } + + /// + /// Removes a color from the palette + /// + public void RemoveColorFromPalette(string colorHex) + { + if (_currentSettings.ColorPalette.Count <= 1) + { + _logger.LogWarning("Cannot remove last color from palette"); + return; + } + + if (!_currentSettings.ColorPalette.Contains(colorHex)) + { + _logger.LogDebug("Color {Color} not found in palette", colorHex); + return; + } + + _logger.LogInformation("Removing color {Color} from palette", colorHex); + _currentSettings.ColorPalette.Remove(colorHex); + SaveSettings(_currentSettings); + ColorPaletteChanged?.Invoke(this, new List(_currentSettings.ColorPalette)); + } + + /// + /// Updates the entire color palette + /// + public void SetColorPalette(List colors) + { + if (colors == null || colors.Count == 0) + { + _logger.LogWarning("Cannot set empty color palette"); + return; + } + + _logger.LogInformation("Setting color palette with {Count} colors", colors.Count); + _currentSettings.ColorPalette = new List(colors); + SaveSettings(_currentSettings); + ColorPaletteChanged?.Invoke(this, new List(_currentSettings.ColorPalette)); + } + /// /// Resets all settings to default values /// diff --git a/Src/GhostDraw/Styles/CyberpunkStyles.xaml b/Src/GhostDraw/Styles/CyberpunkStyles.xaml new file mode 100644 index 0000000..d4dd3eb --- /dev/null +++ b/Src/GhostDraw/Styles/CyberpunkStyles.xaml @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Src/GhostDraw/ViewModels/SettingsViewModel.cs b/Src/GhostDraw/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..00b37ca --- /dev/null +++ b/Src/GhostDraw/ViewModels/SettingsViewModel.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Logging; +using GhostDraw.Services; + +namespace GhostDraw.ViewModels; + +/// +/// ViewModel for the SettingsWindow that aggregates all required services. +/// This enables proper MVVM pattern with dependency injection while keeping +/// UserControls visible in the XAML designer. +/// +public class SettingsViewModel +{ + /// + /// Service for managing application settings (brush, hotkey, mode, etc.) + /// + public AppSettingsService AppSettings { get; } + + /// + /// Service for managing logging configuration + /// + public LoggingSettingsService LoggingSettings { get; } + + /// + /// Factory for creating loggers for child controls + /// + public ILoggerFactory LoggerFactory { get; } + + public SettingsViewModel( + AppSettingsService appSettings, + LoggingSettingsService loggingSettings, + ILoggerFactory loggerFactory) + { + AppSettings = appSettings; + LoggingSettings = loggingSettings; + LoggerFactory = loggerFactory; + } +} diff --git a/Src/GhostDraw/Views/OverlayWindow.xaml.cs b/Src/GhostDraw/Views/OverlayWindow.xaml.cs index 886faad..839a6a0 100644 --- a/Src/GhostDraw/Views/OverlayWindow.xaml.cs +++ b/Src/GhostDraw/Views/OverlayWindow.xaml.cs @@ -80,8 +80,8 @@ private void UpdateCursor() try { var settings = _appSettings.CurrentSettings; - this.Cursor = _cursorHelper.CreateColoredPencilCursor(settings.BrushColor); - _logger.LogDebug("Updated cursor with color {Color}", settings.BrushColor); + this.Cursor = _cursorHelper.CreateColoredPencilCursor(settings.ActiveBrush); + _logger.LogDebug("Updated cursor with color {Color}", settings.ActiveBrush); } catch (Exception ex) { @@ -184,11 +184,11 @@ private void StartNewStroke(System.Windows.Point startPoint) try { strokeBrush = new SolidColorBrush( - (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(settings.BrushColor)); + (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(settings.ActiveBrush)); } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to parse brush color {Color}, using default red", settings.BrushColor); + _logger.LogWarning(ex, "Failed to parse brush color {Color}, using default red", settings.ActiveBrush); strokeBrush = System.Windows.Media.Brushes.Red; } @@ -204,7 +204,7 @@ private void StartNewStroke(System.Windows.Point startPoint) _currentStroke.Points.Add(startPoint); DrawingCanvas.Children.Add(_currentStroke); _logger.LogDebug("Stroke added to canvas with color {Color} and thickness {Thickness}, total strokes: {StrokeCount}", - settings.BrushColor, settings.BrushThickness, DrawingCanvas.Children.Count); + settings.ActiveBrush, settings.BrushThickness, DrawingCanvas.Children.Count); } private void AddPointToStroke(System.Windows.Point point) diff --git a/Src/GhostDraw/Views/SettingsWindow.xaml b/Src/GhostDraw/Views/SettingsWindow.xaml index bf2e7f4..dfc8b1a 100644 --- a/Src/GhostDraw/Views/SettingsWindow.xaml +++ b/Src/GhostDraw/Views/SettingsWindow.xaml @@ -1,6 +1,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -386,6 +56,7 @@ + @@ -418,7 +89,7 @@ Orientation="Horizontal" Margin="24,0,0,0" VerticalAlignment="Center"> - - + + Margin="28,12,28,12"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +