Skip to content
8 changes: 8 additions & 0 deletions src/ui/Features/Main/Layout/InitListViewAndEditBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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"),
};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/ui/Features/Main/Layout/TextEditorBindingHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
110 changes: 107 additions & 3 deletions src/ui/Features/Main/MainHelpers/RightToLeftHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,6 +18,104 @@ internal static void SetRightToLeftForDataGridAndText(Window window)
SetFlowDirectionRecursive(window, flowDirection);
}

/// <summary>
/// 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.
/// </summary>
internal static FlowDirection GetContentDirection(string? text, FlowDirection requested)
{
if (string.IsNullOrWhiteSpace(text))
{
return requested;
}

return LanguageAutoDetect.ContainsRightToLeftLetter(text)
? FlowDirection.RightToLeft
: FlowDirection.LeftToRight;
}

/// <summary>
/// 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.
/// </summary>
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);
}
};
}

/// <summary>
/// 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.
/// </summary>
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));
}
}

/// <summary>
/// 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.
/// </summary>
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)
Expand All @@ -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())
Expand Down
1 change: 1 addition & 0 deletions src/ui/Features/Main/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
40 changes: 40 additions & 0 deletions src/ui/Logic/ValueConverters/TextToFlowDirectionConverter.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
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();
}
}