Этап 8

This commit is contained in:
2026-03-29 16:51:43 +03:00
parent 25e30416a3
commit c3961fcba7
5 changed files with 452 additions and 34 deletions

View File

@@ -1,19 +1,39 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Minint.Core.Models; namespace Minint.Core.Models;
/// <summary> /// <summary>
/// A single raster layer. Pixels are indices into the parent document's palette. /// A single raster layer. Pixels are indices into the parent document's palette.
/// Array layout is row-major: Pixels[y * width + x]. /// Array layout is row-major: Pixels[y * width + x].
/// </summary> /// </summary>
public sealed class MinintLayer public sealed class MinintLayer : INotifyPropertyChanged
{ {
public string Name { get; set; } private string _name;
public bool IsVisible { get; set; } private bool _isVisible;
private byte _opacity;
public string Name
{
get => _name;
set { if (_name != value) { _name = value; Notify(); } }
}
public bool IsVisible
{
get => _isVisible;
set { if (_isVisible != value) { _isVisible = value; Notify(); } }
}
/// <summary> /// <summary>
/// Per-layer opacity (0 = fully transparent, 255 = fully opaque). /// Per-layer opacity (0 = fully transparent, 255 = fully opaque).
/// Used during compositing: effective alpha = paletteColor.A * Opacity / 255. /// Used during compositing: effective alpha = paletteColor.A * Opacity / 255.
/// </summary> /// </summary>
public byte Opacity { get; set; } public byte Opacity
{
get => _opacity;
set { if (_opacity != value) { _opacity = value; Notify(); } }
}
/// <summary> /// <summary>
/// Palette indices, length must equal container Width * Height. /// Palette indices, length must equal container Width * Height.
@@ -23,9 +43,9 @@ public sealed class MinintLayer
public MinintLayer(string name, int pixelCount) public MinintLayer(string name, int pixelCount)
{ {
Name = name; _name = name;
IsVisible = true; _isVisible = true;
Opacity = 255; _opacity = 255;
Pixels = new int[pixelCount]; Pixels = new int[pixelCount];
} }
@@ -34,9 +54,13 @@ public sealed class MinintLayer
/// </summary> /// </summary>
public MinintLayer(string name, bool isVisible, byte opacity, int[] pixels) public MinintLayer(string name, bool isVisible, byte opacity, int[] pixels)
{ {
Name = name; _name = name;
IsVisible = isVisible; _isVisible = isVisible;
Opacity = opacity; _opacity = opacity;
Pixels = pixels; Pixels = pixels;
} }
public event PropertyChangedEventHandler? PropertyChanged;
private void Notify([CallerMemberName] string? prop = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
} }

View File

@@ -0,0 +1,49 @@
using System;
using Minint.Core.Models;
namespace Minint.Core.Services.Impl;
public sealed class FragmentService : IFragmentService
{
public void CopyFragment(
MinintDocument srcDoc, int srcLayerIndex,
int srcX, int srcY, int regionWidth, int regionHeight,
MinintDocument dstDoc, int dstLayerIndex,
int dstX, int dstY,
int containerWidth, int containerHeight)
{
ArgumentOutOfRangeException.ThrowIfNegative(srcLayerIndex);
ArgumentOutOfRangeException.ThrowIfNegative(dstLayerIndex);
if (srcLayerIndex >= srcDoc.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(srcLayerIndex));
if (dstLayerIndex >= dstDoc.Layers.Count)
throw new ArgumentOutOfRangeException(nameof(dstLayerIndex));
var srcLayer = srcDoc.Layers[srcLayerIndex];
var dstLayer = dstDoc.Layers[dstLayerIndex];
int clippedSrcX = Math.Max(srcX, 0);
int clippedSrcY = Math.Max(srcY, 0);
int clippedEndX = Math.Min(srcX + regionWidth, containerWidth);
int clippedEndY = Math.Min(srcY + regionHeight, containerHeight);
for (int sy = clippedSrcY; sy < clippedEndY; sy++)
{
int dy = dstY + (sy - srcY);
if (dy < 0 || dy >= containerHeight) continue;
for (int sx = clippedSrcX; sx < clippedEndX; sx++)
{
int dx = dstX + (sx - srcX);
if (dx < 0 || dx >= containerWidth) continue;
int srcIdx = srcLayer.Pixels[sy * containerWidth + sx];
if (srcIdx == 0) continue; // skip transparent
RgbaColor color = srcDoc.Palette[srcIdx];
int dstIdx = dstDoc.EnsureColorCached(color);
dstLayer.Pixels[dy * containerWidth + dx] = dstIdx;
}
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
namespace Minint.Controls;
/// <summary>
/// Shows a TextBlock by default; switches to an inline TextBox on double-click.
/// Commits on Enter or focus loss, cancels on Escape.
/// </summary>
public class EditableTextBlock : Control
{
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<EditableTextBlock, string>(nameof(Text), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
public string Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
private readonly TextBlock _display;
private readonly TextBox _editor;
private bool _isEditing;
public EditableTextBlock()
{
_display = new TextBlock
{
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
};
_editor = new TextBox
{
VerticalAlignment = VerticalAlignment.Center,
Padding = new Thickness(2, 0),
BorderThickness = new Thickness(1),
MinWidth = 40,
IsVisible = false,
};
LogicalChildren.Add(_display);
LogicalChildren.Add(_editor);
VisualChildren.Add(_display);
VisualChildren.Add(_editor);
_display.Bind(TextBlock.TextProperty, this.GetObservable(TextProperty).ToBinding());
_editor.Bind(TextBox.TextProperty, this.GetObservable(TextProperty).ToBinding());
_editor.KeyDown += OnEditorKeyDown;
_editor.LostFocus += OnEditorLostFocus;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (e.ClickCount == 2 && !_isEditing)
{
BeginEdit();
e.Handled = true;
}
}
private void BeginEdit()
{
_isEditing = true;
_editor.Text = Text;
_display.IsVisible = false;
_editor.IsVisible = true;
_editor.Focus();
_editor.SelectAll();
}
private void CommitEdit()
{
if (!_isEditing) return;
_isEditing = false;
Text = _editor.Text ?? string.Empty;
_editor.IsVisible = false;
_display.IsVisible = true;
}
private void CancelEdit()
{
if (!_isEditing) return;
_isEditing = false;
_editor.Text = Text;
_editor.IsVisible = false;
_display.IsVisible = true;
}
private void OnEditorKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
CommitEdit();
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
CancelEdit();
e.Handled = true;
}
}
private void OnEditorLostFocus(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
CommitEdit();
}
protected override Size MeasureOverride(Size availableSize)
{
_display.Measure(availableSize);
_editor.Measure(availableSize);
return _isEditing ? _editor.DesiredSize : _display.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var rect = new Rect(finalSize);
_display.Arrange(rect);
_editor.Arrange(rect);
return finalSize;
}
}

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -17,6 +19,7 @@ public partial class EditorViewModel : ViewModelBase
private readonly IPaletteService _paletteService = new PaletteService(); private readonly IPaletteService _paletteService = new PaletteService();
private readonly IDrawingService _drawingService = new DrawingService(); private readonly IDrawingService _drawingService = new DrawingService();
private readonly IFloodFillService _floodFillService = new FloodFillService(); private readonly IFloodFillService _floodFillService = new FloodFillService();
private readonly IFragmentService _fragmentService = new FragmentService();
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasContainer))] [NotifyPropertyChangedFor(nameof(HasContainer))]
@@ -44,17 +47,11 @@ public partial class EditorViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private int _brushRadius = 1; private int _brushRadius = 1;
/// <summary>
/// Pixel coordinates of current brush/eraser preview center, or null if cursor is outside image.
/// </summary>
[ObservableProperty] [ObservableProperty]
private (int X, int Y)? _previewCenter; private (int X, int Y)? _previewCenter;
private Avalonia.Media.Color _previewColor = Avalonia.Media.Color.FromArgb(255, 0, 0, 0); private Avalonia.Media.Color _previewColor = Avalonia.Media.Color.FromArgb(255, 0, 0, 0);
/// <summary>
/// Avalonia Color bound two-way to the ColorPicker in the toolbar.
/// </summary>
public Avalonia.Media.Color PreviewColor public Avalonia.Media.Color PreviewColor
{ {
get => _previewColor; get => _previewColor;
@@ -98,10 +95,7 @@ public partial class EditorViewModel : ViewModelBase
Container = container; Container = container;
FilePath = path; FilePath = path;
Documents.Clear(); SyncDocumentsList();
foreach (var doc in container.Documents)
Documents.Add(doc);
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null); SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
} }
@@ -119,8 +113,17 @@ public partial class EditorViewModel : ViewModelBase
SyncLayersAndCanvas(doc); SyncLayersAndCanvas(doc);
} }
private void SyncDocumentsList()
{
Documents.Clear();
if (Container is null) return;
foreach (var doc in Container.Documents)
Documents.Add(doc);
}
private void SyncLayersAndCanvas(MinintDocument? doc) private void SyncLayersAndCanvas(MinintDocument? doc)
{ {
UnsubscribeLayerVisibility();
Layers.Clear(); Layers.Clear();
if (doc is not null) if (doc is not null)
{ {
@@ -132,6 +135,178 @@ public partial class EditorViewModel : ViewModelBase
{ {
ActiveLayer = null; ActiveLayer = null;
} }
SubscribeLayerVisibility();
RefreshCanvas();
}
#endregion
#region Layer visibility change tracking
private void SubscribeLayerVisibility()
{
foreach (var layer in Layers)
{
if (layer is INotifyPropertyChanged npc)
npc.PropertyChanged += OnLayerPropertyChanged;
}
}
private void UnsubscribeLayerVisibility()
{
foreach (var layer in Layers)
{
if (layer is INotifyPropertyChanged npc)
npc.PropertyChanged -= OnLayerPropertyChanged;
}
}
private void OnLayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(MinintLayer.IsVisible) or nameof(MinintLayer.Opacity))
RefreshCanvas();
}
#endregion
#region Document commands
[RelayCommand]
private void AddDocument()
{
if (Container is null) return;
int num = Container.Documents.Count + 1;
var doc = Container.AddNewDocument($"Document {num}");
Documents.Add(doc);
SelectDocument(doc);
}
[RelayCommand]
private void RemoveDocument()
{
if (Container is null || ActiveDocument is null) return;
if (Container.Documents.Count <= 1) return; // keep at least one
var doc = ActiveDocument;
int idx = Container.Documents.IndexOf(doc);
Container.Documents.Remove(doc);
Documents.Remove(doc);
int newIdx = Math.Min(idx, Container.Documents.Count - 1);
SelectDocument(newIdx >= 0 ? Container.Documents[newIdx] : null);
}
[RelayCommand]
private void RenameDocument()
{
// Triggered via UI text edit — the Name property is directly editable via TextBox
}
[RelayCommand]
private void MoveDocumentUp()
{
if (Container is null || ActiveDocument is null) return;
int idx = Container.Documents.IndexOf(ActiveDocument);
if (idx <= 0) return;
(Container.Documents[idx], Container.Documents[idx - 1]) = (Container.Documents[idx - 1], Container.Documents[idx]);
SyncDocumentsList();
_suppressDocumentSync = true;
ActiveDocument = Container.Documents[idx - 1];
_suppressDocumentSync = false;
}
[RelayCommand]
private void MoveDocumentDown()
{
if (Container is null || ActiveDocument is null) return;
int idx = Container.Documents.IndexOf(ActiveDocument);
if (idx < 0 || idx >= Container.Documents.Count - 1) return;
(Container.Documents[idx], Container.Documents[idx + 1]) = (Container.Documents[idx + 1], Container.Documents[idx]);
SyncDocumentsList();
_suppressDocumentSync = true;
ActiveDocument = Container.Documents[idx + 1];
_suppressDocumentSync = false;
}
#endregion
#region Layer commands
[RelayCommand]
private void AddLayer()
{
if (Container is null || ActiveDocument is null) return;
int num = ActiveDocument.Layers.Count + 1;
var layer = new MinintLayer($"Layer {num}", Container.PixelCount);
ActiveDocument.Layers.Add(layer);
Layers.Add(layer);
ActiveLayer = layer;
SubscribeLayerVisibility();
}
[RelayCommand]
private void RemoveLayer()
{
if (ActiveDocument is null || ActiveLayer is null) return;
if (ActiveDocument.Layers.Count <= 1) return; // keep at least one
UnsubscribeLayerVisibility();
var layer = ActiveLayer;
int idx = ActiveDocument.Layers.IndexOf(layer);
ActiveDocument.Layers.Remove(layer);
Layers.Remove(layer);
int newIdx = Math.Min(idx, ActiveDocument.Layers.Count - 1);
ActiveLayer = newIdx >= 0 ? ActiveDocument.Layers[newIdx] : null;
SubscribeLayerVisibility();
RefreshCanvas();
}
[RelayCommand]
private void MoveLayerUp()
{
if (ActiveDocument is null || ActiveLayer is null) return;
int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
if (idx <= 0) return;
UnsubscribeLayerVisibility();
var layer = ActiveLayer;
(ActiveDocument.Layers[idx], ActiveDocument.Layers[idx - 1]) = (ActiveDocument.Layers[idx - 1], ActiveDocument.Layers[idx]);
Layers.Move(idx, idx - 1);
ActiveLayer = layer;
SubscribeLayerVisibility();
RefreshCanvas();
}
[RelayCommand]
private void MoveLayerDown()
{
if (ActiveDocument is null || ActiveLayer is null) return;
int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
if (idx < 0 || idx >= ActiveDocument.Layers.Count - 1) return;
UnsubscribeLayerVisibility();
var layer = ActiveLayer;
(ActiveDocument.Layers[idx], ActiveDocument.Layers[idx + 1]) = (ActiveDocument.Layers[idx + 1], ActiveDocument.Layers[idx]);
Layers.Move(idx, idx + 1);
ActiveLayer = layer;
SubscribeLayerVisibility();
RefreshCanvas();
}
[RelayCommand]
private void DuplicateLayer()
{
if (Container is null || ActiveDocument is null || ActiveLayer is null) return;
var src = ActiveLayer;
var dup = new MinintLayer(src.Name + " copy", src.IsVisible, src.Opacity, (int[])src.Pixels.Clone());
int idx = ActiveDocument.Layers.IndexOf(src) + 1;
ActiveDocument.Layers.Insert(idx, dup);
UnsubscribeLayerVisibility();
Layers.Insert(idx, dup);
ActiveLayer = dup;
SubscribeLayerVisibility();
RefreshCanvas(); RefreshCanvas();
} }
@@ -139,9 +314,6 @@ public partial class EditorViewModel : ViewModelBase
#region Drawing #region Drawing
/// <summary>
/// Called by PixelCanvas on left-click at the given image pixel coordinate.
/// </summary>
public void OnToolDown(int px, int py) public void OnToolDown(int px, int py)
{ {
if (Container is null || ActiveDocument is null || ActiveLayer is null) if (Container is null || ActiveDocument is null || ActiveLayer is null)
@@ -173,19 +345,12 @@ public partial class EditorViewModel : ViewModelBase
RefreshCanvas(); RefreshCanvas();
} }
/// <summary>
/// Called by PixelCanvas on left-drag at the given image pixel coordinate.
/// Same as OnToolDown for brush/eraser, no-op for fill.
/// </summary>
public void OnToolDrag(int px, int py) public void OnToolDrag(int px, int py)
{ {
if (ActiveTool == ToolType.Fill) return; if (ActiveTool == ToolType.Fill) return;
OnToolDown(px, py); OnToolDown(px, py);
} }
/// <summary>
/// Returns brush mask pixels for tool preview overlay.
/// </summary>
public List<(int X, int Y)>? GetPreviewMask() public List<(int X, int Y)>? GetPreviewMask()
{ {
if (PreviewCenter is null || Container is null) if (PreviewCenter is null || Container is null)
@@ -208,6 +373,28 @@ public partial class EditorViewModel : ViewModelBase
#endregion #endregion
#region Fragment copy (A4)
/// <summary>
/// Copies a rectangular fragment from <paramref name="srcDoc"/> active layer
/// to <paramref name="dstDoc"/> active layer.
/// </summary>
public void CopyFragment(
MinintDocument srcDoc, int srcLayerIndex,
int srcX, int srcY, int regionW, int regionH,
MinintDocument dstDoc, int dstLayerIndex,
int dstX, int dstY)
{
if (Container is null) return;
_fragmentService.CopyFragment(
srcDoc, srcLayerIndex, srcX, srcY, regionW, regionH,
dstDoc, dstLayerIndex, dstX, dstY,
Container.Width, Container.Height);
RefreshCanvas();
}
#endregion
#region Canvas rendering #region Canvas rendering
public void RefreshCanvas() public void RefreshCanvas()
@@ -267,4 +454,5 @@ public partial class EditorViewModel : ViewModelBase
public ICompositor Compositor => _compositor; public ICompositor Compositor => _compositor;
public IPaletteService PaletteService => _paletteService; public IPaletteService PaletteService => _paletteService;
public IDrawingService DrawingService => _drawingService; public IDrawingService DrawingService => _drawingService;
public IFragmentService FragmentService => _fragmentService;
} }

