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();
+ }
+}