diff --git a/App.xaml.cs b/App.xaml.cs index e597ea3..fc4f826 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -21,17 +21,27 @@ * */ +using LunaDraw.Logic.Utils; + namespace LunaDraw; public partial class App : Application { - public App() - { - InitializeComponent(); - } + public App(IPreferencesFacade preferencesFacade) + { + InitializeComponent(); + + var theme = preferencesFacade.Get(AppPreference.AppTheme); + UserAppTheme = theme switch + { + "Light" => AppTheme.Light, + "Dark" => AppTheme.Dark, + _ => AppTheme.Unspecified + }; + } - protected override Window CreateWindow(IActivationState? activationState) - { - return new Window(new AppShell()); - } + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); + } } \ No newline at end of file diff --git a/Components/AdvancedSettingsPopup.xaml b/Components/AdvancedSettingsPopup.xaml new file mode 100644 index 0000000..6765595 --- /dev/null +++ b/Components/AdvancedSettingsPopup.xaml @@ -0,0 +1,50 @@ + + + + + + + - + + + + - + + + + diff --git a/Components/LayerControlView.xaml.cs b/Components/LayerControlView.xaml.cs index 22a9a66..79664b6 100644 --- a/Components/LayerControlView.xaml.cs +++ b/Components/LayerControlView.xaml.cs @@ -26,65 +26,67 @@ namespace LunaDraw.Components; - public partial class LayerControlView : ContentView - { - public static readonly BindableProperty IsLayerPanelExpandedProperty = - BindableProperty.Create(nameof(IsLayerPanelExpanded), typeof(bool), typeof(LayerControlView), false, propertyChanged: OnIsLayerPanelExpandedChanged); +public partial class LayerControlView : ContentView +{ + public MainViewModel? ViewModel => BindingContext as MainViewModel; - public bool IsLayerPanelExpanded - { - get => (bool)GetValue(IsLayerPanelExpandedProperty); - set => SetValue(IsLayerPanelExpandedProperty, value); - } + public static readonly BindableProperty IsLayerPanelExpandedProperty = + BindableProperty.Create(nameof(IsLayerPanelExpanded), typeof(bool), typeof(LayerControlView), false, propertyChanged: OnIsLayerPanelExpandedChanged); - public List MaskingModes { get; } = Enum.GetValues().Cast().ToList(); + public bool IsLayerPanelExpanded + { + get => (bool)GetValue(IsLayerPanelExpandedProperty); + set => SetValue(IsLayerPanelExpandedProperty, value); + } - public LayerControlView() - { - InitializeComponent(); - } + public List MaskingModes { get; } = Enum.GetValues().Cast().ToList(); - private static void OnIsLayerPanelExpandedChanged(BindableObject bindable, object oldValue, object newValue) - { - var control = (LayerControlView)bindable; - control.ContentGrid.IsVisible = (bool)newValue; - control.CollapseButton.Text = (bool)newValue ? "▼" : "▶"; - } + public LayerControlView() + { + InitializeComponent(); + } - private void OnCollapseClicked(object sender, EventArgs e) - { - IsLayerPanelExpanded = !IsLayerPanelExpanded; - } + private static void OnIsLayerPanelExpandedChanged(BindableObject bindable, object oldValue, object newValue) + { + var control = (LayerControlView)bindable; + control.ContentGrid.IsVisible = (bool)newValue; + control.CollapseButton.Text = (bool)newValue ? "▼" : "▶"; + } - private void OnDragStarting(object sender, DragStartingEventArgs e) - { - if (sender is Element element && element.BindingContext is Layer layer) - { - e.Data.Properties["SourceLayer"] = layer; - // Ensure the dragged layer is selected - if (this.BindingContext is MainViewModel viewModel) - { - viewModel.CurrentLayer = layer; - } - } - } + private void OnCollapseClicked(object sender, EventArgs e) + { + IsLayerPanelExpanded = !IsLayerPanelExpanded; + } - private void OnDragOver(object sender, DragEventArgs e) + private void OnDragStarting(object sender, DragStartingEventArgs e) + { + if (sender is Element element && element.BindingContext is Layer layer) + { + e.Data.Properties["SourceLayer"] = layer; + // Ensure the dragged layer is selected + if (this.BindingContext is MainViewModel viewModel) { - e.AcceptedOperation = DataPackageOperation.Copy; + viewModel.CurrentLayer = layer; } + } + } + + private void OnDragOver(object sender, DragEventArgs e) + { + e.AcceptedOperation = DataPackageOperation.Copy; + } - private void OnDrop(object sender, DropEventArgs e) + private void OnDrop(object sender, DropEventArgs e) + { + if (e.Data.Properties.TryGetValue("SourceLayer", out var sourceObj) && sourceObj is Layer sourceLayer) + { + if (sender is Element element && element.BindingContext is Layer targetLayer) { - if (e.Data.Properties.TryGetValue("SourceLayer", out var sourceObj) && sourceObj is Layer sourceLayer) - { - if (sender is Element element && element.BindingContext is Layer targetLayer) - { - if (this.BindingContext is MainViewModel viewModel) - { - viewModel.ReorderLayer(sourceLayer, targetLayer); - } - } - } + if (this.BindingContext is MainViewModel viewModel) + { + viewModel.ReorderLayer(sourceLayer, targetLayer); + } } + } } +} diff --git a/Components/MiniMapView.xaml b/Components/MiniMapView.xaml index e17521c..825e120 100644 --- a/Components/MiniMapView.xaml +++ b/Components/MiniMapView.xaml @@ -3,11 +3,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:skiasharp="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls" x:Class="LunaDraw.Components.MiniMapView"> - - - + + + diff --git a/Components/MiniMapView.xaml.cs b/Components/MiniMapView.xaml.cs index 65bb4c6..04e0d1c 100644 --- a/Components/MiniMapView.xaml.cs +++ b/Components/MiniMapView.xaml.cs @@ -23,7 +23,9 @@ using System.Reactive.Linq; +using LunaDraw.Logic.Extensions; using LunaDraw.Logic.Messages; +using LunaDraw.Logic.Utils; using LunaDraw.Logic.ViewModels; using ReactiveUI; @@ -36,32 +38,27 @@ namespace LunaDraw.Components; public partial class MiniMapView : ContentView { + private readonly IMessageBus? messageBus; + private readonly IPreferencesFacade? preferencesFacade; private MainViewModel? viewModel; private SKMatrix fitMatrix; private float density = 1.0f; - private IMessageBus? messageBus; - private IMessageBus? MessageBus - { - get - { - if (messageBus != null) return messageBus; - messageBus = Handler?.MauiContext?.Services.GetService() - ?? IPlatformApplication.Current?.Services.GetService(); - return messageBus; - } - } - public MiniMapView() { InitializeComponent(); - this.Loaded += (s, e) => + Loaded += (s, e) => { - MessageBus?.Listen() + messageBus?.Listen() .Throttle(TimeSpan.FromMilliseconds(30), RxApp.MainThreadScheduler) .Subscribe(_ => miniMapCanvas?.InvalidateSurface()); }; + + this.messageBus = Handler?.MauiContext?.Services.GetService() + ?? IPlatformApplication.Current?.Services.GetService(); + this.preferencesFacade = Handler?.MauiContext?.Services.GetService() + ?? IPlatformApplication.Current?.Services.GetService(); } protected override void OnBindingContextChanged() @@ -83,7 +80,8 @@ private void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e) density = (float)(info.Width / view.Width); } - canvas.Clear(SKColors.White); + var bgColor = preferencesFacade?.GetCanvasBackgroundColor() ?? SKColors.White; + canvas.Clear(bgColor); // Calculate bounds of all elements var contentBounds = SKRect.Empty; @@ -217,7 +215,7 @@ private void OnTouch(object? sender, SKTouchEventArgs e) var translation = SKMatrix.CreateTranslation(delta.X, delta.Y); viewModel.NavigationModel.ViewMatrix = viewModel.NavigationModel.ViewMatrix.PostConcat(translation); - MessageBus?.SendMessage(new CanvasInvalidateMessage()); + messageBus?.SendMessage(new CanvasInvalidateMessage()); } e.Handled = true; break; diff --git a/Components/SettingsFlyoutPanel.xaml b/Components/SettingsFlyoutPanel.xaml index 29fd1f6..87f9a44 100644 --- a/Components/SettingsFlyoutPanel.xaml +++ b/Components/SettingsFlyoutPanel.xaml @@ -3,227 +3,227 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:controls="clr-namespace:Maui.ColorPicker;assembly=Maui.ColorPicker" x:Class="LunaDraw.Components.SettingsFlyoutPanel"> - - - - diff --git a/Converters/ActiveShapeIconConverter.cs b/Converters/ActiveShapeIconConverter.cs index 188a52e..82f0d51 100644 --- a/Converters/ActiveShapeIconConverter.cs +++ b/Converters/ActiveShapeIconConverter.cs @@ -28,23 +28,23 @@ namespace LunaDraw.Converters; public class ActiveShapeIconConverter : IValueConverter { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is IDrawingTool tool) { - if (value is IDrawingTool tool) - { - if (tool is RectangleTool) return "▭"; - if (tool is EllipseTool) return "◯"; - if (tool is LineTool) return "/"; - } - - // Default icon for the shapes button if no specific shape tool is active, - // or if the active tool is not a shape (though the text binding usually only matters when it IS a shape, - // or if we want to revert to the default group icon) - return "🔷"; + if (tool is RectangleTool) return "▭"; + if (tool is EllipseTool) return "◯"; + if (tool is LineTool) return "/"; } - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } + // Default icon for the shapes button if no specific shape tool is active, + // or if the active tool is not a shape (though the text binding usually only matters when it IS a shape, + // or if we want to revert to the default group icon) + return "🔷"; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } } diff --git a/Converters/BoolToEyeIconConverter.cs b/Converters/BoolToEyeIconConverter.cs index 3fcd45c..3c91d45 100644 --- a/Converters/BoolToEyeIconConverter.cs +++ b/Converters/BoolToEyeIconConverter.cs @@ -25,19 +25,19 @@ namespace LunaDraw.Converters; - public class BoolToEyeIconConverter : IValueConverter +public class BoolToEyeIconConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - public object Convert(object ?value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool isVisible) - { - return isVisible ? "👁" : "○"; // Eye vs Empty Circle - } - return "○"; - } + if (value is bool isVisible) + { + return isVisible ? "👁" : "○"; // Eye vs Empty Circle + } + return "○"; + } - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is string str && str == "👁"; - } - } \ No newline at end of file + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is string str && str == "👁"; + } +} \ No newline at end of file diff --git a/Converters/BoolToLockIconConverter.cs b/Converters/BoolToLockIconConverter.cs index 3d879e3..9e8da3e 100644 --- a/Converters/BoolToLockIconConverter.cs +++ b/Converters/BoolToLockIconConverter.cs @@ -25,19 +25,19 @@ namespace LunaDraw.Converters; - public class BoolToLockIconConverter : IValueConverter +public class BoolToLockIconConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is bool isLocked) - { - return isLocked ? "🔒" : "🔓"; - } - return "🔓"; - } + if (value is bool isLocked) + { + return isLocked ? "🔒" : "🔓"; + } + return "🔓"; + } - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } \ No newline at end of file + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Converters/ColorToHexConverter.cs b/Converters/ColorToHexConverter.cs index 60b92b6..c30bc26 100644 --- a/Converters/ColorToHexConverter.cs +++ b/Converters/ColorToHexConverter.cs @@ -26,44 +26,44 @@ namespace LunaDraw.Converters; - public class ColorToHexConverter : IValueConverter +public class ColorToHexConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return string.Empty; - - if (value is SKColor color) - { - return $"#{color.Red:X2}{color.Green:X2}{color.Blue:X2}"; - } + if (value == null) return string.Empty; - return string.Empty; - } + if (value is SKColor color) + { + return $"#{color.Red:X2}{color.Green:X2}{color.Blue:X2}"; + } - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string hexString && !string.IsNullOrWhiteSpace(hexString)) - { - hexString = hexString.TrimStart('#'); + return string.Empty; + } - if (hexString.Length == 6) - { - try - { - byte r = System.Convert.ToByte(hexString.Substring(0, 2), 16); - byte g = System.Convert.ToByte(hexString.Substring(2, 2), 16); - byte b = System.Convert.ToByte(hexString.Substring(4, 2), 16); - return new SKColor(r, g, b); - } - catch - { - // Return a safe default color on conversion failure - return SKColors.Black; - } - } - } + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is string hexString && !string.IsNullOrWhiteSpace(hexString)) + { + hexString = hexString.TrimStart('#'); - // Return a safe default color if the input is invalid or empty + if (hexString.Length == 6) + { + try + { + byte r = System.Convert.ToByte(hexString.Substring(0, 2), 16); + byte g = System.Convert.ToByte(hexString.Substring(2, 2), 16); + byte b = System.Convert.ToByte(hexString.Substring(4, 2), 16); + return new SKColor(r, g, b); + } + catch + { + // Return a safe default color on conversion failure return SKColors.Black; + } } + } + + // Return a safe default color if the input is invalid or empty + return SKColors.Black; } +} diff --git a/Converters/IsToolActiveConverter.cs b/Converters/IsToolActiveConverter.cs index 28f9d22..9ecdf30 100644 --- a/Converters/IsToolActiveConverter.cs +++ b/Converters/IsToolActiveConverter.cs @@ -28,28 +28,28 @@ namespace LunaDraw.Converters; public class IsToolActiveConverter : IValueConverter { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is not IDrawingTool activeTool) - return false; - - if (parameter is string targetToolName) - { - // Handle special case for "Shapes" group - if (targetToolName == "Shapes") - { - return activeTool is RectangleTool or EllipseTool or LineTool; - } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not IDrawingTool activeTool) + return false; - var activeTypeName = activeTool.GetType().Name; - return string.Equals(activeTypeName, targetToolName, StringComparison.Ordinal); - } + if (parameter is string targetToolName) + { + // Handle special case for "Shapes" group + if (targetToolName == "Shapes") + { + return activeTool is RectangleTool or EllipseTool or LineTool; + } - return false; + var activeTypeName = activeTool.GetType().Name; + return string.Equals(activeTypeName, targetToolName, StringComparison.Ordinal); } - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } + return false; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } } diff --git a/Converters/ToolNameToIconConverter.cs b/Converters/ToolNameToIconConverter.cs deleted file mode 100644 index c84c453..0000000 --- a/Converters/ToolNameToIconConverter.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2025 CodeSoupCafe LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -using System.Globalization; - -namespace LunaDraw.Converters; - -// Simple converter that maps tool names to an icon glyph (emoji for now). -// This keeps UI independent of a specific icon font and allows swapping to Syncfusion glyphs later. -public class ToolNameToIconConverter : IValueConverter -{ - private static readonly Dictionary Map = new(StringComparer.OrdinalIgnoreCase) - { - { "Select", "🔲" }, - { "Line", "/" }, - { "Rectangle", "▭" }, - { "Ellipse", "◯" }, - { "Freehand", "✏️" }, - { "Eraser", "🧽" }, - { "Fill", "🖌️" }, - // Fallback for unknown tools - { "Default", "🔧" } - }; - - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string name && !string.IsNullOrWhiteSpace(name)) - { - if (Map.TryGetValue(name, out var glyph)) - return glyph; - - // Try to return the first character as a fallback - return name.Length > 0 ? name.Substring(0, 1) : Map["Default"]; - } - - return Map["Default"]; - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} diff --git a/Converters/ToolToColorConverter.cs b/Converters/ToolToColorConverter.cs deleted file mode 100644 index cd37e70..0000000 --- a/Converters/ToolToColorConverter.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2025 CodeSoupCafe LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -using System.Globalization; -using LunaDraw.Logic.Tools; - -namespace LunaDraw.Converters; - -public class ToolToColorConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isActive = false; - - if (value is IDrawingTool activeTool && parameter is string targetToolName) - { - if (targetToolName == "Shapes") - { - isActive = activeTool is RectangleTool or EllipseTool or LineTool; - } - else - { - isActive = string.Equals(activeTool.GetType().Name, targetToolName, StringComparison.Ordinal); - } - } - - if (isActive) - { - if (Application.Current?.Resources.TryGetValue("Secondary", out var color) == true) - return color; - return Colors.Orange; // Fallback - } - - // Inactive Color - var isDark = Application.Current?.RequestedTheme == AppTheme.Dark; - var key = isDark ? "Gray700" : "Gray200"; - - if (Application.Current?.Resources.TryGetValue(key, out var inactiveColor) == true) - return inactiveColor; - - return isDark ? Colors.DarkGray : Colors.LightGray; // Fallback - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} diff --git a/Logic/Config/FeatureFlags.cs b/Logic/Config/FeatureFlags.cs deleted file mode 100644 index 0c7d1ae..0000000 --- a/Logic/Config/FeatureFlags.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace LunaDraw.Logic.Config; - -public static class FeatureFlags -{ - public static bool EnableTransparentBackground { get; set; } = false; -} diff --git a/Logic/Extensions/PreferencesExtensions.cs b/Logic/Extensions/PreferencesExtensions.cs new file mode 100644 index 0000000..7d4a09b --- /dev/null +++ b/Logic/Extensions/PreferencesExtensions.cs @@ -0,0 +1,24 @@ +using LunaDraw.Logic.Utils; +using SkiaSharp; + +namespace LunaDraw.Logic.Extensions; + +public static class PreferencesExtensions +{ + public static SKColor GetCanvasBackgroundColor(this IPreferencesFacade preferencesFacade) + { + var isTransparentBackground = preferencesFacade.Get(AppPreference.IsTransparentBackgroundEnabled); + + if (isTransparentBackground) return SKColors.Transparent; + + var selectedTheme = Application.Current?.RequestedTheme; + var settingTheme = preferencesFacade.Get(AppPreference.AppTheme); + + if (settingTheme != PreferencesFacade.Defaults[AppPreference.AppTheme]) + { + selectedTheme = settingTheme == AppTheme.Dark.ToString() ? AppTheme.Dark : AppTheme.Light; + } + + return selectedTheme == AppTheme.Dark ? SKColors.Black : SKColors.White; + } +} diff --git a/Logic/Extensions/SkiaSharpExtensions.cs b/Logic/Extensions/SkiaSharpExtensions.cs index 88f2097..1c0c04e 100644 --- a/Logic/Extensions/SkiaSharpExtensions.cs +++ b/Logic/Extensions/SkiaSharpExtensions.cs @@ -27,6 +27,73 @@ namespace LunaDraw.Logic.Extensions; public static class SkiaSharpExtensions { + public static SKBitmap LoadBitmapDownsampled(string path, int targetWidth, int targetHeight) + { + try + { + if (!File.Exists(path)) + { + System.Diagnostics.Debug.WriteLine($"[BitmapCache] File not found: {path}"); + + return new SKBitmap(); + } + + using var stream = File.OpenRead(path); + using var codec = SKCodec.Create(stream); + + if (codec == null) + { + System.Diagnostics.Debug.WriteLine($"[BitmapCache] Failed to create codec for: {path}"); + + return new SKBitmap(); + } + + var info = codec.Info; + + // Calculate scale + float scale = 1.0f; + if (targetWidth > 0 && targetHeight > 0) + { + float scaleX = (float)targetWidth / info.Width; + float scaleY = (float)targetHeight / info.Height; + scale = Math.Min(scaleX, scaleY); + } + + if (scale >= 1.0f || (targetWidth == 0 && targetHeight == 0)) + { + return SKBitmap.Decode(codec); + } + + // Get supported dimensions for this scale + var supportedInfo = codec.GetScaledDimensions(scale); + + // Use the supported dimensions for decoding + var decodeInfo = new SKImageInfo(supportedInfo.Width, supportedInfo.Height, info.ColorType, info.AlphaType); + + var bitmap = new SKBitmap(decodeInfo); + var result = codec.GetPixels(decodeInfo, bitmap.GetPixels()); + + if (result == SKCodecResult.Success || result == SKCodecResult.IncompleteInput) + { + return bitmap; + } + else + { + System.Diagnostics.Debug.WriteLine($"[BitmapCache] GetPixels failed: {result}"); + bitmap.Dispose(); + // Fallback: try full decode if downsample fails? + // Or maybe the scale was just invalid. + return new SKBitmap(); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[BitmapCache] Exception loading bitmap: {ex}"); + + return new SKBitmap(); + } + } + public static int GetAlphaPixelCount(this SKPixmap pixmap) { return GetAlphaPixelCounts(pixmap)[0]; diff --git a/Logic/Handlers/CanvasInputHandler.cs b/Logic/Handlers/CanvasInputHandler.cs index 1f130c9..610dc9f 100644 --- a/Logic/Handlers/CanvasInputHandler.cs +++ b/Logic/Handlers/CanvasInputHandler.cs @@ -30,7 +30,7 @@ using SkiaSharp; using SkiaSharp.Views.Maui; -namespace LunaDraw.Logic.Services; +namespace LunaDraw.Logic.Utils; public class CanvasInputHandler( ToolbarViewModel toolbarViewModel, @@ -207,8 +207,8 @@ private void HandleMultiTouch() float rotation = angle - startAngle; // Aggressive deadzones to filter out noise - if (Math.Abs(scale - 1.0f) < 0.05f) scale = 1.0f; - if (Math.Abs(rotation) < 0.2f) rotation = 0f; // ~11 degrees + if (Math.Abs(scale - 1.0f) < 0.01f) scale = 1.0f; // Reduced from 0.05f + if (Math.Abs(rotation) < 0.05f) rotation = 0f; // Reduced from 0.2f (~3 degrees) // Build transform around start centroid var transform = SKMatrix.CreateIdentity(); @@ -248,6 +248,19 @@ private void HandleMultiTouch() // Calculate target matrix var targetMatrix = SKMatrix.Concat(transform, startMatrix); + // Clamp scale + float currentScale = targetMatrix.ScaleX; // Assuming uniform scale + if (currentScale < 0.1f) + { + float correction = 0.1f / currentScale; + targetMatrix = SKMatrix.Concat(SKMatrix.CreateScale(correction, correction, centroid.X, centroid.Y), targetMatrix); + } + else if (currentScale > 20.0f) + { + float correction = 20.0f / currentScale; + targetMatrix = SKMatrix.Concat(SKMatrix.CreateScale(correction, correction, centroid.X, centroid.Y), targetMatrix); + } + // Smooth the output using exponential moving average var smoothedMatrix = LerpMatrix(previousOutputMatrix, targetMatrix, SmoothingFactor); previousOutputMatrix = smoothedMatrix; diff --git a/Logic/Handlers/ICanvasInputHandler.cs b/Logic/Handlers/ICanvasInputHandler.cs index fddfb67..38bb4ac 100644 --- a/Logic/Handlers/ICanvasInputHandler.cs +++ b/Logic/Handlers/ICanvasInputHandler.cs @@ -26,7 +26,7 @@ // For SKCanvasView -namespace LunaDraw.Logic.Services; +namespace LunaDraw.Logic.Utils; public interface ICanvasInputHandler { diff --git a/Logic/Messages/BrushSettingsChangedMessage.cs b/Logic/Messages/BrushSettingsChangedMessage.cs index af5b8a8..8d91ce9 100644 --- a/Logic/Messages/BrushSettingsChangedMessage.cs +++ b/Logic/Messages/BrushSettingsChangedMessage.cs @@ -25,39 +25,39 @@ namespace LunaDraw.Logic.Messages; - /// - /// Message sent when brush settings (color, transparency) change. - /// - public class BrushSettingsChangedMessage( - SKColor? strokeColor = null, - SKColor? fillColor = null, - byte? transparency = null, - byte? flow = null, - float? spacing = null, - float? strokeWidth = null, - bool? isGlowEnabled = null, - SKColor? glowColor = null, - float? glowRadius = null, - bool? isRainbowEnabled = null, - float? scatterRadius = null, - float? sizeJitter = null, - float? angleJitter = null, - float? hueJitter = null, - bool shouldClearFillColor = false) - { - public SKColor? StrokeColor { get; } = strokeColor; - public SKColor? FillColor { get; } = fillColor; - public byte? Transparency { get; } = transparency; - public byte? Flow { get; } = flow; - public float? Spacing { get; } = spacing; - public float? StrokeWidth { get; } = strokeWidth; - public bool? IsGlowEnabled { get; } = isGlowEnabled; - public SKColor? GlowColor { get; } = glowColor; - public float? GlowRadius { get; } = glowRadius; - public bool? IsRainbowEnabled { get; } = isRainbowEnabled; - public float? ScatterRadius { get; } = scatterRadius; - public float? SizeJitter { get; } = sizeJitter; - public float? AngleJitter { get; } = angleJitter; - public float? HueJitter { get; } = hueJitter; - public bool ShouldClearFillColor { get; } = shouldClearFillColor; - } +/// +/// Message sent when brush settings (color, transparency) change. +/// +public class BrushSettingsChangedMessage( + SKColor? strokeColor = null, + SKColor? fillColor = null, + byte? transparency = null, + byte? flow = null, + float? spacing = null, + float? strokeWidth = null, + bool? isGlowEnabled = null, + SKColor? glowColor = null, + float? glowRadius = null, + bool? isRainbowEnabled = null, + float? scatterRadius = null, + float? sizeJitter = null, + float? angleJitter = null, + float? hueJitter = null, + bool shouldClearFillColor = false) +{ + public SKColor? StrokeColor { get; } = strokeColor; + public SKColor? FillColor { get; } = fillColor; + public byte? Transparency { get; } = transparency; + public byte? Flow { get; } = flow; + public float? Spacing { get; } = spacing; + public float? StrokeWidth { get; } = strokeWidth; + public bool? IsGlowEnabled { get; } = isGlowEnabled; + public SKColor? GlowColor { get; } = glowColor; + public float? GlowRadius { get; } = glowRadius; + public bool? IsRainbowEnabled { get; } = isRainbowEnabled; + public float? ScatterRadius { get; } = scatterRadius; + public float? SizeJitter { get; } = sizeJitter; + public float? AngleJitter { get; } = angleJitter; + public float? HueJitter { get; } = hueJitter; + public bool ShouldClearFillColor { get; } = shouldClearFillColor; +} diff --git a/Logic/Messages/BrushShapeChangedMessage.cs b/Logic/Messages/BrushShapeChangedMessage.cs index 57cf612..b01d8dc 100644 --- a/Logic/Messages/BrushShapeChangedMessage.cs +++ b/Logic/Messages/BrushShapeChangedMessage.cs @@ -25,7 +25,7 @@ namespace LunaDraw.Logic.Messages; - public class BrushShapeChangedMessage(BrushShape shape) - { - public BrushShape Shape { get; } = shape; - } +public class BrushShapeChangedMessage(BrushShape shape) +{ + public BrushShape Shape { get; } = shape; +} diff --git a/Logic/Messages/CanvasInvalidateMessage.cs b/Logic/Messages/CanvasInvalidateMessage.cs index 2b0ae9c..6d3f109 100644 --- a/Logic/Messages/CanvasInvalidateMessage.cs +++ b/Logic/Messages/CanvasInvalidateMessage.cs @@ -23,10 +23,10 @@ namespace LunaDraw.Logic.Messages; - /// - /// Message sent to request the canvas to invalidate and redraw. - /// - public class CanvasInvalidateMessage - { - // No properties needed, just a signal - } +/// +/// Message sent to request the canvas to invalidate and redraw. +/// +public class CanvasInvalidateMessage +{ + // No properties needed, just a signal +} diff --git a/Logic/Messages/DrawingStateChangedMessage.cs b/Logic/Messages/DrawingStateChangedMessage.cs index 51344d7..ac24e21 100644 --- a/Logic/Messages/DrawingStateChangedMessage.cs +++ b/Logic/Messages/DrawingStateChangedMessage.cs @@ -23,9 +23,9 @@ namespace LunaDraw.Logic.Messages; - /// - /// A message to indicate that the drawable state of the canvas has changed and a history snapshot should be taken. - /// - public class DrawingStateChangedMessage - { - } +/// +/// A message to indicate that the drawable state of the canvas has changed and a history snapshot should be taken. +/// +public class DrawingStateChangedMessage +{ +} diff --git a/Logic/Messages/ElementAddedMessage.cs b/Logic/Messages/ElementAddedMessage.cs index 40abca2..4dfee30 100644 --- a/Logic/Messages/ElementAddedMessage.cs +++ b/Logic/Messages/ElementAddedMessage.cs @@ -25,11 +25,11 @@ namespace LunaDraw.Logic.Messages; - /// - /// Message sent when a new element is added to a layer. - /// - public class ElementAddedMessage(IDrawableElement element, Layer targetLayer) - { - public IDrawableElement Element { get; } = element; - public Layer TargetLayer { get; } = targetLayer; - } +/// +/// Message sent when a new element is added to a layer. +/// +public class ElementAddedMessage(IDrawableElement element, Layer targetLayer) +{ + public IDrawableElement Element { get; } = element; + public Layer TargetLayer { get; } = targetLayer; +} diff --git a/Logic/Messages/ElementRemovedMessage.cs b/Logic/Messages/ElementRemovedMessage.cs index 526611f..8a63329 100644 --- a/Logic/Messages/ElementRemovedMessage.cs +++ b/Logic/Messages/ElementRemovedMessage.cs @@ -25,11 +25,11 @@ namespace LunaDraw.Logic.Messages; - /// - /// Message sent when an element is removed from a layer. - /// - public class ElementRemovedMessage(IDrawableElement element, Layer sourceLayer) - { - public IDrawableElement Element { get; } = element; - public Layer SourceLayer { get; } = sourceLayer; - } +/// +/// Message sent when an element is removed from a layer. +/// +public class ElementRemovedMessage(IDrawableElement element, Layer sourceLayer) +{ + public IDrawableElement Element { get; } = element; + public Layer SourceLayer { get; } = sourceLayer; +} diff --git a/Logic/Messages/LayerChangedMessage.cs b/Logic/Messages/LayerChangedMessage.cs index ad6f7e2..f7dc5f9 100644 --- a/Logic/Messages/LayerChangedMessage.cs +++ b/Logic/Messages/LayerChangedMessage.cs @@ -25,10 +25,10 @@ namespace LunaDraw.Logic.Messages; - /// - /// Message sent when a layer's properties (e.g., visibility, lock status) change. - /// - public class LayerChangedMessage(Layer changedLayer) - { - public Layer ChangedLayer { get; } = changedLayer; - } +/// +/// Message sent when a layer's properties (e.g., visibility, lock status) change. +/// +public class LayerChangedMessage(Layer changedLayer) +{ + public Layer ChangedLayer { get; } = changedLayer; +} diff --git a/Logic/Messages/SelectionChangedMessage.cs b/Logic/Messages/SelectionChangedMessage.cs index 77c157c..2a1a7af 100644 --- a/Logic/Messages/SelectionChangedMessage.cs +++ b/Logic/Messages/SelectionChangedMessage.cs @@ -25,10 +25,10 @@ namespace LunaDraw.Logic.Messages; - /// - /// Message sent when the selection of elements changes. - /// - public class SelectionChangedMessage(IEnumerable selectedElements) - { - public IEnumerable SelectedElements { get; } = selectedElements; - } +/// +/// Message sent when the selection of elements changes. +/// +public class SelectionChangedMessage(IEnumerable selectedElements) +{ + public IEnumerable SelectedElements { get; } = selectedElements; +} diff --git a/Logic/Messages/ShowAdvancedSettingsMessage.cs b/Logic/Messages/ShowAdvancedSettingsMessage.cs new file mode 100644 index 0000000..ee83ec6 --- /dev/null +++ b/Logic/Messages/ShowAdvancedSettingsMessage.cs @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 CodeSoupCafe LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +namespace LunaDraw.Logic.Messages; + +public class ShowAdvancedSettingsMessage +{ +} diff --git a/Logic/Messages/ToolChangedMessage.cs b/Logic/Messages/ToolChangedMessage.cs index 9357c5e..d00bffd 100644 --- a/Logic/Messages/ToolChangedMessage.cs +++ b/Logic/Messages/ToolChangedMessage.cs @@ -25,10 +25,10 @@ namespace LunaDraw.Logic.Messages; - /// - /// Message sent when the active drawing tool changes. - /// - public class ToolChangedMessage(IDrawingTool newTool) - { - public IDrawingTool NewTool { get; } = newTool; - } +/// +/// Message sent when the active drawing tool changes. +/// +public class ToolChangedMessage(IDrawingTool newTool) +{ + public IDrawingTool NewTool { get; } = newTool; +} diff --git a/Logic/Messages/ViewOptionsChangedMessage.cs b/Logic/Messages/ViewOptionsChangedMessage.cs new file mode 100644 index 0000000..26987d8 --- /dev/null +++ b/Logic/Messages/ViewOptionsChangedMessage.cs @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 CodeSoupCafe LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +namespace LunaDraw.Logic.Messages; + +public class ViewOptionsChangedMessage +{ + public bool ShowButtonLabels { get; } + public bool ShowLayersPanel { get; } + + public ViewOptionsChangedMessage(bool showButtonLabels, bool showLayersPanel) + { + ShowButtonLabels = showButtonLabels; + ShowLayersPanel = showLayersPanel; + } +} diff --git a/Logic/Models/DrawableImage.cs b/Logic/Models/DrawableImage.cs index 7b6ff4c..309f2c8 100644 --- a/Logic/Models/DrawableImage.cs +++ b/Logic/Models/DrawableImage.cs @@ -100,7 +100,7 @@ public void Draw(SKCanvas canvas) // Draw the Bitmap using (var image = SKImage.FromBitmap(Bitmap)) { - canvas.DrawImage(image, bounds, new SKSamplingOptions(SKFilterMode.Linear), paint); + canvas.DrawImage(image, bounds, new SKSamplingOptions(SKCubicResampler.Mitchell), paint); } // Draw Border if set diff --git a/Logic/Services/IPreferencesService.cs b/Logic/Services/IPreferencesService.cs deleted file mode 100644 index 8762798..0000000 --- a/Logic/Services/IPreferencesService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LunaDraw.Logic.Services; - -public interface IPreferencesService -{ - bool Get(string key, bool defaultValue); - void Set(string key, bool value); -} diff --git a/Logic/Services/PreferencesService.cs b/Logic/Services/PreferencesService.cs deleted file mode 100644 index a334e03..0000000 --- a/Logic/Services/PreferencesService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Maui.Storage; - -namespace LunaDraw.Logic.Services; - -public class PreferencesService : IPreferencesService -{ - public bool Get(string key, bool defaultValue) => Preferences.Get(key, defaultValue); - public void Set(string key, bool value) => Preferences.Set(key, value); -} diff --git a/Logic/Tools/EraserBrushTool.cs b/Logic/Tools/EraserBrushTool.cs index bd9b6b1..197e3ff 100644 --- a/Logic/Tools/EraserBrushTool.cs +++ b/Logic/Tools/EraserBrushTool.cs @@ -21,16 +21,17 @@ * */ +using LunaDraw.Logic.Extensions; using LunaDraw.Logic.Messages; using LunaDraw.Logic.Models; - +using LunaDraw.Logic.Utils; using ReactiveUI; using SkiaSharp; namespace LunaDraw.Logic.Tools; -public class EraserBrushTool(IMessageBus messageBus) : IDrawingTool +public class EraserBrushTool(IMessageBus messageBus, IPreferencesFacade preferencesFacade) : IDrawingTool { public string Name => "Eraser"; public ToolType Type => ToolType.Eraser; @@ -38,6 +39,7 @@ public class EraserBrushTool(IMessageBus messageBus) : IDrawingTool private SKPath? currentPath; private DrawablePath? currentDrawablePath; private readonly IMessageBus messageBus = messageBus; + private readonly IPreferencesFacade preferencesFacade = preferencesFacade; public void OnTouchPressed(SKPoint point, ToolContext context) { @@ -49,7 +51,7 @@ public void OnTouchPressed(SKPoint point, ToolContext context) currentDrawablePath = new DrawablePath { Path = currentPath, - StrokeColor = SKColors.White, // Visual preview color + StrokeColor = preferencesFacade.GetCanvasBackgroundColor(), // Visual preview color StrokeWidth = context.StrokeWidth * 2, // Eraser usually wider Opacity = 255, BlendMode = SKBlendMode.SrcOver, diff --git a/Logic/Tools/RectangleTool.cs b/Logic/Tools/RectangleTool.cs index cf32f4e..b633c94 100644 --- a/Logic/Tools/RectangleTool.cs +++ b/Logic/Tools/RectangleTool.cs @@ -32,7 +32,7 @@ public class RectangleTool(IMessageBus messageBus) : ShapeTool "Rectangle"; public override ToolType Type => ToolType.Rectangle; - protected override DrawableRectangle CreateShape(ToolContext context) + protected override DrawableRectangle CreateShape(ToolContext context) { return new DrawableRectangle { diff --git a/Logic/Utils/BitmapCache.cs b/Logic/Utils/BitmapCache.cs index f0be4c9..5eef2db 100644 --- a/Logic/Utils/BitmapCache.cs +++ b/Logic/Utils/BitmapCache.cs @@ -23,6 +23,7 @@ using SkiaSharp; using System.Collections.Concurrent; +using LunaDraw.Logic.Extensions; namespace LunaDraw.Logic.Utils; @@ -46,7 +47,7 @@ public SKBitmap GetBitmap(string path, int targetWidth, int targetHeight) return bitmap; } - var newBitmap = BitmapCache.LoadDownsampledBitmap(path, targetWidth, targetHeight); + var newBitmap = SkiaSharpExtensions.LoadBitmapDownsampled(path, targetWidth, targetHeight); if (newBitmap != null) { cache.AddOrUpdate(key, @@ -68,7 +69,7 @@ public async Task GetBitmapAsync(string path, int targetWidth, int tar return await Task.Run(() => { - var newBitmap = BitmapCache.LoadDownsampledBitmap(path, targetWidth, targetHeight); + var newBitmap = SkiaSharpExtensions.LoadBitmapDownsampled(path, targetWidth, targetHeight); if (newBitmap != null) { cache.AddOrUpdate(key, @@ -85,73 +86,6 @@ private static string GenerateKey(string path, int width, int height) return $"{path}_{width}x{height}"; } - private static SKBitmap LoadDownsampledBitmap(string path, int targetWidth, int targetHeight) - { - try - { - if (!File.Exists(path)) - { - System.Diagnostics.Debug.WriteLine($"[BitmapCache] File not found: {path}"); - - return new SKBitmap(); - } - - using var stream = File.OpenRead(path); - using var codec = SKCodec.Create(stream); - - if (codec == null) - { - System.Diagnostics.Debug.WriteLine($"[BitmapCache] Failed to create codec for: {path}"); - - return new SKBitmap(); - } - - var info = codec.Info; - - // Calculate scale - float scale = 1.0f; - if (targetWidth > 0 && targetHeight > 0) - { - float scaleX = (float)targetWidth / info.Width; - float scaleY = (float)targetHeight / info.Height; - scale = Math.Min(scaleX, scaleY); - } - - if (scale >= 1.0f || (targetWidth == 0 && targetHeight == 0)) - { - return SKBitmap.Decode(codec); - } - - // Get supported dimensions for this scale - var supportedInfo = codec.GetScaledDimensions(scale); - - // Use the supported dimensions for decoding - var decodeInfo = new SKImageInfo(supportedInfo.Width, supportedInfo.Height, info.ColorType, info.AlphaType); - - var bitmap = new SKBitmap(decodeInfo); - var result = codec.GetPixels(decodeInfo, bitmap.GetPixels()); - - if (result == SKCodecResult.Success || result == SKCodecResult.IncompleteInput) - { - return bitmap; - } - else - { - System.Diagnostics.Debug.WriteLine($"[BitmapCache] GetPixels failed: {result}"); - bitmap.Dispose(); - // Fallback: try full decode if downsample fails? - // Or maybe the scale was just invalid. - return new SKBitmap(); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[BitmapCache] Exception loading bitmap: {ex}"); - - return new SKBitmap(); - } - } - public void ClearCache() { foreach (var weakRef in cache.Values) diff --git a/Logic/Utils/IPreferencesFacade.cs b/Logic/Utils/IPreferencesFacade.cs new file mode 100644 index 0000000..21b4b4f --- /dev/null +++ b/Logic/Utils/IPreferencesFacade.cs @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 CodeSoupCafe LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +namespace LunaDraw.Logic.Utils; + +public interface IPreferencesFacade +{ + + string Get(AppPreference key); + + T Get(AppPreference key); + + void Set(AppPreference key, bool value); + + void Set(AppPreference key, T? value) => Preferences.Set(key.ToString(), value?.ToString()); +} diff --git a/Platforms/Windows/CompositionBrush.cs.cs b/Logic/Utils/PreferencesFacade.cs similarity index 51% rename from Platforms/Windows/CompositionBrush.cs.cs rename to Logic/Utils/PreferencesFacade.cs index 8cf226f..7a3f20f 100644 --- a/Platforms/Windows/CompositionBrush.cs.cs +++ b/Logic/Utils/PreferencesFacade.cs @@ -21,47 +21,43 @@ * */ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Media; +namespace LunaDraw.Logic.Utils; -// Alias namespaces to avoid ambiguity and ensure correct types are used -using WinComp = Windows.UI.Composition; -using WinUIComp = Microsoft.UI.Composition; - -namespace LunaDraw.WinUI; - -public abstract class CompositionBrushBackdrop : SystemBackdrop +public enum AppPreference { - private WinComp.CompositionBrush? brush; - private WinComp.Compositor? compositor; - - protected abstract WinComp.CompositionBrush CreateBrush(WinComp.Compositor compositor); + AppTheme, + ShowButtonLabels, + ShowLayersPanel, + IsTransparentBackgroundEnabled +} - protected override void OnTargetConnected(WinUIComp.ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot) +public class AppPreferenceDefault +{ + public dynamic this[AppPreference appPreference] { - base.OnTargetConnected(connectedTarget, xamlRoot); - - compositor = new WinComp.Compositor(); + get + { + return appPreference switch + { + AppPreference.AppTheme => "Automatic", + AppPreference.ShowButtonLabels => false, + AppPreference.ShowLayersPanel => false, + AppPreference.IsTransparentBackgroundEnabled => false, + _ => "" + }; + } + } +} - brush = CreateBrush(compositor); +public class PreferencesFacade : IPreferencesFacade +{ + public static AppPreferenceDefault Defaults => new(); - connectedTarget.SystemBackdrop = brush; - } + public string Get(AppPreference key) => Preferences.Get(key.ToString(), Defaults[key]); - protected override void OnTargetDisconnected(WinUIComp.ICompositionSupportsSystemBackdrop disconnectedTarget) - { - base.OnTargetDisconnected(disconnectedTarget); + public T Get(AppPreference key) => Preferences.Get(key.ToString(), Defaults[key]); - if (brush != null) - { - brush.Dispose(); - brush = null; - } + public void Set(AppPreference key, bool value) => Preferences.Set(key.ToString(), value); - if (compositor != null) - { - compositor.Dispose(); - compositor = null; - } - } + public void Set(AppPreference key, T? value) => Preferences.Set(key.ToString(), value?.ToString()); } diff --git a/Logic/ViewModels/LayerPanelViewModel.cs b/Logic/ViewModels/LayerPanelViewModel.cs index a3dd0d9..64a4b6b 100644 --- a/Logic/ViewModels/LayerPanelViewModel.cs +++ b/Logic/ViewModels/LayerPanelViewModel.cs @@ -25,11 +25,11 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; -using LunaDraw.Logic.Services; using LunaDraw.Logic.Utils; using LunaDraw.Logic.Messages; using LunaDraw.Logic.Models; using ReactiveUI; +using System.Windows.Input; namespace LunaDraw.Logic.ViewModels; @@ -37,14 +37,13 @@ public class LayerPanelViewModel : ReactiveObject { private readonly ILayerFacade layerFacade; private readonly IMessageBus messageBus; - private readonly IPreferencesService preferencesService; + private readonly IPreferencesFacade preferencesFacade; - public LayerPanelViewModel(ILayerFacade layerFacade, IMessageBus messageBus, IPreferencesService preferencesService) + public LayerPanelViewModel(ILayerFacade layerFacade, IMessageBus messageBus, IPreferencesFacade preferencesFacade) { this.layerFacade = layerFacade; this.messageBus = messageBus; - this.preferencesService = preferencesService; - + this.preferencesFacade = preferencesFacade; layerFacade.WhenAnyValue(x => x.CurrentLayer) .Subscribe(_ => this.RaisePropertyChanged(nameof(CurrentLayer))); @@ -104,8 +103,22 @@ public LayerPanelViewModel(ILayerFacade layerFacade, IMessageBus messageBus, IPr } }, outputScheduler: RxApp.MainThreadScheduler); + ToggleTraceModeCommand = ReactiveCommand.Create(() => + { + if (IsTransparentBackground) + { + WindowTransparency = 255; + } + else + { + WindowTransparency = 125; + } + + messageBus.SendMessage(new CanvasInvalidateMessage()); + }, outputScheduler: RxApp.MainThreadScheduler); + // Initialize state from Preferences - IsTransparentBackground = preferencesService.Get("IsTransparentBackgroundEnabled", false); + IsTransparentBackground = preferencesFacade.Get(AppPreference.IsTransparentBackgroundEnabled); if (!IsTransparentBackground) { windowTransparency = 255; @@ -120,6 +133,14 @@ public Layer? CurrentLayer set => layerFacade.CurrentLayer = value; } + public ReactiveCommand AddLayerCommand { get; } + public ReactiveCommand RemoveLayerCommand { get; } + public ReactiveCommand MoveLayerForwardCommand { get; } + public ReactiveCommand MoveLayerBackwardCommand { get; } + public ReactiveCommand ToggleLayerVisibilityCommand { get; } + public ReactiveCommand ToggleLayerLockCommand { get; } + public ReactiveCommand ToggleTraceModeCommand { get; } + private bool isTransparentBackground = false; public bool IsTransparentBackground { @@ -127,11 +148,10 @@ public bool IsTransparentBackground set { this.RaiseAndSetIfChanged(ref isTransparentBackground, value); - preferencesService.Set("IsTransparentBackgroundEnabled", value); + preferencesFacade.Set(AppPreference.IsTransparentBackgroundEnabled, value); if (!isTransparentBackground) { - WindowTransparency = 255; UpdateWindowTransparency(); } @@ -177,13 +197,4 @@ private void UpdateWindowTransparency() } #endif } - - public static bool IsTransparentBackgroundVisible => Config.FeatureFlags.EnableTransparentBackground; - - public ReactiveCommand AddLayerCommand { get; } - public ReactiveCommand RemoveLayerCommand { get; } - public ReactiveCommand MoveLayerForwardCommand { get; } - public ReactiveCommand MoveLayerBackwardCommand { get; } - public ReactiveCommand ToggleLayerVisibilityCommand { get; } - public ReactiveCommand ToggleLayerLockCommand { get; } } diff --git a/Logic/ViewModels/MainViewModel.cs b/Logic/ViewModels/MainViewModel.cs index 2be2030..daa21e9 100644 --- a/Logic/ViewModels/MainViewModel.cs +++ b/Logic/ViewModels/MainViewModel.cs @@ -28,13 +28,13 @@ using LunaDraw.Logic.Utils; using LunaDraw.Logic.Messages; using LunaDraw.Logic.Models; -using LunaDraw.Logic.Services; using LunaDraw.Logic.Tools; using ReactiveUI; using SkiaSharp; using SkiaSharp.Views.Maui; +using CommunityToolkit.Maui.Extensions; namespace LunaDraw.Logic.ViewModels; @@ -47,6 +47,7 @@ public class MainViewModel : ReactiveObject public NavigationModel NavigationModel { get; } public SelectionObserver SelectionObserver { get; } private readonly IMessageBus messageBus; + private readonly IPreferencesFacade preferencesFacade; // Sub-ViewModels public LayerPanelViewModel LayerPanelVM { get; } @@ -60,6 +61,46 @@ public class MainViewModel : ReactiveObject public SKRect CanvasSize { get; set; } + // UI State + public List AvailableThemes { get; } = new List { "Automatic", "Light", "Dark" }; + + private string selectedTheme = "Automatic"; + public string SelectedTheme + { + get => selectedTheme; + set + { + this.RaiseAndSetIfChanged(ref selectedTheme, value); + preferencesFacade.Set(AppPreference.AppTheme, value); + UpdateAppTheme(value); + } + } + + private bool showButtonLabels; + + public bool ShowButtonLabels + { + get => showButtonLabels; + set + { + this.RaiseAndSetIfChanged(ref showButtonLabels, value); + preferencesFacade.Set(AppPreference.ShowButtonLabels, value); + messageBus.SendMessage(new ViewOptionsChangedMessage(value, ShowLayersPanel)); + } + } + + private bool showLayersPanel; + public bool ShowLayersPanel + { + get => showLayersPanel; + set + { + this.RaiseAndSetIfChanged(ref showLayersPanel, value); + preferencesFacade.Set(AppPreference.ShowLayersPanel, value); + messageBus.SendMessage(new ViewOptionsChangedMessage(ShowButtonLabels, value)); + } + } + // Facades for View/CodeBehind access public ObservableCollection Layers => LayerFacade.Layers; @@ -76,6 +117,7 @@ public MainViewModel( NavigationModel navigationModel, SelectionObserver selectionObserver, IMessageBus messageBus, + IPreferencesFacade preferencesFacade, LayerPanelViewModel layerPanelVM, SelectionViewModel selectionVM, HistoryViewModel historyVM) @@ -86,15 +128,32 @@ public MainViewModel( NavigationModel = navigationModel; SelectionObserver = selectionObserver; this.messageBus = messageBus; + this.preferencesFacade = preferencesFacade; LayerPanelVM = layerPanelVM; SelectionVM = selectionVM; HistoryVM = historyVM; + // Use Property setters to trigger ViewOptionsChangedMessage so ToolbarViewModel syncs up + ShowButtonLabels = this.preferencesFacade.Get(AppPreference.ShowButtonLabels); + ShowLayersPanel = this.preferencesFacade.Get(AppPreference.ShowLayersPanel); + var savedTheme = this.preferencesFacade.Get(AppPreference.AppTheme); + SelectedTheme = AvailableThemes.FirstOrDefault(t => t == savedTheme) ?? AvailableThemes[0]; + ZoomInCommand = ReactiveCommand.Create(ZoomIn); ZoomOutCommand = ReactiveCommand.Create(ZoomOut); ResetZoomCommand = ReactiveCommand.Create(ResetZoom); - } + // Listen for ShowAdvancedSettingsMessage + this.messageBus.Listen().Subscribe(async _ => + { + var popup = new Components.AdvancedSettingsPopup(this); + var page = Application.Current?.Windows[0]?.Page; + if (page != null) + { + await page.ShowPopupAsync(popup); + } + }); + } public IDrawingTool ActiveTool { @@ -149,6 +208,21 @@ public ToolContext CreateToolContext() }; } + private void UpdateAppTheme(string theme) + { + if (Application.Current != null) + { + Application.Current.UserAppTheme = theme switch + { + "Light" => AppTheme.Light, + "Dark" => AppTheme.Dark, + _ => AppTheme.Unspecified + }; + } + + messageBus.SendMessage(new CanvasInvalidateMessage()); + } + private void ZoomIn() => Zoom(1.2f); private void ZoomOut() => Zoom(1f / 1.2f); @@ -162,6 +236,13 @@ private void Zoom(float scaleFactor) { if (CanvasSize.Width <= 0 || CanvasSize.Height <= 0) return; + var currentScale = NavigationModel.ViewMatrix.ScaleX; + var newScale = currentScale * scaleFactor; + + // Clamp scale + if (newScale < 0.1f) scaleFactor = 0.1f / currentScale; + if (newScale > 20.0f) scaleFactor = 20.0f / currentScale; + var center = new SKPoint(CanvasSize.Width / 2, CanvasSize.Height / 2); // Scale around center diff --git a/Logic/ViewModels/ToolbarViewModel.cs b/Logic/ViewModels/ToolbarViewModel.cs index 54c8948..0301068 100644 --- a/Logic/ViewModels/ToolbarViewModel.cs +++ b/Logic/ViewModels/ToolbarViewModel.cs @@ -186,6 +186,7 @@ public virtual float HueJitter public ReactiveCommand SelectBrushShapeCommand { get; } public ReactiveCommand ImportImageCommand { get; } public ReactiveCommand SaveImageCommand { get; } + public ReactiveCommand ShowAdvancedSettingsCommand { get; } // UI state properties private bool isSettingsOpen = false; @@ -195,6 +196,13 @@ public bool IsSettingsOpen set => this.RaiseAndSetIfChanged(ref isSettingsOpen, value); } + private bool showButtonLabels = true; + public bool ShowButtonLabels + { + get => showButtonLabels; + set => this.RaiseAndSetIfChanged(ref showButtonLabels, value); + } + private bool isShapesFlyoutOpen = false; public bool IsShapesFlyoutOpen { @@ -226,7 +234,8 @@ public ToolbarViewModel( IMessageBus messageBus, IBitmapCache bitmapCacheManager, NavigationModel navigationModel, - IFileSaver fileSaver) + IFileSaver fileSaver, + IPreferencesFacade preferencesFacade) { this.layerFacade = layerFacade; this.selectionVM = selectionVM; @@ -236,6 +245,12 @@ public ToolbarViewModel( this.navigationModel = navigationModel; this.fileSaver = fileSaver; + // Listen for ViewOptions changes + this.messageBus.Listen().Subscribe(msg => + { + ShowButtonLabels = msg.ShowButtonLabels; + }); + // Initialize Tools and Shapes AvailableTools = [ @@ -245,7 +260,7 @@ public ToolbarViewModel( new EllipseTool(messageBus), new LineTool(messageBus), new FillTool(messageBus), - new EraserBrushTool(messageBus) + new EraserBrushTool(messageBus, preferencesFacade) ]; AvailableBrushShapes = @@ -366,7 +381,7 @@ public ToolbarViewModel( SelectBrushShapeCommand = ReactiveCommand.Create(shape => { - this.messageBus.SendMessage(new LunaDraw.Logic.Messages.BrushShapeChangedMessage(shape)); + this.messageBus.SendMessage(new BrushShapeChangedMessage(shape)); IsBrushesFlyoutOpen = false; var freehandTool = AvailableTools.FirstOrDefault(t => t.Type == ToolType.Freehand); @@ -383,6 +398,11 @@ public ToolbarViewModel( IsBrushesFlyoutOpen = false; }); + ShowAdvancedSettingsCommand = ReactiveCommand.Create(() => + { + messageBus.SendMessage(new ShowAdvancedSettingsMessage()); + }); + SelectRectangleCommand = ReactiveCommand.Create(() => { var tool = AvailableTools.FirstOrDefault(t => t is RectangleTool) ?? new RectangleTool(messageBus); diff --git a/LunaDraw.csproj b/LunaDraw.csproj index e622d49..2c509b4 100644 --- a/LunaDraw.csproj +++ b/LunaDraw.csproj @@ -42,6 +42,7 @@ 10.0.17763.0 6.5 $(DefaultItemExcludes);Tests\** + true diff --git a/MauiProgram.cs b/MauiProgram.cs index e84f7dc..668729d 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -26,7 +26,6 @@ using Microsoft.Maui.LifecycleEvents; using LunaDraw.Logic.Utils; using LunaDraw.Logic.Models; -using LunaDraw.Logic.Services; using LunaDraw.Logic.ViewModels; using LunaDraw.Pages; using Microsoft.Extensions.Logging; @@ -67,8 +66,8 @@ public static MauiApp CreateMauiApp() { wndLifeCycleBuilder.OnWindowCreated(window => { - window.SystemBackdrop = new WinUI.TransparentBackdrop(); - if (Microsoft.Maui.Storage.Preferences.Get("IsTransparentBackgroundEnabled", false)) + window.SystemBackdrop = new DesktopAcrylicBackdrop(); + if (Preferences.Get(AppPreference.IsTransparentBackgroundEnabled.ToString(), false)) { PlatformHelper.EnableTrueTransparency(180); // Fully transparent } @@ -87,7 +86,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(FileSaver.Default); // Register ViewModels diff --git a/Pages/MainPage.xaml b/Pages/MainPage.xaml index 7832a39..a36039a 100644 --- a/Pages/MainPage.xaml +++ b/Pages/MainPage.xaml @@ -35,29 +35,29 @@ + BindingContext="{Binding ToolbarViewModel}"/> + BindingContext="{Binding ToolbarViewModel}"/> + BindingContext="{Binding ToolbarViewModel}"/> diff --git a/Pages/MainPage.xaml.cs b/Pages/MainPage.xaml.cs index 789b223..f6c154a 100644 --- a/Pages/MainPage.xaml.cs +++ b/Pages/MainPage.xaml.cs @@ -21,8 +21,11 @@ * */ +using CommunityToolkit.Maui; using LunaDraw.Logic.Messages; +using LunaDraw.Logic.Utils; using LunaDraw.Logic.ViewModels; +using LunaDraw.Logic.Extensions; using ReactiveUI; @@ -36,16 +39,17 @@ public partial class MainPage : ContentPage private readonly MainViewModel viewModel; private readonly ToolbarViewModel toolbarViewModel; private readonly IMessageBus messageBus; - + private readonly IPreferencesFacade preferencesFacade; private MenuFlyout? canvasContextMenu; private MenuFlyoutSubItem? moveToLayerSubMenu; - public MainPage(MainViewModel viewModel, ToolbarViewModel toolbarViewModel, IMessageBus messageBus) + public MainPage(MainViewModel viewModel, ToolbarViewModel toolbarViewModel, IMessageBus messageBus, IPreferencesFacade preferencesFacade) { InitializeComponent(); this.viewModel = viewModel; this.toolbarViewModel = toolbarViewModel; this.messageBus = messageBus; + this.preferencesFacade = preferencesFacade; BindingContext = this.viewModel; toolbarView.BindingContext = this.toolbarViewModel; @@ -153,7 +157,7 @@ private void OnCanvasViewPaintSurface(object? sender, SKPaintGLSurfaceEventArgs viewModel.NavigationModel.CanvasWidth = width; viewModel.NavigationModel.CanvasHeight = height; - var bgColor = viewModel.LayerPanelVM.IsTransparentBackground ? SKColors.Transparent : SKColors.White; + var bgColor = preferencesFacade.GetCanvasBackgroundColor(); canvas.Clear(bgColor); if (viewModel == null) return; @@ -190,7 +194,7 @@ private void OnCanvasViewPaintSurface(object? sender, SKPaintGLSurfaceEventArgs canvas.SaveLayer(); layer.Draw(canvas); - using (var paint = new SKPaint { BlendMode = SKBlendMode.SrcATop }) + using (var paint = new SKPaint { BlendMode = SKBlendMode.SrcATop, IsAntialias = true }) { for (int j = i + 1; j < layers.Count; j++) { diff --git a/Platforms/Windows/PlatformHelper.cs.cs b/Platforms/Windows/PlatformHelper.cs similarity index 100% rename from Platforms/Windows/PlatformHelper.cs.cs rename to Platforms/Windows/PlatformHelper.cs diff --git a/README.md b/README.md index 57d4d6e..10569dc 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,15 @@ LunaDraw is a child-centric drawing application designed for children aged 3–8. It provides a safe, ad-free, and magical environment for creativity, featuring special effects brushes, easy-to-use tools, and a "Movie Mode" that replays the drawing process. > ### **Missing Features / Parity / Testing / Deployment** +> > This application is a work in-progress and has several missing features yet to be implemented. Only Windows has been tested on one device. No mobile testing has been done. As the application is new and the holidays draw closer, it may be another month or two before app store submission. -> +> > The running list can be found below: > > - [Missing Features](Documentation/MissingFeatures.md) - > ### **Vibe-Coded** +> > The application is heavily vibe-coded and guided by best practices in the [Cline Rules](.clinerules/.clinerules.md) using [SPARC Agentic Development](https://gist.github.com/ruvnet/7d4e1d5c9233ab0a1d2a66bf5ec3e58f); mostly using [Gemini 3](https://gemini.google.com/). Hallucinations are always possible. It is recognized and should be by all that AI is still sometimes as helpful as a sack of hair. > > There are some very poor implementations by the AI such as string names of commands. @@ -98,4 +99,4 @@ For more detailed information, please refer to the `Documentation` directory: ## Deployment 1. Build the project using Visual Studio or VS Code. -2. Deploy the generated application on the respective platform (Windows, Android, iOS, MacCatalyst). \ No newline at end of file +2. Deploy the generated application on the respective platform (Windows, Android, iOS, MacCatalyst). diff --git a/Resources/Strings/AppResources.Designer.cs b/Resources/Strings/AppResources.Designer.cs new file mode 100644 index 0000000..eefb608 --- /dev/null +++ b/Resources/Strings/AppResources.Designer.cs @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2025 CodeSoupCafe LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace LunaDraw.Resources.Strings { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AppResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AppResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LunaDraw.Resources.Strings.AppResources", typeof(AppResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string Layers_Title { + get { + return ResourceManager.GetString("Layers_Title", resourceCulture); + } + } + + internal static string ToolTip_AddLayer { + get { + return ResourceManager.GetString("ToolTip_AddLayer", resourceCulture); + } + } + + internal static string ToolTip_RemoveLayer { + get { + return ResourceManager.GetString("ToolTip_RemoveLayer", resourceCulture); + } + } + + internal static string ToolTip_ToggleTraceMode { + get { + return ResourceManager.GetString("ToolTip_ToggleTraceMode", resourceCulture); + } + } + + internal static string ToolTip_CollapseLayers { + get { + return ResourceManager.GetString("ToolTip_CollapseLayers", resourceCulture); + } + } + + internal static string ToolTip_LayerVisibility { + get { + return ResourceManager.GetString("ToolTip_LayerVisibility", resourceCulture); + } + } + + internal static string ToolTip_LayerLock { + get { + return ResourceManager.GetString("ToolTip_LayerLock", resourceCulture); + } + } + + internal static string Label_Styles { + get { + return ResourceManager.GetString("Label_Styles", resourceCulture); + } + } + + internal static string Label_Shapes { + get { + return ResourceManager.GetString("Label_Shapes", resourceCulture); + } + } + + internal static string Label_Image { + get { + return ResourceManager.GetString("Label_Image", resourceCulture); + } + } + + internal static string Label_Save { + get { + return ResourceManager.GetString("Label_Save", resourceCulture); + } + } + + internal static string Label_Delete { + get { + return ResourceManager.GetString("Label_Delete", resourceCulture); + } + } + + internal static string Label_Undo { + get { + return ResourceManager.GetString("Label_Undo", resourceCulture); + } + } + + internal static string Label_Redo { + get { + return ResourceManager.GetString("Label_Redo", resourceCulture); + } + } + + internal static string ToolTip_SelectTool { + get { + return ResourceManager.GetString("ToolTip_SelectTool", resourceCulture); + } + } + + internal static string ToolTip_Styles { + get { + return ResourceManager.GetString("ToolTip_Styles", resourceCulture); + } + } + + internal static string ToolTip_BrushSettings { + get { + return ResourceManager.GetString("ToolTip_BrushSettings", resourceCulture); + } + } + + internal static string ToolTip_Shapes { + get { + return ResourceManager.GetString("ToolTip_Shapes", resourceCulture); + } + } + + internal static string ToolTip_ImportImage { + get { + return ResourceManager.GetString("ToolTip_ImportImage", resourceCulture); + } + } + + internal static string ToolTip_SaveImage { + get { + return ResourceManager.GetString("ToolTip_SaveImage", resourceCulture); + } + } + + internal static string ToolTip_Eraser { + get { + return ResourceManager.GetString("ToolTip_Eraser", resourceCulture); + } + } + + internal static string ToolTip_DeleteSelected { + get { + return ResourceManager.GetString("ToolTip_DeleteSelected", resourceCulture); + } + } + + internal static string ToolTip_Undo { + get { + return ResourceManager.GetString("ToolTip_Undo", resourceCulture); + } + } + + internal static string ToolTip_Redo { + get { + return ResourceManager.GetString("ToolTip_Redo", resourceCulture); + } + } + + internal static string Label_ViewOptions { + get { + return ResourceManager.GetString("Label_ViewOptions", resourceCulture); + } + } + + internal static string Label_ShowLayersPanel { + get { + return ResourceManager.GetString("Label_ShowLayersPanel", resourceCulture); + } + } + + internal static string Label_ShowButtonLabels { + get { + return ResourceManager.GetString("Label_ShowButtonLabels", resourceCulture); + } + } + + internal static string ToolTip_ViewOptions { + get { + return ResourceManager.GetString("ToolTip_ViewOptions", resourceCulture); + } + } + + internal static string Label_Close { + get { + return ResourceManager.GetString("Label_Close", resourceCulture); + } + } + } +} diff --git a/Resources/Strings/AppResources.resx b/Resources/Strings/AppResources.resx new file mode 100644 index 0000000..122d55a --- /dev/null +++ b/Resources/Strings/AppResources.resx @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Layers + + + Add Layer + + + Remove Layer + + + Toggle Trace Mode + + + Collapse Layers Panel + + + Toggle Visibility + + + Toggle Lock + + + Styles + + + Shapes + + + Image + + + Save + + + Delete + + + Undo + + + Redo + + + Select Tool + + + Style Settings + + + Brush Settings + + + Shapes + + + Import Image + + + Save Image + + + Eraser + + + Delete Selected + + + Undo + + + Redo + + + View Options + + + Show Layers Panel + + + Show Button Labels + + + View Options + + + Close + + \ No newline at end of file diff --git a/Resources/Styles/Styles.xaml b/Resources/Styles/Styles.xaml index fc9fa18..4613384 100644 --- a/Resources/Styles/Styles.xaml +++ b/Resources/Styles/Styles.xaml @@ -588,7 +588,6 @@ Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}"/> -