View File

@@ -37,12 +37,15 @@
BorderThickness="0,0,0,1" Padding="6,4"> BorderThickness="0,0,0,1" Padding="6,4">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
<RadioButton GroupName="Tool" Content="Brush" <RadioButton GroupName="Tool" Content="Brush"
ToolTip.Tip="Brush tool — draw with selected color"
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsBrush}}" IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsBrush}}"
Command="{Binding Editor.SelectBrushCommand}"/> Command="{Binding Editor.SelectBrushCommand}"/>
<RadioButton GroupName="Tool" Content="Eraser" <RadioButton GroupName="Tool" Content="Eraser"
ToolTip.Tip="Eraser tool — erase to transparent"
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsEraser}}" IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsEraser}}"
Command="{Binding Editor.SelectEraserCommand}"/> Command="{Binding Editor.SelectEraserCommand}"/>
<RadioButton GroupName="Tool" Content="Fill" <RadioButton GroupName="Tool" Content="Fill"
ToolTip.Tip="Fill tool — flood fill with selected color"
IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsFill}}" IsChecked="{Binding Editor.ActiveTool, Converter={x:Static vm:ToolTypeConverters.IsFill}}"
Command="{Binding Editor.SelectFillCommand}"/> Command="{Binding Editor.SelectFillCommand}"/>
@@ -72,19 +75,29 @@
</Border> </Border>
<!-- Main content: left panel, canvas, right panel --> <!-- Main content: left panel, canvas, right panel -->
<Grid ColumnDefinitions="180,*,180"> <Grid ColumnDefinitions="200,*,200">
<!-- Left panel: documents --> <!-- Left panel: documents -->
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" <Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="0,0,1,0" Padding="4"> BorderThickness="0,0,1,0" Padding="4">
<DockPanel> <DockPanel>
<TextBlock DockPanel.Dock="Top" Text="Documents" FontWeight="SemiBold" Margin="0,0,0,4"/> <TextBlock DockPanel.Dock="Top" Text="Documents" FontWeight="SemiBold" Margin="0,0,0,4"/>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="2" Margin="0,4,0,0">
<Button Content="+" ToolTip.Tip="Add document"
Command="{Binding Editor.AddDocumentCommand}" Padding="6,2"/>
<Button Content="" ToolTip.Tip="Remove document"
Command="{Binding Editor.RemoveDocumentCommand}" Padding="6,2"/>
<Button Content="▲" ToolTip.Tip="Move up"
Command="{Binding Editor.MoveDocumentUpCommand}" Padding="6,2"/>
<Button Content="▼" ToolTip.Tip="Move down"
Command="{Binding Editor.MoveDocumentDownCommand}" Padding="6,2"/>
</StackPanel>
<ListBox ItemsSource="{Binding Editor.Documents}" <ListBox ItemsSource="{Binding Editor.Documents}"
SelectedItem="{Binding Editor.ActiveDocument}" SelectedItem="{Binding Editor.ActiveDocument}"
SelectionMode="Single"> SelectionMode="Single">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding Name}"/> <controls:EditableTextBlock Text="{Binding Name}"/>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
@@ -109,14 +122,29 @@
BorderThickness="1,0,0,0" Padding="4"> BorderThickness="1,0,0,0" Padding="4">
<DockPanel> <DockPanel>
<TextBlock DockPanel.Dock="Top" Text="Layers" FontWeight="SemiBold" Margin="0,0,0,4"/> <TextBlock DockPanel.Dock="Top" Text="Layers" FontWeight="SemiBold" Margin="0,0,0,4"/>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="2" Margin="0,4,0,0">
<Button Content="+" ToolTip.Tip="Add layer"
Command="{Binding Editor.AddLayerCommand}" Padding="6,2"/>
<Button Content="" ToolTip.Tip="Remove layer"
Command="{Binding Editor.RemoveLayerCommand}" Padding="6,2"/>
<Button Content="▲" ToolTip.Tip="Move up"
Command="{Binding Editor.MoveLayerUpCommand}" Padding="6,2"/>
<Button Content="▼" ToolTip.Tip="Move down"
Command="{Binding Editor.MoveLayerDownCommand}" Padding="6,2"/>
<Button Content="⧉" ToolTip.Tip="Duplicate layer"
Command="{Binding Editor.DuplicateLayerCommand}" Padding="6,2"/>
</StackPanel>
<ListBox ItemsSource="{Binding Editor.Layers}" <ListBox ItemsSource="{Binding Editor.Layers}"
SelectedItem="{Binding Editor.ActiveLayer}" SelectedItem="{Binding Editor.ActiveLayer}"
SelectionMode="Single"> SelectionMode="Single">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<CheckBox IsChecked="{Binding IsVisible}" MinWidth="0"/> <CheckBox IsChecked="{Binding IsVisible}" MinWidth="0"
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/> VerticalAlignment="Center"
ToolTip.Tip="Toggle layer visibility"/>
<controls:EditableTextBlock Text="{Binding Name}"
VerticalAlignment="Center" MinWidth="60"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>