diff --git a/src/Beutl.Utilities/PooledArrayBufferWriter.cs b/src/Beutl.Utilities/PooledArrayBufferWriter.cs index a86b1bd68..83afae7e7 100644 --- a/src/Beutl.Utilities/PooledArrayBufferWriter.cs +++ b/src/Beutl.Utilities/PooledArrayBufferWriter.cs @@ -105,6 +105,8 @@ public Span GetSpan(int sizeHint = 0) return _buffer.AsSpan(_index); } + public static T[] GetArray(PooledArrayBufferWriter self) => self._buffer; + private void CheckAndResizeBuffer(int sizeHint) { if (sizeHint < 0) diff --git a/src/Beutl/App.axaml.cs b/src/Beutl/App.axaml.cs index ba4eb29fb..42d519c3d 100644 --- a/src/Beutl/App.axaml.cs +++ b/src/Beutl/App.axaml.cs @@ -1,7 +1,9 @@ using System.Reflection; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.Platform; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; @@ -104,6 +106,19 @@ public override void OnFrameworkInitializationCompleted() base.OnFrameworkInitializationCompleted(); } + public static IClipboard? GetClipboard() + { + switch (Current?.ApplicationLifetime) + { + case IClassicDesktopStyleApplicationLifetime desktop: + return desktop.MainWindow?.Clipboard; + case ISingleViewApplicationLifetime { MainView: { } mainview }: + return TopLevel.GetTopLevel(mainview)?.Clipboard; + default: + return null; + } + } + private MainViewModel GetMainViewModel() { return _mainViewModel ??= new MainViewModel(); diff --git a/src/Beutl/Services/CoreObjectReborn.cs b/src/Beutl/Services/CoreObjectReborn.cs new file mode 100644 index 000000000..5b759fff1 --- /dev/null +++ b/src/Beutl/Services/CoreObjectReborn.cs @@ -0,0 +1,97 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +using Beutl.Utilities; + +namespace Beutl.Services; + +public static class CoreObjectReborn +{ + private const int DefaultGuidStringSize = 36; + private const int BufferSizeDefault = 16 * 1024; + + private static void RebornCore(T obj, PooledArrayBufferWriter output) + where T : class, ICoreObject, new() + { + var searcher = new ObjectSearcher(obj, v => v is ICoreObject); + + Guid[] ids = searcher.SearchAll() + .Cast() + .Select(v => v.Id) + .Distinct() + .ToArray(); + + // JsonObjectに変換 + var jsonObject = new JsonObject(); + obj.WriteToJson(jsonObject); + + // UTF-8に書き込む + JsonSerializerOptions options = JsonHelper.SerializerOptions; + var writerOptions = new JsonWriterOptions + { + Encoder = options.Encoder, + Indented = options.WriteIndented, + MaxDepth = options.MaxDepth + }; + + using (var writer = new Utf8JsonWriter(output, writerOptions)) + { + jsonObject.WriteTo(writer, options); + } + + // Idを置き換える + Span buffer = PooledArrayBufferWriter.GetArray(output).AsSpan().Slice(0, output.WrittenCount); + Span oldStr = stackalloc byte[DefaultGuidStringSize]; + Span newStr = stackalloc byte[DefaultGuidStringSize]; + foreach (Guid oldId in ids) + { + Guid newId = Guid.NewGuid(); + GuidToUtf8(oldId, oldStr); + GuidToUtf8(newId, newStr); + Span localBuffer = buffer; + + int index; + while ((index = localBuffer.IndexOf(oldStr)) >= 0) + { + localBuffer = localBuffer.Slice(index); + newStr.CopyTo(localBuffer); + } + } + } + + public static void Reborn(T obj, out T newInstance) + where T : class, ICoreObject, new() + { + using var output = new PooledArrayBufferWriter(BufferSizeDefault); + RebornCore(obj, output); + + Span buffer = PooledArrayBufferWriter.GetArray(output).AsSpan().Slice(0, output.WrittenCount); + + JsonObject jsonObj = JsonNode.Parse(buffer)!.AsObject(); + var instance = new T(); + instance.ReadFromJson(jsonObj); + + newInstance = instance; + } + + public static void Reborn(T obj, out string json) + where T : class, ICoreObject, new() + { + using var output = new PooledArrayBufferWriter(BufferSizeDefault); + RebornCore(obj, output); + + Span buffer = PooledArrayBufferWriter.GetArray(output).AsSpan().Slice(0, output.WrittenCount); + json = Encoding.UTF8.GetString(buffer); + } + + private static void GuidToUtf8(Guid id, Span utf8) + { + Span utf16 = stackalloc char[DefaultGuidStringSize]; + + if (!id.TryFormat(utf16, out _)) + throw new Exception("Failed to 'Guid.TryFormat'."); + + Encoding.UTF8.GetBytes(utf16, utf8); + } +} diff --git a/src/Beutl/ViewModels/ElementViewModel.cs b/src/Beutl/ViewModels/ElementViewModel.cs index 52370b68e..786844fac 100644 --- a/src/Beutl/ViewModels/ElementViewModel.cs +++ b/src/Beutl/ViewModels/ElementViewModel.cs @@ -8,6 +8,7 @@ using Beutl.Commands; using Beutl.Models; using Beutl.ProjectSystem; +using Beutl.Services; using Beutl.Utilities; using Reactive.Bindings; @@ -20,7 +21,6 @@ namespace Beutl.ViewModels; public sealed class ElementViewModel : IDisposable { private readonly CompositeDisposable _disposables = new(); - private IClipboard? _clipboard; public ElementViewModel(Element element, TimelineViewModel timeline) { @@ -163,11 +163,6 @@ public ElementViewModel(Element element, TimelineViewModel timeline) public List KeyBindings { get; } - public void SetClipboard(IClipboard? clipboard) - { - _clipboard = clipboard; - } - public void Dispose() { _disposables.Dispose(); @@ -239,7 +234,7 @@ public async Task SubmitViewModelChanges() private async ValueTask SetClipboard() { - IClipboard? clipboard = _clipboard; + IClipboard? clipboard = App.GetClipboard(); if (clipboard != null) { var jsonNode = new JsonObject(); @@ -323,11 +318,7 @@ private void OnSplit(TimeSpan timeSpan) TimeSpan forwardLength = absTime - Model.Start; TimeSpan backwardLength = Model.Length - forwardLength; - var jsonNode = new JsonObject(); - Model.WriteToJson(jsonNode); - string json = jsonNode.ToJsonString(JsonHelper.SerializerOptions); - var backwardLayer = new Element(); - backwardLayer.ReadFromJson(JsonNode.Parse(json)!.AsObject()); + CoreObjectReborn.Reborn(Model, out Element backwardLayer); Scene.MoveChild(Model.ZIndex, Model.Start, forwardLength, Model).DoAndRecord(CommandRecorder.Default); backwardLayer.Start = absTime; diff --git a/src/Beutl/Views/ElementView.axaml.cs b/src/Beutl/Views/ElementView.axaml.cs index 79051404c..3e6c8ab03 100644 --- a/src/Beutl/Views/ElementView.axaml.cs +++ b/src/Beutl/Views/ElementView.axaml.cs @@ -72,8 +72,6 @@ private void OnDataContextDetached(ElementViewModel obj) obj.AnimationRequested = (_, _) => Task.CompletedTask; _disposable1?.Dispose(); _disposable1 = null; - - obj.SetClipboard(null); } private void OnDataContextAttached(ElementViewModel obj) @@ -143,8 +141,6 @@ await Dispatcher.UIThread.InvokeAsync(async () => _disposable1 = obj.Model.GetObservable(Element.IsEnabledProperty) .Subscribe(b => Dispatcher.UIThread.InvokeAsync(() => border.Opacity = b ? 1 : 0.5)); - - obj.SetClipboard(TopLevel.GetTopLevel(this)?.Clipboard); } protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) diff --git a/src/Beutl/Views/Timeline.axaml.cs b/src/Beutl/Views/Timeline.axaml.cs index 67af4a15d..92ee29a3f 100644 --- a/src/Beutl/Views/Timeline.axaml.cs +++ b/src/Beutl/Views/Timeline.axaml.cs @@ -108,14 +108,16 @@ private void OnDataContextAttached(TimelineViewModel vm) string? json = await clipboard.GetTextAsync(); if (json != null) { - var layer = new Element(); - layer.ReadFromJson(JsonNode.Parse(json)!.AsObject()); - layer.Start = ViewModel.ClickedFrame; - layer.ZIndex = ViewModel.CalculateClickedLayer(); + var oldElement = new Element(); + oldElement.ReadFromJson(JsonNode.Parse(json)!.AsObject()); + CoreObjectReborn.Reborn(oldElement, out Element newElement); - layer.Save(RandomFileNameGenerator.Generate(Path.GetDirectoryName(ViewModel.Scene.FileName)!, Constants.ElementFileExtension)); + newElement.Start = ViewModel.ClickedFrame; + newElement.ZIndex = ViewModel.CalculateClickedLayer(); - ViewModel.Scene.AddChild(layer).DoAndRecord(CommandRecorder.Default); + newElement.Save(RandomFileNameGenerator.Generate(Path.GetDirectoryName(ViewModel.Scene.FileName)!, Constants.ElementFileExtension)); + + ViewModel.Scene.AddChild(newElement).DoAndRecord(CommandRecorder.Default); } } }