diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml
new file mode 100644
index 0000000..d94fd92
--- /dev/null
+++ b/.github/workflows/build-and-release.yml
@@ -0,0 +1,80 @@
+name: Build and Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ name: Build ${{ matrix.rid }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - rid: win-x64
+ artifact_name: Ink-Canvas-Next-win-x64
+ output_name: Ink-Canvas-Next-win-x64
+ # - rid: linux-x64
+ # artifact_name: Ink-Canvas-Next-linux-x64
+ # output_name: Ink-Canvas-Next-linux-x64
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Restore
+ run: dotnet restore "Ink Canvas Next.slnx"
+
+ - name: Test
+ run: dotnet test "Ink Canvas Next.slnx" --configuration Release --no-restore
+
+ - name: Publish
+ run: |
+ dotnet publish Ink-Canvas-Next/Ink-Canvas-Next.csproj \
+ --configuration Release \
+ --runtime ${{ matrix.rid }} \
+ --self-contained false \
+ -p:PublishSingleFile=true \
+ -p:IncludeNativeLibrariesForSelfExtract=true \
+ -o publish/${{ matrix.output_name }}
+
+ - name: Archive package
+ run: |
+ cd publish
+ tar -czf ${{ matrix.output_name }}.tar.gz ${{ matrix.output_name }}
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.artifact_name }}
+ path: publish/${{ matrix.output_name }}.tar.gz
+
+ release:
+ name: Create GitHub Release
+ runs-on: ubuntu-latest
+ needs: build
+ if: startsWith(github.ref, 'refs/tags/v')
+
+ steps:
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: release-assets
+
+ - name: Create release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
+ files: |
+ release-assets/**/*.tar.gz
diff --git a/.github/workflows/qoder-assistant.yml.disabled b/.github/workflows/qoder-assistant.yml
similarity index 100%
rename from .github/workflows/qoder-assistant.yml.disabled
rename to .github/workflows/qoder-assistant.yml
diff --git a/.github/workflows/qoder-auto-review.yml.disabled b/.github/workflows/qoder-auto-review.yml
similarity index 100%
rename from .github/workflows/qoder-auto-review.yml.disabled
rename to .github/workflows/qoder-auto-review.yml
diff --git a/.gitignore b/.gitignore
index 39d4e29..2f71908 100644
--- a/.gitignore
+++ b/.gitignore
@@ -423,3 +423,4 @@ FodyWeavers.xsd
.kiro/
.vscode/
.idea/
+.workbuddy/
\ No newline at end of file
diff --git a/Ink-Canvas-Next.Tests/AvaloniaTestCollection.cs b/Ink-Canvas-Next.Tests/AvaloniaTestCollection.cs
deleted file mode 100644
index 9b4e9c0..0000000
--- a/Ink-Canvas-Next.Tests/AvaloniaTestCollection.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Avalonia;
-using Avalonia.Headless;
-using Avalonia.Threading;
-using Xunit;
-
-namespace Ink_Canvas_Next.Tests;
-
-///
-/// Collection definition for Avalonia tests
-/// Ensures all tests in this collection share the same Avalonia instance
-///
-[CollectionDefinition("Avalonia")]
-public class AvaloniaTestCollection : ICollectionFixture
-{
-}
-
-///
-/// Shared fixture for Avalonia initialization
-/// Initializes Avalonia once for all tests in the collection
-///
-public class AvaloniaFixture
-{
- public AvaloniaFixture()
- {
- // Initialize Avalonia once
- AppBuilder.Configure()
- .UseHeadless(new AvaloniaHeadlessPlatformOptions())
- .SetupWithoutStarting();
- }
-}
-
-///
-/// Test application for Avalonia headless mode
-///
-internal class TestApplication : Application
-{
-}
diff --git a/Ink-Canvas-Next.Tests/Ink-Canvas-Next.Tests.csproj b/Ink-Canvas-Next.Tests/Ink-Canvas-Next.Tests.csproj
deleted file mode 100644
index c8a398b..0000000
--- a/Ink-Canvas-Next.Tests/Ink-Canvas-Next.Tests.csproj
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- false
- true
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Ink-Canvas-Next.Tests/Services/CanvasServicePropertyTests.cs b/Ink-Canvas-Next.Tests/Services/CanvasServicePropertyTests.cs
deleted file mode 100644
index 22dd334..0000000
--- a/Ink-Canvas-Next.Tests/Services/CanvasServicePropertyTests.cs
+++ /dev/null
@@ -1,192 +0,0 @@
-using System;
-using System.IO;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-using Avalonia;
-using Avalonia.Headless;
-using Avalonia.Media.Imaging;
-using Avalonia.Platform;
-using FsCheck;
-using FsCheck.Xunit;
-using Ink_Canvas_Next.Services;
-using Xunit;
-
-namespace Ink_Canvas_Next.Tests.Services;
-
-///
-/// Property-based tests for CanvasService
-/// Uses FsCheck to verify universal properties across many inputs
-///
-[Collection("Avalonia")]
-public class CanvasServicePropertyTests : IDisposable
-{
- private readonly CanvasService _service;
- private readonly string _testDirectory;
-
- public CanvasServicePropertyTests()
- {
- _service = new CanvasService();
-
- // Create a temporary test directory
- var picturesPath = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
- _testDirectory = Path.Combine(picturesPath, "InkCanvasScreenshots");
- }
-
- public void Dispose()
- {
- // Cleanup: Remove test files created during tests
- if (Directory.Exists(_testDirectory))
- {
- try
- {
- var files = Directory.GetFiles(_testDirectory, "InkCanvas_*.png");
- foreach (var file in files)
- {
- try
- {
- File.Delete(file);
- }
- catch
- {
- // Ignore cleanup errors
- }
- }
- }
- catch
- {
- // Ignore cleanup errors
- }
- }
- }
-
- ///
- /// Property 1: Screenshot file format and location
- /// For any canvas state, when saving a screenshot, the resulting file should be in PNG format,
- /// located in the specified image folder, and the file should exist and be readable.
- /// **Validates: Requirements 1.2**
- ///
- [Property(MaxTest = 50)]
- public Property SavedFile_ShouldBePngInCorrectLocation()
- {
- return Prop.ForAll(
- ArbBitmap(),
- (bitmap) =>
- {
- // Act: Save the canvas (synchronously)
- var filePath = _service.SaveCanvasToFileAsync(bitmap).GetAwaiter().GetResult();
-
- // Assert: File should exist
- Assert.True(File.Exists(filePath), "File should exist after saving");
-
- // Assert: File should be in the correct directory
- var directory = Path.GetDirectoryName(filePath);
- Assert.Equal(_testDirectory, directory);
-
- // Assert: File should have .png extension
- var extension = Path.GetExtension(filePath);
- Assert.Equal(".png", extension);
-
- // Assert: File should be readable (non-zero size)
- var fileInfo = new FileInfo(filePath);
- Assert.True(fileInfo.Length > 0, "File should have content");
-
- // Cleanup
- File.Delete(filePath);
-
- return true;
- }
- );
- }
-
- ///
- /// Property 2: Filename uniqueness and format
- /// For any sequence of consecutive save operations, the generated filenames should follow
- /// the "InkCanvas_YYYYMMDD_HHMMSS.png" format, and each filename should be unique (via timestamp).
- /// **Validates: Requirements 1.3**
- ///
- [Property(MaxTest = 20)]
- public Property ConsecutiveSaves_ShouldHaveUniqueFilenamesWithCorrectFormat()
- {
- return Prop.ForAll(
- Gen.Choose(2, 3).ToArbitrary(), // Test with 2-3 consecutive saves
- (saveCount) =>
- {
- var filePaths = new List();
- var fileNames = new HashSet();
-
- try
- {
- for (int i = 0; i < saveCount; i++)
- {
- // Create a small test bitmap
- var bitmap = CreateTestBitmap(10, 10);
-
- // Save the canvas (synchronously)
- var filePath = _service.SaveCanvasToFileAsync(bitmap).GetAwaiter().GetResult();
- filePaths.Add(filePath);
-
- var fileName = Path.GetFileName(filePath);
-
- // Assert: Filename should follow the correct format
- var pattern = @"^InkCanvas_\d{8}_\d{6}\.png$";
- Assert.Matches(pattern, fileName);
-
- // Assert: Filename should be unique
- Assert.DoesNotContain(fileName, fileNames);
- fileNames.Add(fileName);
-
- // Small delay to ensure timestamp changes
- if (i < saveCount - 1)
- {
- Task.Delay(1100).GetAwaiter().GetResult(); // Wait just over 1 second
- }
- }
-
- // Assert: All filenames should be unique
- Assert.Equal(saveCount, fileNames.Count);
-
- return true;
- }
- finally
- {
- // Cleanup
- foreach (var filePath in filePaths)
- {
- try
- {
- if (File.Exists(filePath))
- {
- File.Delete(filePath);
- }
- }
- catch
- {
- // Ignore cleanup errors
- }
- }
- }
- }
- );
- }
-
- ///
- /// Helper: Creates an arbitrary bitmap generator for property tests
- ///
- private static Arbitrary ArbBitmap()
- {
- return Gen.Choose(10, 100)
- .Two()
- .Select(t => CreateTestBitmap(t.Item1, t.Item2))
- .ToArbitrary();
- }
-
- ///
- /// Helper: Creates a test bitmap with the specified dimensions
- ///
- private static RenderTargetBitmap CreateTestBitmap(int width, int height)
- {
- var pixelSize = new PixelSize(width, height);
- var dpi = new Vector(96, 96);
- return new RenderTargetBitmap(pixelSize, dpi);
- }
-}
diff --git a/Ink-Canvas-Next.Tests/Services/CanvasServiceTests.cs b/Ink-Canvas-Next.Tests/Services/CanvasServiceTests.cs
deleted file mode 100644
index 6bfddfb..0000000
--- a/Ink-Canvas-Next.Tests/Services/CanvasServiceTests.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-using System;
-using System.IO;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-using Ink_Canvas_Next.Services;
-using Xunit;
-
-namespace Ink_Canvas_Next.Tests.Services;
-
-///
-/// Unit tests for CanvasService
-/// Validates: Requirements 1.3, 1.4, 1.6
-///
-public class CanvasServiceTests : IDisposable
-{
- private readonly CanvasService _service;
- private readonly string _testDirectory;
-
- public CanvasServiceTests()
- {
- _service = new CanvasService();
-
- // Create a temporary test directory
- var picturesPath = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
- _testDirectory = Path.Combine(picturesPath, "InkCanvasScreenshots");
- }
-
- public void Dispose()
- {
- // Cleanup: Remove test files created during tests
- if (Directory.Exists(_testDirectory))
- {
- try
- {
- var files = Directory.GetFiles(_testDirectory, "InkCanvas_*.png");
- foreach (var file in files)
- {
- try
- {
- File.Delete(file);
- }
- catch
- {
- // Ignore cleanup errors
- }
- }
- }
- catch
- {
- // Ignore cleanup errors
- }
- }
- }
-
- ///
- /// Tests that GenerateFileName produces the correct format
- /// Validates: Requirement 1.3 - filename format "InkCanvas_YYYYMMDD_HHMMSS.png"
- ///
- [Fact]
- public void GenerateFileName_ShouldFollowCorrectFormat()
- {
- // Use reflection to access private method
- var method = typeof(CanvasService).GetMethod("GenerateFileName",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- var fileName = method?.Invoke(_service, null) as string;
-
- Assert.NotNull(fileName);
-
- // Verify format: InkCanvas_YYYYMMDD_HHMMSS.png
- var pattern = @"^InkCanvas_\d{8}_\d{6}\.png$";
- Assert.Matches(pattern, fileName);
- }
-
- ///
- /// Tests that consecutive filename generations produce unique names
- /// Validates: Requirement 1.3 - unique filenames through timestamps
- ///
- [Fact]
- public async Task GenerateFileName_ShouldProduceUniqueNames()
- {
- var method = typeof(CanvasService).GetMethod("GenerateFileName",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- var fileName1 = method?.Invoke(_service, null) as string;
-
- // Wait a small amount to ensure timestamp changes
- await Task.Delay(1100); // Wait just over 1 second
-
- var fileName2 = method?.Invoke(_service, null) as string;
-
- Assert.NotNull(fileName1);
- Assert.NotNull(fileName2);
- Assert.NotEqual(fileName1, fileName2);
- }
-
- ///
- /// Tests that GetSaveDirectory creates the directory if it doesn't exist
- /// Validates: Requirement 1.4 - create folder if it doesn't exist
- ///
- [Fact]
- public void GetSaveDirectory_ShouldCreateDirectoryIfNotExists()
- {
- // Delete the directory if it exists
- if (Directory.Exists(_testDirectory))
- {
- Directory.Delete(_testDirectory, true);
- }
-
- // Use reflection to access private method
- var method = typeof(CanvasService).GetMethod("GetSaveDirectory",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- var directory = method?.Invoke(_service, null) as string;
-
- Assert.NotNull(directory);
- Assert.True(Directory.Exists(directory));
- Assert.Equal(_testDirectory, directory);
- }
-
- ///
- /// Tests that GetSaveDirectory returns correct path when directory exists
- /// Validates: Requirement 1.4 - use existing folder
- ///
- [Fact]
- public void GetSaveDirectory_ShouldReturnExistingDirectory()
- {
- // Ensure directory exists
- if (!Directory.Exists(_testDirectory))
- {
- Directory.CreateDirectory(_testDirectory);
- }
-
- var method = typeof(CanvasService).GetMethod("GetSaveDirectory",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- var directory = method?.Invoke(_service, null) as string;
-
- Assert.NotNull(directory);
- Assert.True(Directory.Exists(directory));
- Assert.Equal(_testDirectory, directory);
- }
-
- ///
- /// Tests that SaveCanvasToFileAsync throws exception when bitmap is null
- /// Validates: Requirement 1.6 - error handling
- ///
- [Fact]
- public async Task SaveCanvasToFileAsync_ShouldThrowException_WhenBitmapIsNull()
- {
- await Assert.ThrowsAsync(async () =>
- {
- await _service.SaveCanvasToFileAsync(null!);
- });
- }
-
- ///
- /// Tests filename timestamp parsing to ensure correct format
- /// Validates: Requirement 1.3 - timestamp format YYYYMMDD_HHMMSS
- ///
- [Fact]
- public void GenerateFileName_ShouldContainValidTimestamp()
- {
- var method = typeof(CanvasService).GetMethod("GenerateFileName",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- var fileName = method?.Invoke(_service, null) as string;
-
- Assert.NotNull(fileName);
-
- // Extract timestamp from filename
- var match = Regex.Match(fileName, @"InkCanvas_(\d{8})_(\d{6})\.png");
- Assert.True(match.Success);
-
- var dateStr = match.Groups[1].Value;
- var timeStr = match.Groups[2].Value;
-
- // Verify date format (YYYYMMDD)
- var year = int.Parse(dateStr.Substring(0, 4));
- var month = int.Parse(dateStr.Substring(4, 2));
- var day = int.Parse(dateStr.Substring(6, 2));
-
- Assert.InRange(year, 2024, 2100);
- Assert.InRange(month, 1, 12);
- Assert.InRange(day, 1, 31);
-
- // Verify time format (HHMMSS)
- var hour = int.Parse(timeStr.Substring(0, 2));
- var minute = int.Parse(timeStr.Substring(2, 2));
- var second = int.Parse(timeStr.Substring(4, 2));
-
- Assert.InRange(hour, 0, 23);
- Assert.InRange(minute, 0, 59);
- Assert.InRange(second, 0, 59);
- }
-}
diff --git a/Ink-Canvas-Next.Tests/ViewModels/MainWindowViewModelPropertyTests.cs b/Ink-Canvas-Next.Tests/ViewModels/MainWindowViewModelPropertyTests.cs
deleted file mode 100644
index 08dafc5..0000000
--- a/Ink-Canvas-Next.Tests/ViewModels/MainWindowViewModelPropertyTests.cs
+++ /dev/null
@@ -1,329 +0,0 @@
-using System;
-using Avalonia;
-using Avalonia.Headless;
-using Avalonia.Media;
-using Avalonia.Threading;
-using FsCheck;
-using FsCheck.Xunit;
-using Ink_Canvas_Next.ViewModels;
-using Ink_Canvas_Next.Models;
-using Xunit;
-
-namespace Ink_Canvas_Next.Tests.ViewModels;
-
-///
-/// Property-based tests for MainWindowViewModel background-adaptive color functionality
-/// Uses FsCheck to verify universal properties across many inputs
-///
-[Collection("Avalonia")]
-public class MainWindowViewModelPropertyTests
-{
- public MainWindowViewModelPropertyTests()
- {
- }
-
- ///
- /// Property 3: Highlighter tool activation
- /// For any initial tool state and slider value, when the user selects highlighter mode,
- /// the CurrentTool property should update to Highlighter, PenColor should be set to yellow
- /// (with 50% alpha), the actual rendering thickness should be 10x the slider value,
- /// but the slider display value remains unchanged.
- /// **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
- ///
- [Property(MaxTest = 100)]
- public Property HighlighterActivation_ShouldSetCorrectProperties()
- {
- return Prop.ForAll(
- ArbDrawingTool(),
- ArbPenSize(),
- (initialTool, penSize) =>
- {
- return Dispatcher.UIThread.InvokeAsync(() =>
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeToolCommand.Execute(initialTool);
- viewModel.PenSize = penSize;
-
- // Act: Switch to highlighter mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
-
- // Assert: CurrentTool should be Highlighter
- Assert.Equal(DrawingTool.Highlighter, viewModel.CurrentTool);
-
- // Assert: PenColor should be yellow with 50% opacity (ARGB: 128, 255, 255, 0)
- var expectedColor = Color.FromArgb(HighlighterSettings.Opacity, 255, 255, 0);
- Assert.Equal(expectedColor, viewModel.PenColor);
-
- // Assert: PenSize slider value should remain unchanged
- Assert.Equal(penSize, viewModel.PenSize);
-
- // Assert: Effective thickness should be 10x the slider value
- var expectedThickness = penSize * HighlighterSettings.ThicknessMultiplier;
- Assert.Equal(expectedThickness, viewModel.GetEffectiveThickness());
-
- return true;
- }).GetAwaiter().GetResult();
- }
- );
- }
-
- ///
- /// Property 4: Pen state round-trip
- /// For any initial pen color and slider value, switching to highlighter mode and then
- /// switching back to pen mode should restore the original color and 100% opacity,
- /// and the actual rendering thickness should restore to the slider value (no longer multiplied by 10).
- /// **Validates: Requirements 2.6, 2.7, 2.8**
- ///
- [Property(MaxTest = 100)]
- public Property PenStateRoundTrip_ShouldRestoreOriginalState()
- {
- return Prop.ForAll(
- ArbPenSize(),
- ArbBackgroundMode(),
- (penSize, backgroundMode) =>
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Set background first
- viewModel.ChangeBackgroundCommand.Execute(backgroundMode);
-
- // The pen color will be set to the adaptive color for this background
- var initialAdaptiveColor = viewModel.PenColor;
-
- // Set pen size
- viewModel.PenSize = penSize;
-
- // Act: Switch to highlighter and back to pen
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
-
- // Assert: PenColor should be the adaptive color for the current background
- // (not the highlighter yellow, and with 100% opacity)
- var expectedColor = GetExpectedAdaptiveColor(backgroundMode);
- Assert.Equal(expectedColor, viewModel.PenColor);
-
- // Assert: Alpha should be 255 (100% opacity)
- Assert.Equal(255, viewModel.PenColor.A);
-
- // Assert: PenSize should remain unchanged
- Assert.Equal(penSize, viewModel.PenSize);
-
- // Assert: Effective thickness should be back to normal (not multiplied by 10)
- Assert.Equal(penSize, viewModel.GetEffectiveThickness());
-
- return true;
- }
- );
- }
-
- ///
- /// Property 5: Background adaptive color mapping
- /// For any background mode change (when not in highlighter mode), the pen color should
- /// automatically update according to the following mapping:
- /// Black background → White ink, White background → Black ink, Transparent background → Red ink
- /// **Validates: Requirements 3.1, 3.2, 3.3**
- ///
- [Property(MaxTest = 100)]
- public Property BackgroundChange_ShouldApplyAdaptiveColorMapping()
- {
- return Prop.ForAll(
- ArbBackgroundMode(),
- (backgroundMode) =>
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Ensure we're in pen mode (not highlighter)
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
-
- // Act
- viewModel.ChangeBackgroundCommand.Execute(backgroundMode);
-
- // Assert: Verify adaptive color mapping
- var expectedColor = GetExpectedAdaptiveColor(backgroundMode);
- Assert.Equal(expectedColor, viewModel.PenColor);
-
- return true;
- }
- );
- }
-
- ///
- /// Property 6: Highlighter mode ignores background adaptation
- /// For any background mode change, when highlighter mode is active, the pen color should
- /// remain yellow and not be affected by background changes.
- /// **Validates: Requirements 3.4**
- ///
- [Property(MaxTest = 100)]
- public Property HighlighterMode_ShouldIgnoreBackgroundAdaptiveColor()
- {
- return Prop.ForAll(
- ArbBackgroundMode(),
- (backgroundMode) =>
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Switch to highlighter mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
- var highlighterColor = viewModel.PenColor;
-
- // Act: Change background while in highlighter mode
- viewModel.ChangeBackgroundCommand.Execute(backgroundMode);
-
- // Assert: Pen color should remain unchanged (yellow highlighter color)
- Assert.Equal(highlighterColor, viewModel.PenColor);
-
- return true;
- }
- );
- }
-
- ///
- /// Property 7: Manual color change persistence
- /// For any manual color change, the color should persist and not be affected by
- /// operations other than background changes, until the next background change occurs.
- /// **Validates: Requirements 3.5**
- ///
- [Property(MaxTest = 100)]
- public Property ManualColorChange_ShouldPersistUntilBackgroundChange()
- {
- return Prop.ForAll(
- ArbColor(),
- ArbBackgroundMode(),
- (manualColor, initialBackground) =>
- {
- return Dispatcher.UIThread.InvokeAsync(() =>
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeBackgroundCommand.Execute(initialBackground);
-
- // Act: Manually change pen color
- viewModel.PenColor = manualColor;
-
- // Assert: Color should remain after tool switches (except highlighter)
- viewModel.ChangeToolCommand.Execute(DrawingTool.Eraser);
- Assert.Equal(manualColor, viewModel.PenColor);
-
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
- Assert.Equal(manualColor, viewModel.PenColor);
-
- // Assert: Manual color should persist through first background change
- var newBackground = GetDifferentBackground(initialBackground);
- viewModel.ChangeBackgroundCommand.Execute(newBackground);
- Assert.Equal(manualColor, viewModel.PenColor);
-
- // Assert: After first background change, next background change should apply adaptive color
- var thirdBackground = GetDifferentBackground(newBackground);
- viewModel.ChangeBackgroundCommand.Execute(thirdBackground);
- var expectedColor = GetExpectedAdaptiveColor(thirdBackground);
- Assert.Equal(expectedColor, viewModel.PenColor);
-
- return true;
- }).GetAwaiter().GetResult();
- }
- );
- }
-
- ///
- /// Property 8: Highlighter to pen background adaptation
- /// For any current background mode, when switching from highlighter mode back to pen mode,
- /// the pen color should apply background adaptive color rules based on the current background.
- /// **Validates: Requirements 3.6**
- ///
- [Property(MaxTest = 100)]
- public Property HighlighterToPen_ShouldApplyBackgroundAdaptiveColor()
- {
- return Prop.ForAll(
- ArbBackgroundMode(),
- (backgroundMode) =>
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Set background mode first
- viewModel.ChangeBackgroundCommand.Execute(backgroundMode);
-
- // Switch to highlighter mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
-
- // Act: Switch back to pen mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
-
- // Assert: Pen color should match the background adaptive color
- var expectedColor = GetExpectedAdaptiveColor(backgroundMode);
- Assert.Equal(expectedColor, viewModel.PenColor);
-
- return true;
- }
- );
- }
-
- // Helper methods
-
- ///
- /// Generates arbitrary background modes for property tests
- ///
- private static Arbitrary ArbBackgroundMode()
- {
- return Gen.Elements("transparent", "white", "black").ToArbitrary();
- }
-
- ///
- /// Generates arbitrary colors for property tests
- ///
- private static Arbitrary ArbColor()
- {
- return Gen.Choose(0, 255)
- .Four()
- .Select(t => Color.FromArgb((byte)t.Item1, (byte)t.Item2, (byte)t.Item3, (byte)t.Item4))
- .ToArbitrary();
- }
-
- ///
- /// Generates arbitrary drawing tools for property tests
- ///
- private static Arbitrary ArbDrawingTool()
- {
- return Gen.Elements(DrawingTool.Pen, DrawingTool.Highlighter, DrawingTool.Eraser).ToArbitrary();
- }
-
- ///
- /// Generates arbitrary pen sizes for property tests (1.0 to 20.0)
- ///
- private static Arbitrary ArbPenSize()
- {
- return Gen.Choose(1, 20).Select(i => (double)i).ToArbitrary();
- }
-
- ///
- /// Gets the expected adaptive color for a given background mode
- ///
- private static Color GetExpectedAdaptiveColor(string backgroundMode)
- {
- return backgroundMode.ToLower() switch
- {
- "black" => Colors.White,
- "white" => Colors.Black,
- "transparent" => Colors.Red,
- _ => Colors.Red
- };
- }
-
- ///
- /// Gets a different background mode from the given one
- ///
- private static string GetDifferentBackground(string currentBackground)
- {
- return currentBackground.ToLower() switch
- {
- "black" => "white",
- "white" => "transparent",
- "transparent" => "black",
- _ => "white"
- };
- }
-}
diff --git a/Ink-Canvas-Next.Tests/ViewModels/MainWindowViewModelTests.cs b/Ink-Canvas-Next.Tests/ViewModels/MainWindowViewModelTests.cs
deleted file mode 100644
index 4bac6ee..0000000
--- a/Ink-Canvas-Next.Tests/ViewModels/MainWindowViewModelTests.cs
+++ /dev/null
@@ -1,663 +0,0 @@
-using Avalonia;
-using Avalonia.Headless;
-using Avalonia.Media;
-using Ink_Canvas_Next.ViewModels;
-using Xunit;
-
-namespace Ink_Canvas_Next.Tests.ViewModels;
-
-///
-/// Unit tests for MainWindowViewModel
-/// Validates: Requirements 3.1, 3.2, 3.3
-///
-[Collection("Avalonia")]
-public class MainWindowViewModelTests
-{
- public MainWindowViewModelTests()
- {
- }
- ///
- /// Tests that BackgroundMode is initialized to "Transparent"
- /// Validates: Requirement 3.3 - default background mode
- ///
- [Fact]
- public void BackgroundMode_ShouldInitializeToTransparent()
- {
- // Arrange & Act
- var viewModel = new MainWindowViewModel();
-
- // Assert
- Assert.Equal("Transparent", viewModel.BackgroundMode);
- }
-
- ///
- /// Tests that ChangeBackground updates BackgroundMode to "White"
- /// Validates: Requirement 3.2 - white background mode tracking
- ///
- [Fact]
- public void ChangeBackground_ShouldSetBackgroundModeToWhite()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Act
- viewModel.ChangeBackgroundCommand.Execute("white");
-
- // Assert
- Assert.Equal("White", viewModel.BackgroundMode);
- }
-
- ///
- /// Tests that ChangeBackground updates BackgroundMode to "Black"
- /// Validates: Requirement 3.1 - black background mode tracking
- ///
- [Fact]
- public void ChangeBackground_ShouldSetBackgroundModeToBlack()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Act
- viewModel.ChangeBackgroundCommand.Execute("black");
-
- // Assert
- Assert.Equal("Black", viewModel.BackgroundMode);
- }
-
- ///
- /// Tests that ChangeBackground updates BackgroundMode to "Transparent"
- /// Validates: Requirement 3.3 - transparent background mode tracking
- ///
- [Fact]
- public void ChangeBackground_ShouldSetBackgroundModeToTransparent()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeBackgroundCommand.Execute("black"); // Start with non-transparent
-
- // Act
- viewModel.ChangeBackgroundCommand.Execute("transparent");
-
- // Assert
- Assert.Equal("Transparent", viewModel.BackgroundMode);
- }
-
- ///
- /// Tests that ChangeBackground is case-insensitive
- /// Validates: Requirement 3.1, 3.2, 3.3 - robust mode handling
- ///
- [Theory]
- [InlineData("WHITE", "White")]
- [InlineData("White", "White")]
- [InlineData("white", "White")]
- [InlineData("BLACK", "Black")]
- [InlineData("Black", "Black")]
- [InlineData("black", "Black")]
- [InlineData("TRANSPARENT", "Transparent")]
- [InlineData("Transparent", "Transparent")]
- [InlineData("transparent", "Transparent")]
- public void ChangeBackground_ShouldBeCaseInsensitive(string input, string expected)
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Act
- viewModel.ChangeBackgroundCommand.Execute(input);
-
- // Assert
- Assert.Equal(expected, viewModel.BackgroundMode);
- }
-
- ///
- /// Tests that BackgroundMode changes are observable
- /// Validates: Requirements 3.1, 3.2, 3.3 - property change notifications
- ///
- [Fact]
- public void BackgroundMode_ShouldRaisePropertyChanged()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var propertyChangedRaised = false;
- viewModel.PropertyChanged += (sender, args) =>
- {
- if (args.PropertyName == nameof(MainWindowViewModel.BackgroundMode))
- {
- propertyChangedRaised = true;
- }
- };
-
- // Act
- viewModel.ChangeBackgroundCommand.Execute("white");
-
- // Assert
- Assert.True(propertyChangedRaised);
- }
-
- ///
- /// Tests that BackgroundMode persists across multiple changes
- /// Validates: Requirements 3.1, 3.2, 3.3 - state persistence
- ///
- [Fact]
- public void BackgroundMode_ShouldPersistAcrossMultipleChanges()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
-
- // Act & Assert
- viewModel.ChangeBackgroundCommand.Execute("white");
- Assert.Equal("White", viewModel.BackgroundMode);
-
- viewModel.ChangeBackgroundCommand.Execute("black");
- Assert.Equal("Black", viewModel.BackgroundMode);
-
- viewModel.ChangeBackgroundCommand.Execute("transparent");
- Assert.Equal("Transparent", viewModel.BackgroundMode);
-
- viewModel.ChangeBackgroundCommand.Execute("white");
- Assert.Equal("White", viewModel.BackgroundMode);
- }
-
- ///
- /// Tests that PenColor initializes to Red (default for transparent background)
- /// Validates: Requirement 3.3 - default pen color
- ///
- [Fact]
- public void PenColor_ShouldInitializeToRed()
- {
- // Arrange & Act
- var viewModel = new MainWindowViewModel();
-
- // Assert
- Assert.Equal(Colors.Red, viewModel.PenColor);
- }
-
- ///
- /// Tests that manually changing PenColor prevents background adaptive color changes
- /// Validates: Requirement 3.5 - manual color change persistence
- ///
- [Fact]
- public void ManualPenColorChange_ShouldPreventBackgroundAdaptiveColor()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeBackgroundCommand.Execute("transparent"); // Start with transparent (red pen)
-
- // Act - Manually change pen color
- viewModel.PenColor = Colors.Blue;
-
- // Change background - should NOT change pen color because it was manually set
- viewModel.ChangeBackgroundCommand.Execute("white");
-
- // Assert - Pen color should remain blue, not change to black
- Assert.Equal(Colors.Blue, viewModel.PenColor);
- }
-
- ///
- /// Tests that background change resets manual color flag and applies adaptive color
- /// Validates: Requirement 3.5 - manual color preference until next background change
- ///
- [Fact]
- public void BackgroundChange_ShouldResetManualColorFlagAndApplyAdaptiveColor()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.PenColor = Colors.Blue; // Manual color change
-
- // Act - First background change resets flag but doesn't change color (already manual)
- viewModel.ChangeBackgroundCommand.Execute("white");
- Assert.Equal(Colors.Blue, viewModel.PenColor); // Still blue from manual change
-
- // Second background change should now apply adaptive color
- viewModel.ChangeBackgroundCommand.Execute("black");
-
- // Assert - Should apply adaptive color (white for black background)
- Assert.Equal(Colors.White, viewModel.PenColor);
- }
-
- ///
- /// Tests that manual color change in highlighter mode does NOT set the flag
- /// Validates: Requirement 3.5 - highlighter mode exception
- ///
- [Fact]
- public void ManualPenColorChange_InHighlighterMode_ShouldNotSetFlag()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
-
- // Act - Change color in highlighter mode
- viewModel.PenColor = Colors.Green;
-
- // Switch back to pen mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
-
- // Change background - should apply adaptive color because flag wasn't set in highlighter mode
- viewModel.ChangeBackgroundCommand.Execute("black");
-
- // Assert - Should apply adaptive color (white for black background)
- Assert.Equal(Colors.White, viewModel.PenColor);
- }
-
- ///
- /// Tests that pen color changes apply adaptive color when not manually changed
- /// Validates: Requirement 3.1, 3.2, 3.3 - background adaptive color
- ///
- [Theory]
- [InlineData("black", 255, 255, 255, 255)] // White for black background
- [InlineData("white", 255, 0, 0, 0)] // Black for white background
- [InlineData("transparent", 255, 255, 0, 0)] // Red for transparent background
- public void BackgroundChange_ShouldApplyAdaptiveColor(string background, byte a, byte r, byte g, byte b)
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var expectedColor = Color.FromArgb(a, r, g, b);
-
- // Act
- viewModel.ChangeBackgroundCommand.Execute(background);
-
- // Assert
- Assert.Equal(expectedColor, viewModel.PenColor);
- }
-
- ///
- /// Tests that SavePenState preserves the current pen color
- /// Validates: Requirement 2.6 - pen state preservation
- ///
- [Fact]
- public void SavePenState_ShouldPreserveCurrentPenColor()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var originalColor = Colors.Blue;
- viewModel.PenColor = originalColor;
-
- // Act - Use reflection to call private SavePenState method
- var method = typeof(MainWindowViewModel).GetMethod("SavePenState",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- method?.Invoke(viewModel, null);
-
- // Change the pen color after saving
- viewModel.PenColor = Colors.Green;
-
- // Act - Restore the saved state
- var restoreMethod = typeof(MainWindowViewModel).GetMethod("RestorePenState",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- restoreMethod?.Invoke(viewModel, null);
-
- // Assert - Should restore to the saved color
- Assert.Equal(originalColor, viewModel.PenColor);
- }
-
- ///
- /// Tests that RestorePenState restores the previously saved pen color
- /// Validates: Requirement 2.6 - pen state restoration
- ///
- [Fact]
- public void RestorePenState_ShouldRestoreSavedPenColor()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var savedColor = Colors.Purple;
- viewModel.PenColor = savedColor;
-
- // Act - Save the state
- var saveMethod = typeof(MainWindowViewModel).GetMethod("SavePenState",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- saveMethod?.Invoke(viewModel, null);
-
- // Change color multiple times
- viewModel.PenColor = Colors.Orange;
- viewModel.PenColor = Colors.Cyan;
-
- // Restore the state
- var restoreMethod = typeof(MainWindowViewModel).GetMethod("RestorePenState",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- restoreMethod?.Invoke(viewModel, null);
-
- // Assert - Should restore to the originally saved color
- Assert.Equal(savedColor, viewModel.PenColor);
- }
-
- ///
- /// Tests that SavePenState can be called multiple times and only the last save is restored
- /// Validates: Requirement 2.6 - pen state save/restore behavior
- ///
- [Fact]
- public void SavePenState_MultipleSaves_ShouldOnlyRestoreLastSave()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var firstColor = Colors.Red;
- var secondColor = Colors.Blue;
-
- // Act - First save
- viewModel.PenColor = firstColor;
- var saveMethod = typeof(MainWindowViewModel).GetMethod("SavePenState",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- saveMethod?.Invoke(viewModel, null);
-
- // Second save (should overwrite first)
- viewModel.PenColor = secondColor;
- saveMethod?.Invoke(viewModel, null);
-
- // Change color
- viewModel.PenColor = Colors.Green;
-
- // Restore
- var restoreMethod = typeof(MainWindowViewModel).GetMethod("RestorePenState",
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
- restoreMethod?.Invoke(viewModel, null);
-
- // Assert - Should restore to the second saved color, not the first
- Assert.Equal(secondColor, viewModel.PenColor);
- }
-
- ///
- /// Tests that GetEffectiveThickness returns PenSize for Pen mode
- /// Validates: Requirement 2.5 - pen mode thickness
- ///
- [Fact]
- public void GetEffectiveThickness_InPenMode_ShouldReturnPenSize()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
- viewModel.PenSize = 5.0;
-
- // Act
- var thickness = viewModel.GetEffectiveThickness();
-
- // Assert
- Assert.Equal(5.0, thickness);
- }
-
- ///
- /// Tests that GetEffectiveThickness returns PenSize × 10 for Highlighter mode
- /// Validates: Requirement 2.4 - highlighter mode thickness multiplier
- ///
- [Fact]
- public void GetEffectiveThickness_InHighlighterMode_ShouldReturnPenSizeTimesMultiplier()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
- viewModel.PenSize = 3.0;
-
- // Act
- var thickness = viewModel.GetEffectiveThickness();
-
- // Assert
- Assert.Equal(30.0, thickness); // 3.0 × 10
- }
-
- ///
- /// Tests that GetEffectiveThickness returns PenSize for Eraser mode
- /// Validates: Requirement 2.5 - eraser mode thickness
- ///
- [Fact]
- public void GetEffectiveThickness_InEraserMode_ShouldReturnPenSize()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeToolCommand.Execute(DrawingTool.Eraser);
- viewModel.PenSize = 8.0;
-
- // Act
- var thickness = viewModel.GetEffectiveThickness();
-
- // Assert
- Assert.Equal(8.0, thickness);
- }
-
- ///
- /// Tests that GetEffectiveThickness updates when PenSize changes in Pen mode
- /// Validates: Requirement 2.5 - dynamic thickness updates
- ///
- [Theory]
- [InlineData(1.0)]
- [InlineData(5.0)]
- [InlineData(10.0)]
- [InlineData(15.0)]
- [InlineData(20.0)]
- public void GetEffectiveThickness_InPenMode_ShouldUpdateWithPenSize(double penSize)
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
-
- // Act
- viewModel.PenSize = penSize;
- var thickness = viewModel.GetEffectiveThickness();
-
- // Assert
- Assert.Equal(penSize, thickness);
- }
-
- ///
- /// Tests that GetEffectiveThickness updates when PenSize changes in Highlighter mode
- /// Validates: Requirement 2.4 - dynamic thickness updates with multiplier
- ///
- [Theory]
- [InlineData(1.0, 10.0)]
- [InlineData(2.0, 20.0)]
- [InlineData(3.0, 30.0)]
- [InlineData(5.0, 50.0)]
- [InlineData(10.0, 100.0)]
- public void GetEffectiveThickness_InHighlighterMode_ShouldUpdateWithPenSize(double penSize, double expectedThickness)
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
-
- // Act
- viewModel.PenSize = penSize;
- var thickness = viewModel.GetEffectiveThickness();
-
- // Assert
- Assert.Equal(expectedThickness, thickness);
- }
-
- ///
- /// Tests that GetEffectiveThickness changes when switching between tools
- /// Validates: Requirements 2.4, 2.5 - tool-specific thickness behavior
- ///
- [Fact]
- public void GetEffectiveThickness_ShouldChangeWhenSwitchingTools()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- viewModel.PenSize = 4.0;
-
- // Act & Assert - Pen mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
- Assert.Equal(4.0, viewModel.GetEffectiveThickness());
-
- // Act & Assert - Highlighter mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
- Assert.Equal(40.0, viewModel.GetEffectiveThickness());
-
- // Act & Assert - Back to Pen mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
- Assert.Equal(4.0, viewModel.GetEffectiveThickness());
-
- // Act & Assert - Eraser mode
- viewModel.ChangeToolCommand.Execute(DrawingTool.Eraser);
- Assert.Equal(4.0, viewModel.GetEffectiveThickness());
- }
-
- ///
- /// Tests that clear operation preserves BackgroundMode
- /// Validates: Requirement 4.2 - clear preserves background mode
- ///
- [Theory]
- [InlineData("transparent")]
- [InlineData("white")]
- [InlineData("black")]
- public void ClearCanvas_ShouldPreserveBackgroundMode(string backgroundMode)
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var clearWasCalled = false;
- viewModel.ClearCanvasAction = () => clearWasCalled = true;
-
- // Set background mode
- viewModel.ChangeBackgroundCommand.Execute(backgroundMode);
- var expectedBackgroundMode = viewModel.BackgroundMode;
-
- // Act
- viewModel.ClearCommand.Execute(null);
-
- // Assert
- Assert.True(clearWasCalled, "ClearCanvasAction should have been invoked");
- Assert.Equal(expectedBackgroundMode, viewModel.BackgroundMode);
- }
-
- ///
- /// Tests that clear operation preserves CurrentTool
- /// Validates: Requirement 4.3 - clear preserves current tool
- ///
- [Theory]
- [InlineData(DrawingTool.Pen)]
- [InlineData(DrawingTool.Highlighter)]
- [InlineData(DrawingTool.Eraser)]
- public void ClearCanvas_ShouldPreserveCurrentTool(DrawingTool tool)
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var clearWasCalled = false;
- viewModel.ClearCanvasAction = () => clearWasCalled = true;
-
- // Set tool
- viewModel.ChangeToolCommand.Execute(tool);
-
- // Act
- viewModel.ClearCommand.Execute(null);
-
- // Assert
- Assert.True(clearWasCalled, "ClearCanvasAction should have been invoked");
- Assert.Equal(tool, viewModel.CurrentTool);
- }
-
- ///
- /// Tests that clear operation preserves PenColor
- /// Validates: Requirement 4.4 - clear preserves pen color
- ///
- [Fact]
- public void ClearCanvas_ShouldPreservePenColor()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var clearWasCalled = false;
- viewModel.ClearCanvasAction = () => clearWasCalled = true;
-
- // Set a specific pen color
- var expectedColor = Colors.Purple;
- viewModel.PenColor = expectedColor;
-
- // Act
- viewModel.ClearCommand.Execute(null);
-
- // Assert
- Assert.True(clearWasCalled, "ClearCanvasAction should have been invoked");
- Assert.Equal(expectedColor, viewModel.PenColor);
- }
-
- ///
- /// Tests that clear operation preserves PenSize
- /// Validates: Requirement 4.4 - clear preserves pen size
- ///
- [Theory]
- [InlineData(1.0)]
- [InlineData(5.0)]
- [InlineData(10.0)]
- [InlineData(15.0)]
- [InlineData(20.0)]
- public void ClearCanvas_ShouldPreservePenSize(double penSize)
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var clearWasCalled = false;
- viewModel.ClearCanvasAction = () => clearWasCalled = true;
-
- // Set pen size
- viewModel.PenSize = penSize;
-
- // Act
- viewModel.ClearCommand.Execute(null);
-
- // Assert
- Assert.True(clearWasCalled, "ClearCanvasAction should have been invoked");
- Assert.Equal(penSize, viewModel.PenSize);
- }
-
- ///
- /// Tests that clear operation preserves all settings together
- /// Validates: Requirements 4.2, 4.3, 4.4 - clear preserves all settings
- ///
- [Fact]
- public void ClearCanvas_ShouldPreserveAllSettings()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var clearWasCalled = false;
- viewModel.ClearCanvasAction = () => clearWasCalled = true;
-
- // Set up a specific state
- viewModel.ChangeBackgroundCommand.Execute("black");
- viewModel.ChangeToolCommand.Execute(DrawingTool.Highlighter);
- viewModel.PenSize = 7.5;
-
- // Capture expected values
- var expectedBackgroundMode = viewModel.BackgroundMode;
- var expectedTool = viewModel.CurrentTool;
- var expectedColor = viewModel.PenColor;
- var expectedSize = viewModel.PenSize;
-
- // Act
- viewModel.ClearCommand.Execute(null);
-
- // Assert
- Assert.True(clearWasCalled, "ClearCanvasAction should have been invoked");
- Assert.Equal(expectedBackgroundMode, viewModel.BackgroundMode);
- Assert.Equal(expectedTool, viewModel.CurrentTool);
- Assert.Equal(expectedColor, viewModel.PenColor);
- Assert.Equal(expectedSize, viewModel.PenSize);
- }
-
- ///
- /// Tests that clear operation can be called multiple times without affecting settings
- /// Validates: Requirements 4.2, 4.3, 4.4 - clear is idempotent regarding settings
- ///
- [Fact]
- public void ClearCanvas_MultipleCalls_ShouldPreserveSettings()
- {
- // Arrange
- var viewModel = new MainWindowViewModel();
- var clearCallCount = 0;
- viewModel.ClearCanvasAction = () => clearCallCount++;
-
- // Set up a specific state
- viewModel.ChangeBackgroundCommand.Execute("white");
- viewModel.ChangeToolCommand.Execute(DrawingTool.Pen);
- viewModel.PenColor = Colors.Blue;
- viewModel.PenSize = 4.0;
-
- // Capture expected values
- var expectedBackgroundMode = viewModel.BackgroundMode;
- var expectedTool = viewModel.CurrentTool;
- var expectedColor = viewModel.PenColor;
- var expectedSize = viewModel.PenSize;
-
- // Act - Call clear multiple times
- viewModel.ClearCommand.Execute(null);
- viewModel.ClearCommand.Execute(null);
- viewModel.ClearCommand.Execute(null);
-
- // Assert
- Assert.Equal(3, clearCallCount);
- Assert.Equal(expectedBackgroundMode, viewModel.BackgroundMode);
- Assert.Equal(expectedTool, viewModel.CurrentTool);
- Assert.Equal(expectedColor, viewModel.PenColor);
- Assert.Equal(expectedSize, viewModel.PenSize);
- }
-}
diff --git a/Ink-Canvas-Next/Converters/UiLanguageDisplayConverter.cs b/Ink-Canvas-Next/Converters/UiLanguageDisplayConverter.cs
new file mode 100644
index 0000000..57d4e7b
--- /dev/null
+++ b/Ink-Canvas-Next/Converters/UiLanguageDisplayConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Ink_Canvas_Next.ViewModels;
+
+namespace Ink_Canvas_Next.Converters;
+
+///
+/// Converts UiLanguage enum values to their display strings
+///
+public class UiLanguageDisplayConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is UiLanguage lang)
+ {
+ return lang switch
+ {
+ UiLanguage.ZhCn => "中文",
+ UiLanguage.EnUs => "English",
+ _ => lang.ToString()
+ };
+ }
+ return value?.ToString();
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/Ink-Canvas-Next/Ink-Canvas-Next.csproj b/Ink-Canvas-Next/Ink-Canvas-Next.csproj
index 659ede1..e9d7091 100644
--- a/Ink-Canvas-Next/Ink-Canvas-Next.csproj
+++ b/Ink-Canvas-Next/Ink-Canvas-Next.csproj
@@ -24,6 +24,6 @@
-
+
diff --git a/Ink-Canvas-Next/Ink-Canvas-Next.csproj.lscache b/Ink-Canvas-Next/Ink-Canvas-Next.csproj.lscache
new file mode 100644
index 0000000..8422c6a
--- /dev/null
+++ b/Ink-Canvas-Next/Ink-Canvas-Next.csproj.lscache
@@ -0,0 +1,309 @@
+version=1
+
+# This file caches language service data to improve the performance of C# Dev Kit.
+# It is not intended for manual editing. It can safely be deleted and will be
+# regenerated automatically. For more information, see https://aka.ms/lscache
+#
+# To control where cache files are stored, use the following VS Code setting:
+# "dotnet.projectsystem.cacheInProjectFolder": true
+
+[project]
+language=C#
+primary
+lastDtbSucceeded
+
+[properties]
+AssemblyName=Ink-Canvas-Next
+CommandLineArgsForDesignTimeEvaluation=-langversion:14.0 -define:TRACE
+CompilerGeneratedFilesOutputPath=
+MaxSupportedLangVersion=14.0
+ProjectAssetsFile=obj/project.assets.json
+RootNamespace=Ink-Canvas-Next
+RunAnalyzers=
+RunAnalyzersDuringLiveAnalysis=
+SolutionPath=../Ink Canvas Next.slnx
+TargetFrameworkIdentifier=.NETCoreApp
+TargetPath=bin/Debug/net11.0/Ink-Canvas-Next.dll
+TargetRefPath=obj/Debug/net11.0/ref/Ink-Canvas-Next.dll
+TemporaryDependencyNodeTargetIdentifier=net11.0
+
+[commandLineArguments]
+/noconfig
+/unsafe-
+/checked-
+/nowarn:1701,1702,1701,1702,8002
+/fullpaths
+/nostdlib+
+/errorreport:prompt
+/warn:11
+/define:TRACE;DEBUG;NET;NET11_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NET11_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER
+/highentropyva+
+/nullable:enable
+/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated"
+/debug+
+/debug:portable
+/filealign:512
+/optimize-
+/out:obj\Debug\net11.0\Ink-Canvas-Next.dll
+/refout:obj\Debug\net11.0\refint\Ink-Canvas-Next.dll
+/target:winexe
+/warnaserror-
+/utf8output
+/win32manifest:app.manifest
+/deterministic+
+/langversion:14.0
+/warnaserror+:NU1605,SYSLIB0011
+
+[sourceFiles]
+App.axaml.cs
+Converters/UiLanguageDisplayConverter.cs
+Models/
+ AdaptiveColors.cs
+ HighlighterSettings.cs
+obj/Debug/net11.0/
+ .NETCoreApp,Version=v11.0.AssemblyAttributes.cs
+ Ink-Canvas-Next.AssemblyInfo.cs
+Program.cs
+Services/CanvasService.cs
+ViewLocator.cs
+ViewModels/
+ MainWindowViewModel.cs
+ ViewModelBase.cs
+Views/MainWindow.axaml.cs
+
+[metadataReferences]
+/packs/Microsoft.NETCore.App.Ref/11.0.0-preview.3.26207.106/ref/net11.0/
+ Microsoft.CSharp.dll
+ Microsoft.VisualBasic.Core.dll
+ Microsoft.VisualBasic.dll
+ Microsoft.Win32.Primitives.dll
+ Microsoft.Win32.Registry.dll
+ mscorlib.dll
+ netstandard.dll
+ System.AppContext.dll
+ System.Buffers.dll
+ System.Collections.Concurrent.dll
+ System.Collections.dll
+ System.Collections.Immutable.dll
+ System.Collections.NonGeneric.dll
+ System.Collections.Specialized.dll
+ System.ComponentModel.Annotations.dll
+ System.ComponentModel.DataAnnotations.dll
+ System.ComponentModel.dll
+ System.ComponentModel.EventBasedAsync.dll
+ System.ComponentModel.Primitives.dll
+ System.ComponentModel.TypeConverter.dll
+ System.Configuration.dll
+ System.Console.dll
+ System.Core.dll
+ System.Data.Common.dll
+ System.Data.DataSetExtensions.dll
+ System.Data.dll
+ System.Diagnostics.Contracts.dll
+ System.Diagnostics.Debug.dll
+ System.Diagnostics.DiagnosticSource.dll
+ System.Diagnostics.FileVersionInfo.dll
+ System.Diagnostics.Process.dll
+ System.Diagnostics.StackTrace.dll
+ System.Diagnostics.TextWriterTraceListener.dll
+ System.Diagnostics.Tools.dll
+ System.Diagnostics.TraceSource.dll
+ System.Diagnostics.Tracing.dll
+ System.dll
+ System.Drawing.dll
+ System.Drawing.Primitives.dll
+ System.Dynamic.Runtime.dll
+ System.Formats.Asn1.dll
+ System.Formats.Tar.dll
+ System.Globalization.Calendars.dll
+ System.Globalization.dll
+ System.Globalization.Extensions.dll
+ System.IO.Compression.Brotli.dll
+ System.IO.Compression.dll
+ System.IO.Compression.FileSystem.dll
+ System.IO.Compression.ZipFile.dll
+ System.IO.dll
+ System.IO.FileSystem.AccessControl.dll
+ System.IO.FileSystem.dll
+ System.IO.FileSystem.DriveInfo.dll
+ System.IO.FileSystem.Primitives.dll
+ System.IO.FileSystem.Watcher.dll
+ System.IO.IsolatedStorage.dll
+ System.IO.MemoryMappedFiles.dll
+ System.IO.Pipelines.dll
+ System.IO.Pipes.AccessControl.dll
+ System.IO.Pipes.dll
+ System.IO.UnmanagedMemoryStream.dll
+ System.Linq.AsyncEnumerable.dll
+ System.Linq.dll
+ System.Linq.Expressions.dll
+ System.Linq.Parallel.dll
+ System.Linq.Queryable.dll
+ System.Memory.dll
+ System.Net.dll
+ System.Net.Http.dll
+ System.Net.Http.Json.dll
+ System.Net.HttpListener.dll
+ System.Net.Mail.dll
+ System.Net.NameResolution.dll
+ System.Net.NetworkInformation.dll
+ System.Net.Ping.dll
+ System.Net.Primitives.dll
+ System.Net.Quic.dll
+ System.Net.Requests.dll
+ System.Net.Security.dll
+ System.Net.ServerSentEvents.dll
+ System.Net.ServicePoint.dll
+ System.Net.Sockets.dll
+ System.Net.WebClient.dll
+ System.Net.WebHeaderCollection.dll
+ System.Net.WebProxy.dll
+ System.Net.WebSockets.Client.dll
+ System.Net.WebSockets.dll
+ System.Numerics.dll
+ System.Numerics.Vectors.dll
+ System.ObjectModel.dll
+ System.Reflection.DispatchProxy.dll
+ System.Reflection.dll
+ System.Reflection.Emit.dll
+ System.Reflection.Emit.ILGeneration.dll
+ System.Reflection.Emit.Lightweight.dll
+ System.Reflection.Extensions.dll
+ System.Reflection.Metadata.dll
+ System.Reflection.Primitives.dll
+ System.Reflection.TypeExtensions.dll
+ System.Resources.Reader.dll
+ System.Resources.ResourceManager.dll
+ System.Resources.Writer.dll
+ System.Runtime.CompilerServices.Unsafe.dll
+ System.Runtime.CompilerServices.VisualC.dll
+ System.Runtime.dll
+ System.Runtime.Extensions.dll
+ System.Runtime.Handles.dll
+ System.Runtime.InteropServices.dll
+ System.Runtime.InteropServices.JavaScript.dll
+ System.Runtime.InteropServices.RuntimeInformation.dll
+ System.Runtime.Intrinsics.dll
+ System.Runtime.Loader.dll
+ System.Runtime.Numerics.dll
+ System.Runtime.Serialization.dll
+ System.Runtime.Serialization.Formatters.dll
+ System.Runtime.Serialization.Json.dll
+ System.Runtime.Serialization.Primitives.dll
+ System.Runtime.Serialization.Xml.dll
+ System.Security.AccessControl.dll
+ System.Security.Claims.dll
+ System.Security.Cryptography.Algorithms.dll
+ System.Security.Cryptography.Cng.dll
+ System.Security.Cryptography.Csp.dll
+ System.Security.Cryptography.dll
+ System.Security.Cryptography.Encoding.dll
+ System.Security.Cryptography.OpenSsl.dll
+ System.Security.Cryptography.Primitives.dll
+ System.Security.Cryptography.X509Certificates.dll
+ System.Security.dll
+ System.Security.Principal.dll
+ System.Security.Principal.Windows.dll
+ System.Security.SecureString.dll
+ System.ServiceModel.Web.dll
+ System.ServiceProcess.dll
+ System.Text.Encoding.CodePages.dll
+ System.Text.Encoding.dll
+ System.Text.Encoding.Extensions.dll
+ System.Text.Encodings.Web.dll
+ System.Text.Json.dll
+ System.Text.RegularExpressions.dll
+ System.Threading.AccessControl.dll
+ System.Threading.Channels.dll
+ System.Threading.dll
+ System.Threading.Overlapped.dll
+ System.Threading.Tasks.Dataflow.dll
+ System.Threading.Tasks.dll
+ System.Threading.Tasks.Extensions.dll
+ System.Threading.Tasks.Parallel.dll
+ System.Threading.Thread.dll
+ System.Threading.ThreadPool.dll
+ System.Threading.Timer.dll
+ System.Transactions.dll
+ System.Transactions.Local.dll
+ System.ValueTuple.dll
+ System.Web.dll
+ System.Web.HttpUtility.dll
+ System.Windows.dll
+ System.Xml.dll
+ System.Xml.Linq.dll
+ System.Xml.ReaderWriter.dll
+ System.Xml.Serialization.dll
+ System.Xml.XDocument.dll
+ System.Xml.XmlDocument.dll
+ System.Xml.XmlSerializer.dll
+ System.Xml.XPath.dll
+ System.Xml.XPath.XDocument.dll
+ WindowsBase.dll
+/
+ avalonia.controls.colorpicker/11.3.11/lib/net8.0/Avalonia.Controls.ColorPicker.dll
+ avalonia.controls.datagrid/11.3.10/lib/net8.0/Avalonia.Controls.DataGrid.dll
+ avalonia.desktop/11.3.11/lib/net8.0/Avalonia.Desktop.dll
+ avalonia.diagnostics/11.3.11/lib/net8.0/Avalonia.Diagnostics.dll
+ avalonia.fonts.inter/11.3.11/lib/net8.0/Avalonia.Fonts.Inter.dll
+ avalonia.freedesktop/11.3.11/lib/net8.0/Avalonia.FreeDesktop.dll
+ avalonia.native/11.3.11/lib/net8.0/Avalonia.Native.dll
+ avalonia.remote.protocol/11.3.11/lib/net8.0/Avalonia.Remote.Protocol.dll
+ avalonia.skia/11.3.11/lib/net8.0/Avalonia.Skia.dll
+ avalonia.themes.fluent/11.3.11/lib/net8.0/Avalonia.Themes.Fluent.dll
+ avalonia.themes.simple/11.3.11/lib/net8.0/Avalonia.Themes.Simple.dll
+ avalonia.win32/11.3.11/lib/net8.0/
+ Avalonia.Win32.Automation.dll
+ Avalonia.Win32.dll
+ avalonia.x11/11.3.11/lib/net8.0/Avalonia.X11.dll
+ avalonia/11.3.11/ref/net8.0/
+ Avalonia.Base.dll
+ Avalonia.Controls.dll
+ Avalonia.DesignerSupport.dll
+ Avalonia.Dialogs.dll
+ Avalonia.dll
+ Avalonia.Markup.dll
+ Avalonia.Markup.Xaml.dll
+ Avalonia.Metal.dll
+ Avalonia.MicroCom.dll
+ Avalonia.OpenGL.dll
+ Avalonia.Vulkan.dll
+ communitytoolkit.mvvm/8.2.1/lib/net6.0/CommunityToolkit.Mvvm.dll
+ dotnetcampus.avaloniainkcanvas/1.0.1/lib/net8.0/DotNetCampus.AvaloniaInkCanvas.dll
+ dotnetcampus.logger/1.3.0-alpha01/lib/net8.0/DotNetCampus.Logger.dll
+ dotnetcampus.numerics.geometry/1.0.1-alpha22/lib/net8.0/DotNetCampus.Numerics.Geometry.dll
+ dotnetcampus.numerics/1.0.1-alpha22/lib/net8.0/DotNetCampus.Numerics.dll
+ fluentavaloniaui/2.5.0/lib/net10.0/FluentAvalonia.dll
+ harfbuzzsharp/8.3.1.1/lib/net8.0/HarfBuzzSharp.dll
+ microcom.runtime/0.11.0/lib/net5.0/MicroCom.Runtime.dll
+ skiasharp/2.88.9/lib/net6.0/SkiaSharp.dll
+ tmds.dbus.protocol/0.21.2/lib/net8.0/Tmds.DBus.Protocol.dll
+
+[analyzerReferences]
+/packs/Microsoft.NETCore.App.Ref/11.0.0-preview.3.26207.106/analyzers/dotnet/cs/
+ Microsoft.Interop.ComInterfaceGenerator.dll
+ Microsoft.Interop.JavaScript.JSImportGenerator.dll
+ Microsoft.Interop.LibraryImportGenerator.dll
+ Microsoft.Interop.SourceGeneration.dll
+ System.Text.Json.SourceGeneration.dll
+ System.Text.RegularExpressions.Generator.dll
+/sdk/11.0.100-preview.3.26207.106/Sdks/Microsoft.NET.Sdk/analyzers/
+ Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll
+ Microsoft.CodeAnalysis.NetAnalyzers.dll
+/avalonia/11.3.11/analyzers/dotnet/cs/
+ Avalonia.Analyzers.dll
+ Avalonia.Generators.dll
+/communitytoolkit.mvvm/8.2.1/analyzers/dotnet/roslyn4.3/cs/
+ CommunityToolkit.Mvvm.CodeFixers.dll
+ CommunityToolkit.Mvvm.SourceGenerators.dll
+/dotnetcampus.logger/1.3.0-alpha01/analyzers/dotnet/cs/DotNetCampus.Logger.Analyzer.dll
+
+[analyzerConfigFiles]
+/sdk/11.0.100-preview.3.26207.106/Sdks/Microsoft.NET.Sdk/
+ analyzers/build/config/analysislevel_11_default.globalconfig
+ codestyle/cs/build/config/analysislevelstyle_default.globalconfig
+obj/Debug/net11.0/Ink-Canvas-Next.GeneratedMSBuildEditorConfig.editorconfig
+
+[additionalFiles]
+App.axaml
+Views/MainWindow.axaml
diff --git a/Ink-Canvas-Next/Services/CanvasService.cs b/Ink-Canvas-Next/Services/CanvasService.cs
index a314909..34aa536 100644
--- a/Ink-Canvas-Next/Services/CanvasService.cs
+++ b/Ink-Canvas-Next/Services/CanvasService.cs
@@ -11,16 +11,20 @@ namespace Ink_Canvas_Next.Services;
public class CanvasService
{
private const string DefaultFolderName = "InkCanvasScreenshots";
+ private static int _fileCounter = 0;
///
/// Saves a canvas bitmap to a PNG file
///
/// The rendered canvas bitmap to save
/// The full path to the saved file
+ /// Thrown when bitmap is null
/// Thrown when file save operation fails
/// Thrown when lacking permissions to write
public async Task SaveCanvasToFileAsync(RenderTargetBitmap bitmap)
{
+ ArgumentNullException.ThrowIfNull(bitmap);
+
// Get save directory
var directory = GetSaveDirectory();
@@ -28,11 +32,16 @@ public async Task SaveCanvasToFileAsync(RenderTargetBitmap bitmap)
var fileName = GenerateFileName();
var filePath = Path.Combine(directory, fileName);
- // Save bitmap as PNG
+ // Save bitmap to memory on UI thread (bitmap is an Avalonia UI resource),
+ // then write to disk in background to avoid blocking UI
+ using var memoryStream = new MemoryStream();
+ bitmap.Save(memoryStream);
+ memoryStream.Position = 0;
+
await Task.Run(() =>
{
using var fileStream = File.Create(filePath);
- bitmap.Save(fileStream);
+ memoryStream.CopyTo(fileStream);
});
return filePath;
@@ -60,12 +69,14 @@ private string GetSaveDirectory()
}
///
- /// Generates a unique filename with timestamp
+ /// Generates a unique filename with timestamp and counter
///
- /// Filename in format "InkCanvas_YYYYMMDD_HHMMSS.png"
+ /// Filename in format "InkCanvas_YYYYMMDD_HHmmss_fff_NNN.png"
private string GenerateFileName()
{
- var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
- return $"InkCanvas_{timestamp}.png";
+ var now = DateTime.Now;
+ var timestamp = now.ToString("yyyyMMdd_HHmmss_fff");
+ var counter = System.Threading.Interlocked.Increment(ref _fileCounter);
+ return $"InkCanvas_{timestamp}_{counter:D3}.png";
}
}
diff --git a/Ink-Canvas-Next/ViewModels/MainWindowViewModel.cs b/Ink-Canvas-Next/ViewModels/MainWindowViewModel.cs
index 44574a8..c269d37 100644
--- a/Ink-Canvas-Next/ViewModels/MainWindowViewModel.cs
+++ b/Ink-Canvas-Next/ViewModels/MainWindowViewModel.cs
@@ -1,8 +1,9 @@
-using Avalonia.Media;
+using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System;
+using System.Threading.Tasks;
using Ink_Canvas_Next.Models;
namespace Ink_Canvas_Next.ViewModels
@@ -14,8 +15,27 @@ public enum DrawingTool
Eraser
}
+ public enum UiLanguage
+ {
+ ZhCn,
+ EnUs
+ }
+
public partial class MainWindowViewModel : ViewModelBase
{
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(PenToolTip))]
+ [NotifyPropertyChangedFor(nameof(HighlighterToolTip))]
+ [NotifyPropertyChangedFor(nameof(EraserToolTip))]
+ [NotifyPropertyChangedFor(nameof(SizeToolTip))]
+ [NotifyPropertyChangedFor(nameof(TransparentBackgroundToolTip))]
+ [NotifyPropertyChangedFor(nameof(WhiteboardToolTip))]
+ [NotifyPropertyChangedFor(nameof(BlackboardToolTip))]
+ [NotifyPropertyChangedFor(nameof(ClearCanvasToolTip))]
+ [NotifyPropertyChangedFor(nameof(SaveToolTip))]
+ [NotifyPropertyChangedFor(nameof(ExitToolTip))]
+ private UiLanguage _currentLanguage = UiLanguage.ZhCn;
+
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsPenSelected))]
[NotifyPropertyChangedFor(nameof(IsHighlighterSelected))]
@@ -33,7 +53,7 @@ public partial class MainWindowViewModel : ViewModelBase
private double _penSize = 3.0;
[ObservableProperty]
- private IBrush _backgroundBrush = Brushes.Transparent;
+ private IBrush _backgroundBrush = new SolidColorBrush(Color.Parse("#01000000"));
[ObservableProperty]
private string _backgroundMode = "Transparent";
@@ -42,16 +62,17 @@ public partial class MainWindowViewModel : ViewModelBase
private Color _savedPenColor = Colors.Red;
private bool _isManualColorChange = false;
private bool _isRestoringPenState = false; // Flag to prevent manual color change tracking during restoration
+ private bool _isApplyingAdaptiveColor = false; // Flag to prevent manual color change tracking during adaptive color
// Action to trigger view operations
public Action? ClearCanvasAction { get; set; }
- public Action? SaveCanvasAction { get; set; }
+ public Func? SaveCanvasAction { get; set; }
partial void OnPenColorChanged(Color value)
{
- // Skip tracking if we're restoring pen state
- if (_isRestoringPenState) return;
-
+ // Skip tracking if we're restoring pen state or applying adaptive color
+ if (_isRestoringPenState || _isApplyingAdaptiveColor) return;
+
// Mark as manual color change only if not in highlighter mode
// This allows background adaptive color to work when switching from highlighter
if (CurrentTool != DrawingTool.Highlighter)
@@ -68,6 +89,8 @@ private void ApplyBackgroundAdaptiveColor()
// Skip if user manually changed color
if (_isManualColorChange) return;
+ _isApplyingAdaptiveColor = true;
+
// Apply adaptive color based on background mode
switch (BackgroundMode)
{
@@ -81,6 +104,8 @@ private void ApplyBackgroundAdaptiveColor()
PenColor = Colors.Red;
break;
}
+
+ _isApplyingAdaptiveColor = false;
}
private void SavePenState()
@@ -108,11 +133,33 @@ public double GetEffectiveThickness()
Colors.Orange, Colors.Purple, Colors.Yellow, Colors.White
};
+ public ObservableCollection Languages { get; } = new()
+ {
+ UiLanguage.ZhCn,
+ UiLanguage.EnUs
+ };
+
+ public string PenToolTip => CurrentLanguage == UiLanguage.EnUs ? "Pen" : "画笔";
+ public string HighlighterToolTip => CurrentLanguage == UiLanguage.EnUs ? "Highlighter" : "荧光笔";
+ public string EraserToolTip => CurrentLanguage == UiLanguage.EnUs ? "Eraser" : "橡皮擦";
+ public string SizeToolTip => CurrentLanguage == UiLanguage.EnUs ? "Size" : "笔宽";
+ public string TransparentBackgroundToolTip => CurrentLanguage == UiLanguage.EnUs ? "Transparent" : "透明";
+ public string WhiteboardToolTip => CurrentLanguage == UiLanguage.EnUs ? "Whiteboard" : "白板";
+ public string BlackboardToolTip => CurrentLanguage == UiLanguage.EnUs ? "Blackboard" : "黑板";
+ public string ClearCanvasToolTip => CurrentLanguage == UiLanguage.EnUs ? "Clear Canvas" : "清空画布";
+ public string SaveToolTip => CurrentLanguage == UiLanguage.EnUs ? "Save" : "保存";
+ public string ExitToolTip => CurrentLanguage == UiLanguage.EnUs ? "Exit" : "退出";
+
+ public string CurrentLanguageDisplay => CurrentLanguage == UiLanguage.EnUs ? "English" : "中文";
+
[RelayCommand]
private void ChangeTool(DrawingTool tool)
{
+ var oldTool = CurrentTool;
+ CurrentTool = tool;
+
// Handle switching TO highlighter
- if (tool == DrawingTool.Highlighter && CurrentTool != DrawingTool.Highlighter)
+ if (tool == DrawingTool.Highlighter && oldTool != DrawingTool.Highlighter)
{
// Save current pen color
SavePenState();
@@ -121,7 +168,7 @@ private void ChangeTool(DrawingTool tool)
PenColor = Color.FromArgb(HighlighterSettings.Opacity, 255, 255, 0);
}
// Handle switching FROM highlighter to another tool
- else if (CurrentTool == DrawingTool.Highlighter && tool != DrawingTool.Highlighter)
+ else if (oldTool == DrawingTool.Highlighter && tool != DrawingTool.Highlighter)
{
// Restore saved pen color
RestorePenState();
@@ -132,8 +179,6 @@ private void ChangeTool(DrawingTool tool)
// Apply background adaptive color rules
ApplyBackgroundAdaptiveColor();
}
-
- CurrentTool = tool;
}
[RelayCommand]
@@ -157,13 +202,11 @@ private void ChangeBackground(string mode)
break;
}
- // Apply background adaptive color BEFORE resetting the flag
- // This ensures manual color changes persist until the NEXT background change
- ApplyBackgroundAdaptiveColor();
-
- // Reset manual color change flag AFTER applying adaptive color
- // This allows the next background change to apply adaptive color
+ // Reset manual color change flag to allow adaptive color for this background change
_isManualColorChange = false;
+
+ // Apply background adaptive color
+ ApplyBackgroundAdaptiveColor();
}
[RelayCommand]
@@ -173,9 +216,18 @@ private void Clear()
}
[RelayCommand]
- private void Save()
+ private async Task Save()
+ {
+ if (SaveCanvasAction != null)
+ {
+ await SaveCanvasAction();
+ }
+ }
+
+ [RelayCommand]
+ private void ChangeLanguage(UiLanguage language)
{
- SaveCanvasAction?.Invoke();
+ CurrentLanguage = language;
}
}
}
diff --git a/Ink-Canvas-Next/Views/MainWindow.axaml b/Ink-Canvas-Next/Views/MainWindow.axaml
index 9aa7d9a..f617eb1 100644
--- a/Ink-Canvas-Next/Views/MainWindow.axaml
+++ b/Ink-Canvas-Next/Views/MainWindow.axaml
@@ -5,6 +5,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ic="using:DotNetCampus.Inking"
xmlns:ui="using:FluentAvalonia.UI.Controls"
+ xmlns:conv="using:Ink_Canvas_Next.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ink_Canvas_Next.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
@@ -16,6 +17,10 @@
WindowState="Maximized"
Topmost="False">
+
+
+
+
@@ -25,13 +30,13 @@
-
+
-
+
-
+
@@ -51,31 +56,39 @@
-
+
+
+
+
+
+
+
+
+
-
diff --git a/Ink-Canvas-Next/Views/MainWindow.axaml.cs b/Ink-Canvas-Next/Views/MainWindow.axaml.cs
index 91f9901..030c557 100644
--- a/Ink-Canvas-Next/Views/MainWindow.axaml.cs
+++ b/Ink-Canvas-Next/Views/MainWindow.axaml.cs
@@ -18,18 +18,36 @@ public MainWindow()
InitializeComponent();
}
+ private readonly Services.CanvasService _canvasService = new();
+
+ private MainWindowViewModel? _previousViewModel;
+
protected override void OnDataContextChanged(EventArgs e)
{
+ // Unsubscribe from previous ViewModel to prevent memory leak
+ if (_previousViewModel != null)
+ {
+ _previousViewModel.PropertyChanged -= ViewModel_PropertyChanged;
+ _previousViewModel.ClearCanvasAction = null;
+ _previousViewModel.SaveCanvasAction = null;
+ }
+
base.OnDataContextChanged(e);
+
if (DataContext is MainWindowViewModel vm)
{
+ _previousViewModel = vm;
vm.PropertyChanged += ViewModel_PropertyChanged;
vm.ClearCanvasAction = ClearCanvas;
- vm.SaveCanvasAction = SaveCanvas;
-
+ vm.SaveCanvasAction = SaveCanvasAsync;
+
// Initialize
UpdatePen(vm);
}
+ else
+ {
+ _previousViewModel = null;
+ }
}
private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -48,7 +66,14 @@ private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs
private void UpdatePen(MainWindowViewModel vm)
{
// Access settings via AvaloniaSkiaInkCanvas property
- var settings = MyInkCanvas.AvaloniaSkiaInkCanvas.Settings;
+ var skiaCanvas = MyInkCanvas?.AvaloniaSkiaInkCanvas;
+ if (skiaCanvas == null)
+ {
+ System.Diagnostics.Debug.WriteLine("Warning: AvaloniaSkiaInkCanvas is null, cannot update pen settings");
+ return;
+ }
+
+ var settings = skiaCanvas.Settings;
settings.InkColor = new SKColor(vm.PenColor.R, vm.PenColor.G, vm.PenColor.B, vm.PenColor.A);
// Apply effective thickness (10x for highlighter mode)
@@ -69,150 +94,99 @@ private void ClearCanvas()
{
try
{
- // Primary method: Access the AvaloniaSkiaInkCanvas and try to manipulate strokes
- var skiaCanvas = MyInkCanvas.AvaloniaSkiaInkCanvas;
+ var skiaCanvas = MyInkCanvas?.AvaloniaSkiaInkCanvas;
if (skiaCanvas != null)
{
- // Attempt 1: Try to use reflection to access and clear the internal strokes collection
- // The Strokes property is IReadOnlyList, but the underlying collection might be mutable
- var strokesProperty = skiaCanvas.GetType().GetProperty("Strokes");
- if (strokesProperty != null)
+ // Use ResetStaticStrokeListByEraserResult with empty collection to clear all strokes
+ // This internal method replaces the entire stroke list, which is the correct way to clear
+ var resetMethod = skiaCanvas.GetType().GetMethod("ResetStaticStrokeListByEraserResult",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
+ if (resetMethod != null)
{
- var strokes = strokesProperty.GetValue(skiaCanvas);
- if (strokes != null)
- {
- // Try to find a Clear method on the collection
- var clearMethod = strokes.GetType().GetMethod("Clear");
- if (clearMethod != null)
- {
- clearMethod.Invoke(strokes, null);
- System.Diagnostics.Debug.WriteLine("Canvas cleared using Strokes.Clear()");
-
- // Force a visual update
- MyInkCanvas.InvalidateVisual();
- return;
- }
-
- // Try to find RemoveAll or similar method
- var removeAllMethod = strokes.GetType().GetMethod("RemoveAll");
- if (removeAllMethod != null)
- {
- removeAllMethod.Invoke(strokes, new object[] { (Func