diff --git a/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs b/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs index cdc5155d8d..b03b9395b3 100644 --- a/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs +++ b/src/ui/Features/Main/Layout/InitListViewAndEditBox.cs @@ -118,6 +118,7 @@ public static Grid MakeLayoutListViewAndEditBox(MainView mainPage, MainViewModel var notNullConverter = new NotNullConverter(); var nullToOpacityConverter = new NullToOpacityConverter(); var syntaxHighlightingConverter = new TextWithSubtitleSyntaxHighlightingConverter(); + var textToFlowDirectionConverter = new TextToFlowDirectionConverter(); vm.SubtitleDataGridSyntaxHighlighting = syntaxHighlightingConverter; // How the Text/Original cells fit their text to the window (feature #11590). Read once here; // the grid is rebuilt when settings are applied, so a changed mode takes effect then. @@ -305,6 +306,7 @@ public static Grid MakeLayoutListViewAndEditBox(MainView mainPage, MainViewModel { VerticalAlignment = VerticalAlignment.Center, [!TextBlock.InlinesProperty] = new Binding(nameof(SubtitleLineViewModel.Text)) { Converter = syntaxHighlightingConverter, Mode = BindingMode.OneWay }, + [!TextBlock.FlowDirectionProperty] = new Binding(nameof(SubtitleLineViewModel.Text)) { Converter = textToFlowDirectionConverter, Mode = BindingMode.OneWay }, }; SubtitleGridTextDisplayModeDisplay.ApplyTo(textBlock, gridTextDisplayMode); @@ -336,6 +338,7 @@ public static Grid MakeLayoutListViewAndEditBox(MainView mainPage, MainViewModel { VerticalAlignment = VerticalAlignment.Center, [!TextBlock.InlinesProperty] = new Binding(nameof(SubtitleLineViewModel.OriginalText)) { Converter = syntaxHighlightingConverter, Mode = BindingMode.OneWay }, + [!TextBlock.FlowDirectionProperty] = new Binding(nameof(SubtitleLineViewModel.OriginalText)) { Converter = textToFlowDirectionConverter, Mode = BindingMode.OneWay }, }; SubtitleGridTextDisplayModeDisplay.ApplyTo(textBlock, gridTextDisplayMode); @@ -1250,6 +1253,9 @@ public static Grid MakeLayoutListViewAndEditBox(MainView mainPage, MainViewModel // Right panel for text editing (show/duration is to the left) var textEditGrid = new Grid { + // RightToLeftHelper mirrors this grid by name so the current/original + // text boxes keep matching the mirrored subtitle grid columns. + Name = "SubtitleTextEditGrid", ColumnDefinitions = new ColumnDefinitions("*,*,Auto"), RowDefinitions = new RowDefinitions("Auto,*,Auto"), }; @@ -1733,6 +1739,7 @@ private static Avalonia.Controls.Control MakeTextBox(MainViewModel vm) textBox.AddHandler(InputElement.PointerPressedEvent, (_, e) => vm.StoreTextEditorPointerArgs(e), RoutingStrategies.Tunnel); SetupMacContextMenuForTextBox(textBox, vm); + MainHelpers.RightToLeftHelper.FollowContentDirection(textBox); vm.EditTextBox = new TextBoxWrapper(textBox); return textBox; @@ -1843,6 +1850,7 @@ private static Avalonia.Controls.Control MakeTextBoxOriginal(MainViewModel vm) } SetupMacContextMenuForTextBox(textBox, vm); + MainHelpers.RightToLeftHelper.FollowContentDirection(textBox); vm.EditTextBoxOriginal = new TextBoxWrapper(textBox); return textBox; diff --git a/src/ui/Features/Main/Layout/TextEditorBindingHelper.cs b/src/ui/Features/Main/Layout/TextEditorBindingHelper.cs index 3bbe46245e..62444aa144 100644 --- a/src/ui/Features/Main/Layout/TextEditorBindingHelper.cs +++ b/src/ui/Features/Main/Layout/TextEditorBindingHelper.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using AvaloniaEdit; using Nikse.SubtitleEdit.Features.Shared.TextBoxUtils; +using Nikse.SubtitleEdit.Logic.Config; namespace Nikse.SubtitleEdit.Features.Main.Layout; @@ -157,6 +158,14 @@ private void OnTextEditorTextChanged(object? sender, System.EventArgs e) UpdateViewModelFromEditor(); var routedEvent = _isOriginal ? OriginalTextChangedEvent : TextChangedEvent; _vm.SubtitleTextChanged(sender, new TextChangedEventArgs(routedEvent)); + + // Follow the content: an original subtitle in a left to right language + // stays readable next to a right to left working language and vice versa. + var requestedDirection = Se.Settings.Appearance.RightToLeft + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + _textEditor.TextArea.FlowDirection = MainHelpers.RightToLeftHelper + .GetContentDirection(_textEditor.Text, requestedDirection); } private void OnTextEditorTapped(object? sender, RoutedEventArgs e) diff --git a/src/ui/Features/Main/MainHelpers/RightToLeftHelper.cs b/src/ui/Features/Main/MainHelpers/RightToLeftHelper.cs index 3470818f51..8fcc20e80e 100644 --- a/src/ui/Features/Main/MainHelpers/RightToLeftHelper.cs +++ b/src/ui/Features/Main/MainHelpers/RightToLeftHelper.cs @@ -1,9 +1,11 @@ -using Avalonia; +using System.Linq; +using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.VisualTree; using AvaloniaEdit.Editing; using Nikse.SubtitleEdit.Controls; +using Nikse.SubtitleEdit.Core.Common; using Nikse.SubtitleEdit.Logic.Config; namespace Nikse.SubtitleEdit.Features.Main.MainHelpers; @@ -16,6 +18,104 @@ internal static void SetRightToLeftForDataGridAndText(Window window) SetFlowDirectionRecursive(window, flowDirection); } + /// + /// A text control follows its content in either mode: text with right to left + /// letters (Arabic, Hebrew, and so on) flows right to left, text in a left to + /// right script (for example a Turkish or English original subtitle) flows + /// left to right. The layout itself still follows only the mode toggle; this + /// governs text direction inside a control. Empty controls keep the requested + /// direction so typing starts on the side matching the active mode. + /// + internal static FlowDirection GetContentDirection(string? text, FlowDirection requested) + { + if (string.IsNullOrWhiteSpace(text)) + { + return requested; + } + + return LanguageAutoDetect.ContainsRightToLeftLetter(text) + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + } + + /// + /// Keeps a text box's flow direction following its content while right to left + /// mode is enabled: right to left content stays right to left, left to right + /// content (for example a Turkish original next to an Arabic working language) + /// aligns left to right. Attach once when the text box is created; text set + /// while right to left mode is off is left alone. + /// + internal static void FollowContentDirection(TextBox textBox) + { + textBox.PropertyChanged += (_, e) => + { + if (e.Property == TextBox.TextProperty) + { + var requested = Se.Settings.Appearance.RightToLeft + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + textBox.FlowDirection = GetContentDirection(textBox.Text, requested); + } + }; + } + + /// + /// Mirrors the main text edit grid so the current and original text boxes keep + /// matching the mirrored subtitle grid columns (issue #12249). Setting + /// FlowDirection on a grid does not rearrange its children, so the columns are + /// swapped for real: the column definitions are reversed (the original column's + /// width binding travels with its definition object) and every child is + /// remapped to the mirrored column index. Idempotent via the grid's Tag. + /// + private static void MirrorTextEditGrid(Grid grid, FlowDirection flowDirection) + { + var wantMirrored = flowDirection == FlowDirection.RightToLeft; + var isMirrored = Equals(grid.Tag, "mirrored"); + if (wantMirrored == isMirrored) + { + return; + } + + grid.Tag = wantMirrored ? "mirrored" : null; + + var definitions = grid.ColumnDefinitions.ToList(); + grid.ColumnDefinitions.Clear(); + definitions.Reverse(); + foreach (var definition in definitions) + { + grid.ColumnDefinitions.Add(definition); + } + + var last = definitions.Count - 1; + foreach (var child in grid.Children) + { + Grid.SetColumn(child, last - Grid.GetColumn(child)); + } + } + + /// + /// Re-runs the subtitle grid's cell bindings after the right to left mode + /// changed: the per line flow direction converter reads the setting, and + /// bindings do not re-evaluate on their own when a setting changes. Rebinding + /// the items source re-creates the rows; selection and scroll position are + /// restored. + /// + internal static void RefreshDataGridBindings(DataGrid? grid, System.Collections.IEnumerable? itemsSource, object? selected) + { + if (grid == null) + { + return; + } + + grid.ItemsSource = null; + grid.ItemsSource = itemsSource; + if (selected != null) + { + grid.SelectedItem = selected; + grid.ScrollIntoView(selected, null); + } + } + private static void SetFlowDirectionRecursive(Visual visual, FlowDirection flowDirection) { if (visual is ComboBox) @@ -42,13 +142,17 @@ private static void SetFlowDirectionRecursive(Visual visual, FlowDirection flowD { dataGrid.FlowDirection = flowDirection; } + else if (visual is Grid grid && grid.Name == "SubtitleTextEditGrid") + { + MirrorTextEditGrid(grid, flowDirection); + } else if (visual is TextBox textBox) { - textBox.FlowDirection = flowDirection; + textBox.FlowDirection = GetContentDirection(textBox.Text, flowDirection); } else if (visual is TextArea textArea) { - textArea.FlowDirection = flowDirection; + textArea.FlowDirection = GetContentDirection(textArea.Document?.Text, flowDirection); } foreach (var child in visual.GetVisualChildren()) diff --git a/src/ui/Features/Main/MainViewModel.cs b/src/ui/Features/Main/MainViewModel.cs index d2c0a594ec..391b065a4a 100644 --- a/src/ui/Features/Main/MainViewModel.cs +++ b/src/ui/Features/Main/MainViewModel.cs @@ -11065,6 +11065,7 @@ private void RightToLeftToggle() Se.Settings.Appearance.RightToLeft = !Se.Settings.Appearance.RightToLeft; IsRightToLeftEnabled = Se.Settings.Appearance.RightToLeft; RightToLeftHelper.SetRightToLeftForDataGridAndText(Window); + RightToLeftHelper.RefreshDataGridBindings(SubtitleGrid, Subtitles, SelectedSubtitle); return; } diff --git a/src/ui/Logic/ValueConverters/TextToFlowDirectionConverter.cs b/src/ui/Logic/ValueConverters/TextToFlowDirectionConverter.cs new file mode 100644 index 0000000000..854230a559 --- /dev/null +++ b/src/ui/Logic/ValueConverters/TextToFlowDirectionConverter.cs @@ -0,0 +1,40 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Logic.Config; +using System; +using System.Globalization; + +namespace Nikse.SubtitleEdit.Logic.ValueConverters; + +/// +/// Gives a text presenter the flow direction of its content in either mode: cells +/// with right to left letters flow right to left (also as the original next to a +/// left to right working language), cells in a left to right script flow left to +/// right (also as the original next to a right to left working language). Empty +/// cells follow the active mode. +/// +public class TextToFlowDirectionConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var rightToLeftMode = Se.Settings.Appearance.RightToLeft; + if (value is string text && !string.IsNullOrWhiteSpace(text)) + { + return LanguageAutoDetect.ContainsRightToLeftLetter(text) + ? FlowDirection.RightToLeft + : FlowDirection.LeftToRight; + } + + // Always produce an explicit direction: a binding that yields UnsetValue + // does not fall back to the inherited direction but to the property + // default (left to right), which left right to left cells misaligned in + // right to left mode. + return rightToLeftMode ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +}