diff --git a/src/Beutl.Language/Strings.Designer.cs b/src/Beutl.Language/Strings.Designer.cs index 785654270..22624d4e7 100644 --- a/src/Beutl.Language/Strings.Designer.cs +++ b/src/Beutl.Language/Strings.Designer.cs @@ -2240,5 +2240,11 @@ public static string ClipTransparentArea { return ResourceManager.GetString("ClipTransparentArea", resourceCulture); } } + + public static string EnableElement { + get { + return ResourceManager.GetString("EnableElement", resourceCulture); + } + } } } diff --git a/src/Beutl.Language/Strings.ja.resx b/src/Beutl.Language/Strings.ja.resx index 6a5e4d608..e1d16f218 100644 --- a/src/Beutl.Language/Strings.ja.resx +++ b/src/Beutl.Language/Strings.ja.resx @@ -1216,4 +1216,7 @@ b-editorがダウンロードURLを管理します。 透明部分をクリップ + + 要素を有効化 + diff --git a/src/Beutl.Language/Strings.resx b/src/Beutl.Language/Strings.resx index 8b31f2e06..ee9d54de5 100644 --- a/src/Beutl.Language/Strings.resx +++ b/src/Beutl.Language/Strings.resx @@ -1216,4 +1216,7 @@ and b-editor maintains the download URL. Clip transparent area + + Enable element + diff --git a/src/Beutl/ViewModels/ElementViewModel.cs b/src/Beutl/ViewModels/ElementViewModel.cs index f629893d8..c3583cbde 100644 --- a/src/Beutl/ViewModels/ElementViewModel.cs +++ b/src/Beutl/ViewModels/ElementViewModel.cs @@ -121,11 +121,9 @@ public ElementViewModel(Element element, TimelineViewModel timeline) { LayerHeaderViewModel? newLH = Timeline.LayerHeaders.FirstOrDefault(i => i.Number.Value == number); - if (LayerHeader.Value != null) - LayerHeader.Value.ItemsCount.Value--; + LayerHeader.Value?.ElementRemoved(this); - if (newLH != null) - newLH.ItemsCount.Value++; + newLH?.ElementAdded(this); LayerHeader.Value = newLH; }) .AddTo(_disposables); diff --git a/src/Beutl/ViewModels/LayerHeaderViewModel.cs b/src/Beutl/ViewModels/LayerHeaderViewModel.cs index 96dfdeeb7..5303e8ca7 100644 --- a/src/Beutl/ViewModels/LayerHeaderViewModel.cs +++ b/src/Beutl/ViewModels/LayerHeaderViewModel.cs @@ -1,12 +1,9 @@ using System.Collections.Specialized; using System.Text.Json.Nodes; - using Avalonia.Media; - using Beutl.Commands; using Beutl.ProjectSystem; using Beutl.Reactive; - using Reactive.Bindings; using Reactive.Bindings.Extensions; @@ -15,6 +12,9 @@ namespace Beutl.ViewModels; public sealed class LayerHeaderViewModel : IDisposable, IJsonSerializable { private readonly CompositeDisposable _disposables = []; + private readonly List _elements = []; + private IDisposable? _elementsSubscription; + private bool _skipSubscription; public LayerHeaderViewModel(int num, TimelineViewModel timeline) { @@ -26,30 +26,40 @@ public LayerHeaderViewModel(int num, TimelineViewModel timeline) .ToReadOnlyReactivePropertySlim() .DisposeWith(_disposables); - IsEnabled.Skip(1).Subscribe(b => - { - CommandRecorder recorder = Timeline.EditorContext.CommandRecorder; - Timeline.Scene.Children.Where(i => i.ZIndex == Number.Value && i.IsEnabled != b) - .Select(item => RecordableCommands.Edit(item, Element.IsEnabledProperty, b).WithStoables([item])) - .ToArray() - .ToCommand() - .DoAndRecord(recorder); - }).DisposeWith(_disposables); + SwitchEnabledCommand = new ReactiveCommand() + .WithSubscribe(() => + { + CommandRecorder recorder = Timeline.EditorContext.CommandRecorder; + try + { + _skipSubscription = true; + IsEnabled.Value = !IsEnabled.Value; + Timeline.Scene.Children.Where(i => i.ZIndex == Number.Value && i.IsEnabled != IsEnabled.Value) + .Select(item => RecordableCommands.Edit(item, Element.IsEnabledProperty, IsEnabled.Value).WithStoables([item])) + .ToArray() + .ToCommand() + .DoAndRecord(recorder); + } + finally + { + _skipSubscription = false; + } + }); Height.Subscribe(_ => Timeline.RaiseLayerHeightChanged(this)).DisposeWith(_disposables); Inlines.ForEachItem( - (idx, x) => - { - Height.Value += FrameNumberHelper.LayerHeight; - x.Index.Value = idx; - }, - (_, x) => - { - Height.Value -= FrameNumberHelper.LayerHeight; - x.Index.Value = -1; - }, - () => { }) + (idx, x) => + { + Height.Value += FrameNumberHelper.LayerHeight; + x.Index.Value = idx; + }, + (_, x) => + { + Height.Value -= FrameNumberHelper.LayerHeight; + x.Index.Value = -1; + }, + () => { }) .DisposeWith(_disposables); Inlines.CollectionChangedAsObservable() @@ -118,6 +128,8 @@ void OnRemoved() public CoreList Inlines { get; } = new() { ResetBehavior = ResetBehavior.Remove }; + public ReactiveCommand SwitchEnabledCommand { get; } + public void AnimationRequest(int layerNum, bool affectModel = true) { if (affectModel) @@ -127,6 +139,37 @@ public void AnimationRequest(int layerNum, bool affectModel = true) PosY.Value = 0; } + public void ElementAdded(ElementViewModel element) + { + ItemsCount.Value++; + _elements.Add(element); + BuildSubscription(); + } + + public void ElementRemoved(ElementViewModel element) + { + ItemsCount.Value--; + _elements.Remove(element); + BuildSubscription(); + } + + private void BuildSubscription() + { + _elementsSubscription?.Dispose(); + _elementsSubscription = null; + if (_elements.Count == 0) + { + IsEnabled.Value = true; + return; + } + + _elementsSubscription = _elements.Select(obj => obj.IsEnabled.Select(b => (bool?)b)) + .Aggregate((x, y) => x.CombineLatest(y) + .Select(t => t.First == t.Second ? t.First : null)) + .Where(b => b.HasValue && !_skipSubscription) + .Subscribe(b => IsEnabled.Value = b!.Value); + } + public void Dispose() { _disposables.Dispose(); diff --git a/src/Beutl/Views/ElementView.axaml b/src/Beutl/Views/ElementView.axaml index 1f5efae57..050f85eee 100644 --- a/src/Beutl/Views/ElementView.axaml +++ b/src/Beutl/Views/ElementView.axaml @@ -19,7 +19,6 @@ x:DataType="vm:ElementViewModel" ClipToBounds="True" Focusable="True" - IsEnabled="{CompiledBinding IsEnabled.Value}" mc:Ignorable="d"> + + + @@ -38,6 +47,15 @@ + + + + + diff --git a/src/Beutl/Views/ElementView.axaml.cs b/src/Beutl/Views/ElementView.axaml.cs index a2ce807bd..98dba65c4 100644 --- a/src/Beutl/Views/ElementView.axaml.cs +++ b/src/Beutl/Views/ElementView.axaml.cs @@ -8,16 +8,12 @@ using Avalonia.Media; using Avalonia.Threading; using Avalonia.Xaml.Interactivity; - using Beutl.Commands; using Beutl.ProjectSystem; using Beutl.ViewModels; using Beutl.ViewModels.NodeTree; - using FluentAvalonia.UI.Controls; - using Reactive.Bindings.Extensions; - using Setter = Avalonia.Styling.Setter; namespace Beutl.Views; @@ -51,37 +47,35 @@ public ElementView() protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); - if (DataContext is ElementViewModel viewModel) + if (DataContext is not ElementViewModel viewModel) return; + + if (e.Key == Key.F2) { - if (e.Key == Key.F2) - { - Rename_Click(null, null!); - e.Handled = true; - return; - } - else if (e.Key == Key.LeftCtrl) - { - _resizeBehavior?.OnLeftCtrlPressed(e); - return; - } + Rename_Click(null, null!); + e.Handled = true; + return; + } + else if (e.Key == Key.LeftCtrl) + { + _resizeBehavior?.OnLeftCtrlPressed(e); + return; + } - // KeyBindingsは変更してはならない。 - foreach (KeyBinding binding in viewModel.KeyBindings) - { - if (e.Handled) - break; - binding.TryHandle(e); - } + // KeyBindingsは変更してはならない。 + foreach (KeyBinding binding in viewModel.KeyBindings) + { + if (e.Handled) + break; + binding.TryHandle(e); } } private void OnContextFlyoutOpening(object? sender, EventArgs e) { - if (DataContext is ElementViewModel viewModel) - { - change2OriginalLength.IsEnabled = viewModel.HasOriginalLength(); - splitByCurrent.IsEnabled = viewModel.Model.Range.Contains(viewModel.Timeline.EditorContext.CurrentTime.Value); - } + if (DataContext is not ElementViewModel viewModel) return; + + change2OriginalLength.IsEnabled = viewModel.HasOriginalLength(); + splitByCurrent.IsEnabled = viewModel.Model.Range.Contains(viewModel.Timeline.EditorContext.CurrentTime.Value); } private void OnDataContextDetached(ElementViewModel obj) @@ -97,58 +91,8 @@ private void OnDataContextAttached(ElementViewModel obj) { await Dispatcher.UIThread.InvokeAsync(async () => { - var animation1 = new Avalonia.Animation.Animation - { - Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0), - Duration = TimeSpan.FromSeconds(0.25), - FillMode = FillMode.Forward, - Children = - { - new KeyFrame() - { - Cue = new Cue(0), - Setters = - { - new Setter(MarginProperty, border.Margin), - new Setter(WidthProperty, border.Width), - } - }, - new KeyFrame() - { - Cue = new Cue(1), - Setters = - { - new Setter(MarginProperty, args.BorderMargin), - new Setter(WidthProperty, args.Width) - } - } - } - }; - var animation2 = new Avalonia.Animation.Animation - { - Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0), - Duration = TimeSpan.FromSeconds(0.25), - FillMode = FillMode.Forward, - Children = - { - new KeyFrame() - { - Cue = new Cue(0), - Setters = - { - new Setter(MarginProperty, obj.Margin.Value) - } - }, - new KeyFrame() - { - Cue = new Cue(1), - Setters = - { - new Setter(MarginProperty, args.Margin) - } - } - } - }; + var animation1 = new Avalonia.Animation.Animation { Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0), Duration = TimeSpan.FromSeconds(0.25), FillMode = FillMode.Forward, Children = { new KeyFrame() { Cue = new Cue(0), Setters = { new Setter(MarginProperty, border.Margin), new Setter(WidthProperty, border.Width), } }, new KeyFrame() { Cue = new Cue(1), Setters = { new Setter(MarginProperty, args.BorderMargin), new Setter(WidthProperty, args.Width) } } } }; + var animation2 = new Avalonia.Animation.Animation { Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0), Duration = TimeSpan.FromSeconds(0.25), FillMode = FillMode.Forward, Children = { new KeyFrame() { Cue = new Cue(0), Setters = { new Setter(MarginProperty, obj.Margin.Value) } }, new KeyFrame() { Cue = new Cue(1), Setters = { new Setter(MarginProperty, args.Margin) } } } }; Task task1 = animation1.RunAsync(border, token); Task task2 = animation2.RunAsync(this, token); @@ -157,11 +101,6 @@ await Dispatcher.UIThread.InvokeAsync(async () => }; obj.GetClickedTime = () => _pointerPosition; - obj.Model.GetObservable(Element.IsEnabledProperty) - .ObserveOnUIDispatcher() - .Subscribe(b => border.Opacity = b ? 1 : 0.5) - .DisposeWith(_disposables); - obj.IsSelected .ObserveOnUIDispatcher() .Subscribe(v => ZIndex = v ? 5 : 0) @@ -206,6 +145,15 @@ private void UseNodeClick(object? sender, RoutedEventArgs e) .DoAndRecord(recorder); } + private void EnableElementClick(object? sender, RoutedEventArgs e) + { + Element model = ViewModel.Model; + CommandRecorder recorder = ViewModel.Timeline.EditorContext.CommandRecorder; + RecordableCommands.Edit(model, Element.IsEnabledProperty, !model.IsEnabled) + .WithStoables([model]) + .DoAndRecord(recorder); + } + private void OnTextBoxLostFocus(object? sender, RoutedEventArgs e) { textBlock.IsVisible = true; @@ -222,28 +170,27 @@ private void Rename_Click(object? sender, RoutedEventArgs e) private void ChangeColor_Click(object? sender, RoutedEventArgs e) { - if (DataContext is ElementViewModel viewModel) + if (DataContext is not ElementViewModel viewModel) return; + + // ContextMenuから開いているので、閉じるのを待つ + s_colorPickerFlyout ??= new ColorPickerFlyout(); + s_colorPickerFlyout.ColorPicker.Color = viewModel.Color.Value; + s_colorPickerFlyout.ColorPicker.IsAlphaEnabled = false; + s_colorPickerFlyout.ColorPicker.UseColorPalette = true; + s_colorPickerFlyout.ColorPicker.IsCompact = true; + s_colorPickerFlyout.ColorPicker.IsMoreButtonVisible = true; + s_colorPickerFlyout.Placement = PlacementMode.Top; + + if (this.TryFindResource("PaletteColors", out object? colors) + && colors is IEnumerable tcolors) { - // ContextMenuから開いているので、閉じるのを待つ - s_colorPickerFlyout ??= new ColorPickerFlyout(); - s_colorPickerFlyout.ColorPicker.Color = viewModel.Color.Value; - s_colorPickerFlyout.ColorPicker.IsAlphaEnabled = false; - s_colorPickerFlyout.ColorPicker.UseColorPalette = true; - s_colorPickerFlyout.ColorPicker.IsCompact = true; - s_colorPickerFlyout.ColorPicker.IsMoreButtonVisible = true; - s_colorPickerFlyout.Placement = PlacementMode.Top; - - if (this.TryFindResource("PaletteColors", out object? colors) - && colors is IEnumerable tcolors) - { - s_colorPickerFlyout.ColorPicker.CustomPaletteColors = tcolors; - } + s_colorPickerFlyout.ColorPicker.CustomPaletteColors = tcolors; + } - s_colorPickerFlyout.Confirmed += OnColorPickerFlyoutConfirmed; - s_colorPickerFlyout.Closed += OnColorPickerFlyoutClosed; + s_colorPickerFlyout.Confirmed += OnColorPickerFlyoutConfirmed; + s_colorPickerFlyout.Closed += OnColorPickerFlyoutClosed; - s_colorPickerFlyout.ShowAt(border); - } + s_colorPickerFlyout.ShowAt(border); } private void OnColorPickerFlyoutClosed(object? sender, EventArgs e) @@ -325,13 +272,12 @@ public void OnLeftCtrlPressed(KeyEventArgs e) protected override void OnAttached() { base.OnAttached(); - if (AssociatedObject != null) - { - AssociatedObject.AddHandler(PointerMovedEvent, OnPointerMoved); - AssociatedObject.border.AddHandler(PointerPressedEvent, OnBorderPointerPressed); - AssociatedObject.border.AddHandler(PointerReleasedEvent, OnBorderPointerReleased); - AssociatedObject.border.AddHandler(PointerMovedEvent, OnBorderPointerMoved); - } + if (AssociatedObject == null) return; + + AssociatedObject.AddHandler(PointerMovedEvent, OnPointerMoved); + AssociatedObject.border.AddHandler(PointerPressedEvent, OnBorderPointerPressed); + AssociatedObject.border.AddHandler(PointerReleasedEvent, OnBorderPointerReleased); + AssociatedObject.border.AddHandler(PointerMovedEvent, OnBorderPointerMoved); } protected override void OnDetaching() @@ -398,11 +344,11 @@ private void OnPointerMoved(object? sender, PointerEventArgs e) private void OnBorderPointerPressed(object? sender, PointerPressedEventArgs e) { - if (AssociatedObject is { _timeline: { }, ViewModel: { } viewModel } view) + if (AssociatedObject is { _timeline: not null, ViewModel: { } viewModel } view) { PointerPoint point = e.GetCurrentPoint(view.border); if (point.Properties.IsLeftButtonPressed && e.KeyModifiers is KeyModifiers.None or KeyModifiers.Alt - && view.Cursor != Cursors.Arrow && view.Cursor is { }) + && view.Cursor != Cursors.Arrow && view.Cursor is not null) { _before = viewModel.Model.GetBefore(viewModel.Model.ZIndex, viewModel.Model.Start); _after = viewModel.Model.GetAfter(viewModel.Model.ZIndex, viewModel.Model.Range.End); @@ -483,29 +429,26 @@ private sealed class _MoveBehavior : Behavior protected override void OnAttached() { base.OnAttached(); - if (AssociatedObject != null) - { - AssociatedObject.AddHandler(PointerMovedEvent, OnPointerMoved); - AssociatedObject.border.AddHandler(PointerPressedEvent, OnBorderPointerPressed); - AssociatedObject.border.AddHandler(PointerReleasedEvent, OnBorderPointerReleased); - } + if (AssociatedObject == null) return; + + AssociatedObject.AddHandler(PointerMovedEvent, OnPointerMoved); + AssociatedObject.border.AddHandler(PointerPressedEvent, OnBorderPointerPressed); + AssociatedObject.border.AddHandler(PointerReleasedEvent, OnBorderPointerReleased); } protected override void OnDetaching() { base.OnDetaching(); - if (AssociatedObject != null) - { - AssociatedObject.RemoveHandler(PointerMovedEvent, OnPointerMoved); - AssociatedObject.border.RemoveHandler(PointerPressedEvent, OnBorderPointerPressed); - AssociatedObject.border.RemoveHandler(PointerReleasedEvent, OnBorderPointerReleased); - } + if (AssociatedObject == null) return; + + AssociatedObject.RemoveHandler(PointerMovedEvent, OnPointerMoved); + AssociatedObject.border.RemoveHandler(PointerPressedEvent, OnBorderPointerPressed); + AssociatedObject.border.RemoveHandler(PointerReleasedEvent, OnBorderPointerReleased); } private void OnPointerMoved(object? sender, PointerEventArgs e) { - if (AssociatedObject is { ViewModel: { } viewModel } view - && view._timeline is { } timeline && _pressed) + if (AssociatedObject is { ViewModel: { } viewModel, _timeline: { } timeline } view && _pressed) { Point point = e.GetPosition(view); float scale = viewModel.Timeline.Options.Value.Scale; @@ -601,39 +544,36 @@ private sealed class _SelectBehavior : Behavior protected override void OnAttached() { base.OnAttached(); - if (AssociatedObject != null) - { - AssociatedObject.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); - AssociatedObject.border.AddHandler(PointerPressedEvent, OnBorderPointerPressed); - AssociatedObject.border.AddHandler(PointerReleasedEvent, OnBorderPointerReleased); - } + if (AssociatedObject == null) return; + + AssociatedObject.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + AssociatedObject.border.AddHandler(PointerPressedEvent, OnBorderPointerPressed); + AssociatedObject.border.AddHandler(PointerReleasedEvent, OnBorderPointerReleased); } protected override void OnDetaching() { base.OnDetaching(); - if (AssociatedObject != null) - { - AssociatedObject.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); - AssociatedObject.border.RemoveHandler(PointerPressedEvent, OnBorderPointerPressed); - AssociatedObject.border.RemoveHandler(PointerReleasedEvent, OnBorderPointerReleased); - } + if (AssociatedObject == null) return; + + AssociatedObject.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + AssociatedObject.border.RemoveHandler(PointerPressedEvent, OnBorderPointerPressed); + AssociatedObject.border.RemoveHandler(PointerReleasedEvent, OnBorderPointerReleased); } private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { - if (AssociatedObject is { } obj) + if (AssociatedObject is not { } obj) return; + + if (!obj.textBox.IsFocused) { - if (!obj.textBox.IsFocused) - { - obj.Focus(); - } + obj.Focus(); } } private void OnBorderPointerPressed(object? sender, PointerPressedEventArgs e) { - if (AssociatedObject is { _timeline: { } } obj) + if (AssociatedObject is { _timeline.ViewModel: not null } obj) { PointerPoint point = e.GetCurrentPoint(obj.border); if (point.Properties.IsLeftButtonPressed) @@ -657,11 +597,9 @@ private void OnBorderPointerPressed(object? sender, PointerPressedEventArgs e) { Thickness margin = obj.ViewModel.Margin.Value; Thickness borderMargin = obj.ViewModel.BorderMargin.Value; - _snapshot = new(borderMargin.Left, margin.Top, 0, 0); + _snapshot = new Thickness(borderMargin.Left, margin.Top, 0, 0); _pressedWithModifier = true; } - - obj.border.Opacity = 0.8; } } } @@ -669,22 +607,22 @@ private void OnBorderPointerPressed(object? sender, PointerPressedEventArgs e) private void OnBorderPointerReleased(object? sender, PointerReleasedEventArgs e) { - if (AssociatedObject is { _timeline: { } } obj) + if (AssociatedObject is { _timeline: not null } obj) { if (_pressedWithModifier) { Thickness margin = obj.ViewModel.Margin.Value; Thickness borderMargin = obj.ViewModel.BorderMargin.Value; + // ReSharper disable CompareOfFloatsByEqualityOperator if (borderMargin.Left == _snapshot.Left && margin.Top == _snapshot.Top) { obj.ViewModel.IsSelected.Value = !obj.ViewModel.IsSelected.Value; } + // ReSharper restore CompareOfFloatsByEqualityOperator _pressedWithModifier = false; } - - obj.border.Opacity = 1; } } } diff --git a/src/Beutl/Views/LayerHeader.axaml b/src/Beutl/Views/LayerHeader.axaml index 58b141c01..5b3466c12 100644 --- a/src/Beutl/Views/LayerHeader.axaml +++ b/src/Beutl/Views/LayerHeader.axaml @@ -57,7 +57,8 @@