Этап 5
This commit is contained in:
@@ -21,6 +21,13 @@ public sealed class MinintDocument
|
||||
|
||||
public List<MinintLayer> Layers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reverse lookup cache: RgbaColor → palette index. Built lazily, invalidated
|
||||
/// on structural palette changes (compact, clear). Call <see cref="InvalidatePaletteCache"/>
|
||||
/// after bulk palette modifications.
|
||||
/// </summary>
|
||||
private Dictionary<RgbaColor, int>? _paletteCache;
|
||||
|
||||
public MinintDocument(string name)
|
||||
{
|
||||
Name = name;
|
||||
@@ -50,4 +57,48 @@ public sealed class MinintDocument
|
||||
<= 16_777_215 => 3,
|
||||
_ => 4
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// O(1) lookup of a color in the palette. Returns the index, or -1 if not found.
|
||||
/// Lazily builds an internal dictionary on first call.
|
||||
/// </summary>
|
||||
public int FindColorCached(RgbaColor color)
|
||||
{
|
||||
var cache = EnsurePaletteCache();
|
||||
return cache.TryGetValue(color, out int idx) ? idx : -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of <paramref name="color"/>. If absent, appends it to the palette
|
||||
/// and updates the cache. O(1) amortized.
|
||||
/// </summary>
|
||||
public int EnsureColorCached(RgbaColor color)
|
||||
{
|
||||
var cache = EnsurePaletteCache();
|
||||
if (cache.TryGetValue(color, out int idx))
|
||||
return idx;
|
||||
|
||||
idx = Palette.Count;
|
||||
Palette.Add(color);
|
||||
cache[color] = idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the reverse lookup cache. Must be called after any operation that
|
||||
/// reorders, removes, or bulk-replaces palette entries (e.g. compact, grayscale).
|
||||
/// </summary>
|
||||
public void InvalidatePaletteCache() => _paletteCache = null;
|
||||
|
||||
private Dictionary<RgbaColor, int> EnsurePaletteCache()
|
||||
{
|
||||
if (_paletteCache is not null)
|
||||
return _paletteCache;
|
||||
|
||||
var cache = new Dictionary<RgbaColor, int>(Palette.Count);
|
||||
for (int i = 0; i < Palette.Count; i++)
|
||||
cache.TryAdd(Palette[i], i); // first occurrence wins (for dupes)
|
||||
_paletteCache = cache;
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,10 @@ namespace Minint.Core.Services.Impl;
|
||||
public sealed class PaletteService : IPaletteService
|
||||
{
|
||||
public int FindColor(MinintDocument document, RgbaColor color)
|
||||
{
|
||||
var palette = document.Palette;
|
||||
for (int i = 0; i < palette.Count; i++)
|
||||
{
|
||||
if (palette[i] == color)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
=> document.FindColorCached(color);
|
||||
|
||||
public int EnsureColor(MinintDocument document, RgbaColor color)
|
||||
{
|
||||
int idx = FindColor(document, color);
|
||||
if (idx >= 0)
|
||||
return idx;
|
||||
|
||||
idx = document.Palette.Count;
|
||||
document.Palette.Add(color);
|
||||
return idx;
|
||||
}
|
||||
=> document.EnsureColorCached(color);
|
||||
|
||||
public void CompactPalette(MinintDocument document)
|
||||
{
|
||||
@@ -32,19 +16,16 @@ public sealed class PaletteService : IPaletteService
|
||||
if (palette.Count <= 1)
|
||||
return;
|
||||
|
||||
// 1. Collect indices actually used across all layers
|
||||
var usedIndices = new HashSet<int> { 0 }; // always keep transparent
|
||||
var usedIndices = new HashSet<int> { 0 };
|
||||
foreach (var layer in document.Layers)
|
||||
{
|
||||
foreach (int idx in layer.Pixels)
|
||||
usedIndices.Add(idx);
|
||||
}
|
||||
|
||||
// 2. Build new palette and old→new mapping
|
||||
var oldToNew = new int[palette.Count];
|
||||
var newPalette = new List<RgbaColor>(usedIndices.Count);
|
||||
|
||||
// Index 0 (transparent) stays at 0
|
||||
newPalette.Add(palette[0]);
|
||||
oldToNew[0] = 0;
|
||||
|
||||
@@ -55,23 +36,21 @@ public sealed class PaletteService : IPaletteService
|
||||
oldToNew[i] = newPalette.Count;
|
||||
newPalette.Add(palette[i]);
|
||||
}
|
||||
// unused indices don't get a mapping — they'll never be looked up
|
||||
}
|
||||
|
||||
// 3. If nothing was removed, skip the remap
|
||||
if (newPalette.Count == palette.Count)
|
||||
return;
|
||||
|
||||
// 4. Replace palette
|
||||
palette.Clear();
|
||||
palette.AddRange(newPalette);
|
||||
|
||||
// 5. Remap all pixel arrays
|
||||
foreach (var layer in document.Layers)
|
||||
{
|
||||
var px = layer.Pixels;
|
||||
for (int i = 0; i < px.Length; i++)
|
||||
px[i] = oldToNew[px[i]];
|
||||
}
|
||||
|
||||
document.InvalidatePaletteCache();
|
||||
}
|
||||
}
|
||||
|
||||
59
Minint/Controls/PixelCanvas.cs
Normal file
59
Minint/Controls/PixelCanvas.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace Minint.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Custom control that renders a WriteableBitmap with nearest-neighbor interpolation.
|
||||
/// Pan/zoom will be added in Stage 6.
|
||||
/// </summary>
|
||||
public class PixelCanvas : Control
|
||||
{
|
||||
public static readonly StyledProperty<WriteableBitmap?> SourceBitmapProperty =
|
||||
AvaloniaProperty.Register<PixelCanvas, WriteableBitmap?>(nameof(SourceBitmap));
|
||||
|
||||
public WriteableBitmap? SourceBitmap
|
||||
{
|
||||
get => GetValue(SourceBitmapProperty);
|
||||
set => SetValue(SourceBitmapProperty, value);
|
||||
}
|
||||
|
||||
static PixelCanvas()
|
||||
{
|
||||
AffectsRender<PixelCanvas>(SourceBitmapProperty);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
base.Render(context);
|
||||
|
||||
var bmp = SourceBitmap;
|
||||
if (bmp is null)
|
||||
return;
|
||||
|
||||
var srcSize = bmp.PixelSize;
|
||||
var bounds = Bounds;
|
||||
|
||||
// Fit image into control bounds preserving aspect ratio, centered
|
||||
double scaleX = bounds.Width / srcSize.Width;
|
||||
double scaleY = bounds.Height / srcSize.Height;
|
||||
double scale = Math.Min(scaleX, scaleY);
|
||||
if (scale < 1) scale = Math.Max(1, Math.Floor(scale));
|
||||
else scale = Math.Max(1, Math.Floor(scale));
|
||||
|
||||
double dstW = srcSize.Width * scale;
|
||||
double dstH = srcSize.Height * scale;
|
||||
double offsetX = (bounds.Width - dstW) / 2;
|
||||
double offsetY = (bounds.Height - dstH) / 2;
|
||||
|
||||
var destRect = new Rect(offsetX, offsetY, dstW, dstH);
|
||||
var srcRect = new Rect(0, 0, srcSize.Width, srcSize.Height);
|
||||
|
||||
// Nearest-neighbor for pixel-perfect rendering
|
||||
RenderOptions.SetBitmapInterpolationMode(this, BitmapInterpolationMode.None);
|
||||
context.DrawImage(bmp, srcRect, destRect);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
|
||||
160
Minint/ViewModels/EditorViewModel.cs
Normal file
160
Minint/ViewModels/EditorViewModel.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Minint.Core.Models;
|
||||
using Minint.Core.Services;
|
||||
using Minint.Core.Services.Impl;
|
||||
|
||||
namespace Minint.ViewModels;
|
||||
|
||||
public partial class EditorViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ICompositor _compositor = new Compositor();
|
||||
private readonly IPaletteService _paletteService = new PaletteService();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private MinintContainer? _container;
|
||||
|
||||
[ObservableProperty]
|
||||
private MinintDocument? _activeDocument;
|
||||
|
||||
[ObservableProperty]
|
||||
private MinintLayer? _activeLayer;
|
||||
|
||||
private bool _suppressDocumentSync;
|
||||
|
||||
[ObservableProperty]
|
||||
private WriteableBitmap? _canvasBitmap;
|
||||
|
||||
/// <summary>
|
||||
/// Path of the currently open file, or null for unsaved new containers.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(Title))]
|
||||
private string? _filePath;
|
||||
|
||||
public bool HasContainer => Container is not null;
|
||||
|
||||
public string Title => FilePath is not null
|
||||
? $"Minint — {System.IO.Path.GetFileName(FilePath)}"
|
||||
: Container is not null
|
||||
? "Minint — Untitled"
|
||||
: "Minint";
|
||||
|
||||
public ObservableCollection<MinintDocument> Documents { get; } = [];
|
||||
public ObservableCollection<MinintLayer> Layers { get; } = [];
|
||||
|
||||
public void NewContainer(int width, int height)
|
||||
{
|
||||
var c = new MinintContainer(width, height);
|
||||
c.AddNewDocument("Document 1");
|
||||
LoadContainer(c, null);
|
||||
}
|
||||
|
||||
public void LoadContainer(MinintContainer container, string? path)
|
||||
{
|
||||
Container = container;
|
||||
FilePath = path;
|
||||
|
||||
Documents.Clear();
|
||||
foreach (var doc in container.Documents)
|
||||
Documents.Add(doc);
|
||||
|
||||
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by CommunityToolkit when ActiveDocument property changes (e.g. from ListBox binding).
|
||||
/// </summary>
|
||||
partial void OnActiveDocumentChanged(MinintDocument? value)
|
||||
{
|
||||
if (_suppressDocumentSync) return;
|
||||
SyncLayersAndCanvas(value);
|
||||
}
|
||||
|
||||
public void SelectDocument(MinintDocument? doc)
|
||||
{
|
||||
_suppressDocumentSync = true;
|
||||
ActiveDocument = doc;
|
||||
_suppressDocumentSync = false;
|
||||
|
||||
SyncLayersAndCanvas(doc);
|
||||
}
|
||||
|
||||
private void SyncLayersAndCanvas(MinintDocument? doc)
|
||||
{
|
||||
Layers.Clear();
|
||||
if (doc is not null)
|
||||
{
|
||||
foreach (var layer in doc.Layers)
|
||||
Layers.Add(layer);
|
||||
ActiveLayer = doc.Layers.Count > 0 ? doc.Layers[0] : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveLayer = null;
|
||||
}
|
||||
|
||||
RefreshCanvas();
|
||||
}
|
||||
|
||||
public void RefreshCanvas()
|
||||
{
|
||||
if (Container is null || ActiveDocument is null)
|
||||
{
|
||||
CanvasBitmap = null;
|
||||
return;
|
||||
}
|
||||
|
||||
int w = Container.Width;
|
||||
int h = Container.Height;
|
||||
uint[] argb = _compositor.Composite(ActiveDocument, w, h);
|
||||
|
||||
var bmp = new WriteableBitmap(
|
||||
new PixelSize(w, h),
|
||||
new Vector(96, 96),
|
||||
Avalonia.Platform.PixelFormat.Bgra8888);
|
||||
|
||||
using (var fb = bmp.Lock())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var dst = new Span<uint>((void*)fb.Address, w * h);
|
||||
for (int i = 0; i < argb.Length; i++)
|
||||
{
|
||||
// argb[i] is 0xAARRGGBB, need premultiplied BGRA for the bitmap
|
||||
uint px = argb[i];
|
||||
byte a = (byte)(px >> 24);
|
||||
byte r = (byte)((px >> 16) & 0xFF);
|
||||
byte g = (byte)((px >> 8) & 0xFF);
|
||||
byte b = (byte)(px & 0xFF);
|
||||
|
||||
if (a == 255)
|
||||
{
|
||||
dst[i] = px; // ARGB layout == BGRA in LE memory, alpha=255 → no premul needed
|
||||
}
|
||||
else if (a == 0)
|
||||
{
|
||||
dst[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = (byte)(r * a / 255);
|
||||
g = (byte)(g * a / 255);
|
||||
b = (byte)(b * a / 255);
|
||||
dst[i] = (uint)(b | (g << 8) | (r << 16) | (a << 24));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CanvasBitmap = bmp;
|
||||
}
|
||||
|
||||
public ICompositor Compositor => _compositor;
|
||||
public IPaletteService PaletteService => _paletteService;
|
||||
}
|
||||
@@ -1,6 +1,124 @@
|
||||
namespace Minint.ViewModels;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Minint.Infrastructure.Serialization;
|
||||
|
||||
namespace Minint.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
public string Greeting { get; } = "Welcome to Avalonia!";
|
||||
private readonly MinintSerializer _serializer = new();
|
||||
|
||||
private static readonly FilePickerFileType MinintFileType = new("Minint Files")
|
||||
{
|
||||
Patterns = ["*.minint"],
|
||||
};
|
||||
|
||||
[ObservableProperty]
|
||||
private EditorViewModel _editor = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Ready";
|
||||
|
||||
/// <summary>
|
||||
/// Set by the view so that file dialogs can use the correct parent window.
|
||||
/// </summary>
|
||||
public TopLevel? Owner { get; set; }
|
||||
|
||||
[RelayCommand]
|
||||
private void NewFile()
|
||||
{
|
||||
Editor.NewContainer(64, 64);
|
||||
StatusText = "New 64×64 container created.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenFileAsync()
|
||||
{
|
||||
if (Owner?.StorageProvider is not { } sp) return;
|
||||
|
||||
var files = await sp.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Open .minint file",
|
||||
FileTypeFilter = [MinintFileType],
|
||||
AllowMultiple = false,
|
||||
});
|
||||
|
||||
if (files.Count == 0) return;
|
||||
|
||||
var file = files[0];
|
||||
try
|
||||
{
|
||||
await using var stream = await file.OpenReadAsync();
|
||||
var container = _serializer.Read(stream);
|
||||
var path = file.TryGetLocalPath();
|
||||
Editor.LoadContainer(container, path);
|
||||
StatusText = $"Opened {file.Name}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Error opening file: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveFileAsync()
|
||||
{
|
||||
if (Editor.Container is null) return;
|
||||
|
||||
if (Editor.FilePath is not null)
|
||||
{
|
||||
await SaveToPathAsync(Editor.FilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SaveFileAsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveFileAsAsync()
|
||||
{
|
||||
if (Owner?.StorageProvider is not { } sp || Editor.Container is null) return;
|
||||
|
||||
var file = await sp.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Save .minint file",
|
||||
DefaultExtension = "minint",
|
||||
FileTypeChoices = [MinintFileType],
|
||||
SuggestedFileName = Editor.FilePath is not null
|
||||
? Path.GetFileName(Editor.FilePath) : "untitled.minint",
|
||||
});
|
||||
|
||||
if (file is null) return;
|
||||
|
||||
var path = file.TryGetLocalPath();
|
||||
if (path is null)
|
||||
{
|
||||
StatusText = "Error: could not resolve file path.";
|
||||
return;
|
||||
}
|
||||
|
||||
await SaveToPathAsync(path);
|
||||
}
|
||||
|
||||
private async Task SaveToPathAsync(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var fs = File.Create(path);
|
||||
_serializer.Write(fs, Editor.Container!);
|
||||
Editor.FilePath = path;
|
||||
StatusText = $"Saved {Path.GetFileName(path)}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Error saving file: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,83 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:Minint.ViewModels"
|
||||
xmlns:controls="using:Minint.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
mc:Ignorable="d" d:DesignWidth="1024" d:DesignHeight="700"
|
||||
x:Class="Minint.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Icon="/Assets/avalonia-logo.ico"
|
||||
Title="Minint">
|
||||
Title="{Binding Editor.Title}"
|
||||
Width="1024" Height="700">
|
||||
|
||||
<Design.DataContext>
|
||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
||||
<vm:MainWindowViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<DockPanel>
|
||||
<!-- Menu bar -->
|
||||
<Menu DockPanel.Dock="Top">
|
||||
<MenuItem Header="_File">
|
||||
<MenuItem Header="_New" Command="{Binding NewFileCommand}" HotKey="Ctrl+N"/>
|
||||
<MenuItem Header="_Open…" Command="{Binding OpenFileCommand}" HotKey="Ctrl+O"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="_Save" Command="{Binding SaveFileCommand}" HotKey="Ctrl+S"/>
|
||||
<MenuItem Header="Save _As…" Command="{Binding SaveFileAsCommand}" HotKey="Ctrl+Shift+S"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<!-- Status bar -->
|
||||
<Border DockPanel.Dock="Bottom" Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||
Padding="8,2">
|
||||
<TextBlock Text="{Binding StatusText}" FontSize="12"/>
|
||||
</Border>
|
||||
|
||||
<!-- Main content: left panel, canvas, right panel -->
|
||||
<Grid ColumnDefinitions="180,*,180">
|
||||
|
||||
<!-- Left panel: documents -->
|
||||
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
||||
BorderThickness="0,0,1,0" Padding="4">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="Documents" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||
<ListBox ItemsSource="{Binding Editor.Documents}"
|
||||
SelectedItem="{Binding Editor.ActiveDocument}"
|
||||
SelectionMode="Single">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Center: canvas -->
|
||||
<Border Grid.Column="1" Background="#FF1E1E1E" ClipToBounds="True">
|
||||
<controls:PixelCanvas SourceBitmap="{Binding Editor.CanvasBitmap}"/>
|
||||
</Border>
|
||||
|
||||
<!-- Right panel: layers -->
|
||||
<Border Grid.Column="2" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
||||
BorderThickness="1,0,0,0" Padding="4">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="Layers" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||
<ListBox ItemsSource="{Binding Editor.Layers}"
|
||||
SelectedItem="{Binding Editor.ActiveLayer}"
|
||||
SelectionMode="Single">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<CheckBox IsChecked="{Binding IsVisible}" MinWidth="0"/>
|
||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Minint.ViewModels;
|
||||
|
||||
namespace Minint.Views;
|
||||
|
||||
@@ -8,4 +10,11 @@ public partial class MainWindow : Window
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.Owner = this;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user