From c584bd3f9786ca01cdc7324a6b98c4a6070cd31f Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Mon, 21 Oct 2024 08:27:41 -0700 Subject: [PATCH] WIP Right To Left support --- src/Eto.Mac/AppDelegate.cs | 5 ++ src/Eto.Mac/Forms/ApplicationHandler.cs | 59 +++++++++++++++---- .../Forms/Cells/ImageTextCellHandler.cs | 2 +- src/Eto.Mac/Forms/Cells/TextBoxCellHandler.cs | 3 +- .../Forms/Controls/MacImageTextView.cs | 6 +- src/Eto.Mac/Forms/Controls/SplitterHandler.cs | 6 ++ src/Eto.Mac/Forms/MacView.cs | 21 +++++++ src/Eto.Mac/Forms/TableLayoutHandler.cs | 4 ++ src/Eto.Mac/MacConversions.cs | 30 ++++++---- src/Eto/Forms/Application.cs | 14 +++++ src/Eto/Forms/Controls/Control.cs | 28 +++++++++ .../Forms/Controls/ThemedControlHandler.cs | 8 +++ test/Eto.Test.Gtk/Startup.cs | 1 + test/Eto.Test.Mac/Info.plist | 2 +- test/Eto.Test.Mac/Startup.cs | 7 ++- .../Layouts/LayoutDirectionSection.cs | 25 ++++++++ test/Eto.Test/TestApplication.cs | 2 + 17 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 test/Eto.Test/Sections/Layouts/LayoutDirectionSection.cs diff --git a/src/Eto.Mac/AppDelegate.cs b/src/Eto.Mac/AppDelegate.cs index c2b9c9c086..32394dbb36 100644 --- a/src/Eto.Mac/AppDelegate.cs +++ b/src/Eto.Mac/AppDelegate.cs @@ -48,6 +48,11 @@ public override NSApplicationTerminateReply ApplicationShouldTerminate(NSApplica } return args.Cancel ? NSApplicationTerminateReply.Cancel : NSApplicationTerminateReply.Now; } + + public override void WillTerminate(NSNotification notification) + { + ApplicationHandler.ResetRtlPreference(); + } } } diff --git a/src/Eto.Mac/Forms/ApplicationHandler.cs b/src/Eto.Mac/Forms/ApplicationHandler.cs index f3792c38d6..4b5c6f5745 100644 --- a/src/Eto.Mac/Forms/ApplicationHandler.cs +++ b/src/Eto.Mac/Forms/ApplicationHandler.cs @@ -115,7 +115,7 @@ static void restart_WillTerminate(object sender, EventArgs e) var args = new string[] { "-c", - "open \"$1\"", + "open \"$1\"", string.Empty, NSBundle.MainBundle.BundlePath }; @@ -129,7 +129,7 @@ public void Invoke(Action action) else Control.InvokeOnMainThread(action); } - + public void AsyncInvoke(Action action) { Control.BeginInvokeOnMainThread(action); @@ -150,23 +150,23 @@ public void Restart() Control.Delegate = oldDelegate; } - static readonly IntPtr selNextEventMatchingMaskUntilDateInModeDequeue_Handle = Selector.GetHandle ("nextEventMatchingMask:untilDate:inMode:dequeue:"); - static readonly IntPtr selSendEvent_Handle = Selector.GetHandle ("sendEvent:"); - + static readonly IntPtr selNextEventMatchingMaskUntilDateInModeDequeue_Handle = Selector.GetHandle("nextEventMatchingMask:untilDate:inMode:dequeue:"); + static readonly IntPtr selSendEvent_Handle = Selector.GetHandle("sendEvent:"); + public void RunIteration() { MacView.InMouseTrackingLoop = false; // drain the event queue only for a short period of time so it doesn't lock up var date = NSDate.FromTimeIntervalSinceNow(0.001); - for (;;) + for (; ; ) { // dequeue the event var evt = Control.NextEvent(NSEventMask.AnyEvent, date, NSRunLoopMode.Default, true); - + // no event? cool, let's get out of here if (evt == null) break; - + // dispatch the event Control.SendEvent(evt); } @@ -196,7 +196,7 @@ public void Run() EtoBundle.Init(); - + EtoFontManager.Install(); if (Control.Delegate == null) @@ -227,7 +227,7 @@ public void Open(string url) #if Mac64 delegate void UncaughtExceptionHandlerDelegate(IntPtr nsexceptionPtr); - + [DllImport(Constants.FoundationLibrary)] static extern void NSSetUncaughtExceptionHandler(UncaughtExceptionHandlerDelegate handler); @@ -325,5 +325,44 @@ public void EnableFullScreen() public Keys AlternateModifier => Keys.Alt; public bool IsActive => NSApplication.SharedApplication.Active; + + public LayoutDirection DefaultLayoutDirection + { + get => NSApplication.SharedApplication.UserInterfaceLayoutDirection switch + { + NSApplicationLayoutDirection.LeftToRight => LayoutDirection.LeftToRight, + NSApplicationLayoutDirection.RightToLeft => LayoutDirection.RightToLeft, + _ => LayoutDirection.LeftToRight + }; + set + { + var rtl = value == LayoutDirection.RightToLeft; + // NSUserDefaults.StandardUserDefaults.RegisterDefaults(NSDictionary.FromObjectsAndKeys( + // new NSObject[] { NSNumber.FromBoolean(rtl), NSNumber.FromBoolean(rtl) }, + // new NSObject[] { new NSString("NSForceRightToLeftWritingDirection"), new NSString("AppleTextDirection") } + // )); + // Environment.SetEnvironmentVariable("NSForceRightToLeftWritingDirection", "YES"); + // Environment.SetEnvironmentVariable("AppleTextDirection", "YES"); + // Control.WillTerminate -= ResetRtlPreference; + if (rtl) + { + NSUserDefaults.StandardUserDefaults.SetValueForKey(NSNumber.FromBoolean(rtl), new NSString("NSForceRightToLeftWritingDirection")); + NSUserDefaults.StandardUserDefaults.SetValueForKey(NSNumber.FromBoolean(rtl), new NSString("AppleTextDirection")); + } + else + { + NSUserDefaults.StandardUserDefaults.RemoveObject(new NSString("NSForceRightToLeftWritingDirection")); + NSUserDefaults.StandardUserDefaults.RemoveObject(new NSString("AppleTextDirection")); + } + NSUserDefaults.StandardUserDefaults.Synchronize(); + } + } + + internal static void ResetRtlPreference() + { + // don't actually save these + NSUserDefaults.StandardUserDefaults.RemoveObject(new NSString("NSForceRightToLeftWritingDirection")); + NSUserDefaults.StandardUserDefaults.RemoveObject(new NSString("AppleTextDirection")); + } } } diff --git a/src/Eto.Mac/Forms/Cells/ImageTextCellHandler.cs b/src/Eto.Mac/Forms/Cells/ImageTextCellHandler.cs index 09d9f2338e..9539129c1a 100644 --- a/src/Eto.Mac/Forms/Cells/ImageTextCellHandler.cs +++ b/src/Eto.Mac/Forms/Cells/ImageTextCellHandler.cs @@ -227,7 +227,7 @@ public override NSView GetViewForItem(NSTableView tableView, NSTableColumn table var cell = view.TextCell; cell.VerticalAlignment = VerticalAlignment; - cell.Alignment = TextAlignment.ToNS(); + cell.Alignment = TextAlignment.ToNS(view.UserInterfaceLayoutDirection); view.Tag = row; view.Item = obj; diff --git a/src/Eto.Mac/Forms/Cells/TextBoxCellHandler.cs b/src/Eto.Mac/Forms/Cells/TextBoxCellHandler.cs index a5f9b732f5..b629228bbe 100644 --- a/src/Eto.Mac/Forms/Cells/TextBoxCellHandler.cs +++ b/src/Eto.Mac/Forms/Cells/TextBoxCellHandler.cs @@ -100,12 +100,13 @@ public CellView() { Wraps = false, Scrollable = true, - UsesSingleLineMode = false // true prevents proper vertical alignment + UsesSingleLineMode = false, // true prevents proper vertical alignment }; Selectable = false; DrawsBackground = false; Bezeled = false; Bordered = false; + Alignment = NSTextAlignment.Right; AutoresizingMask = NSViewResizingMask.HeightSizable | NSViewResizingMask.WidthSizable; } public CellView(IntPtr handle) : base(handle) { } diff --git a/src/Eto.Mac/Forms/Controls/MacImageTextView.cs b/src/Eto.Mac/Forms/Controls/MacImageTextView.cs index 1c096ecb13..b7de80b236 100644 --- a/src/Eto.Mac/Forms/Controls/MacImageTextView.cs +++ b/src/Eto.Mac/Forms/Controls/MacImageTextView.cs @@ -33,7 +33,8 @@ void SetSizes(CGSize bounds) var scaledHeight = Math.Min(imageSize.Height, bounds.Height); var scaledWidth = imageSize.Width * scaledHeight / imageSize.Height; _imageSize = new CGSize(scaledWidth, scaledHeight); - TextField.Frame = new CGRect(scaledWidth + ImagePadding, 0, bounds.Width - scaledWidth - ImagePadding, bounds.Height); + var isrtl = UserInterfaceLayoutDirection == NSUserInterfaceLayoutDirection.RightToLeft; + TextField.Frame = new CGRect(isrtl ? 0 : scaledWidth + ImagePadding, 0, bounds.Width - scaledWidth - ImagePadding, bounds.Height); } } @@ -91,6 +92,9 @@ public override void DrawRect(CGRect dirtyRect) var imageRect = new CGRect(0, bounds.Y, _imageSize.Width, _imageSize.Height); imageRect.Y += (bounds.Height - _imageSize.Height) / 2; + var isrtl = UserInterfaceLayoutDirection == NSUserInterfaceLayoutDirection.RightToLeft; + if (isrtl) + imageRect.X += bounds.Width - _imageSize.Width; const float alpha = 1; //Enabled ? 1 : (nfloat)0.5; diff --git a/src/Eto.Mac/Forms/Controls/SplitterHandler.cs b/src/Eto.Mac/Forms/Controls/SplitterHandler.cs index ff3d5d036c..66d257a5ee 100644 --- a/src/Eto.Mac/Forms/Controls/SplitterHandler.cs +++ b/src/Eto.Mac/Forms/Controls/SplitterHandler.cs @@ -229,6 +229,12 @@ void ResizeSubviews(CGSize oldSize2) panel1Rect.Width = panel1Rect.X = panel2Rect.Width = panel2Rect.X = 0; if (newFrame.Height <= 0) panel1Rect.Height = panel1Rect.Y = panel2Rect.Height = panel2Rect.Y = 0; + + if (splitView.IsVertical && splitView.UserInterfaceLayoutDirection == NSUserInterfaceLayoutDirection.RightToLeft) + { + panel1Rect.X = panel2Rect.Width + dividerThickness; + panel2Rect.X = 0; + } splitView.Subviews[0].Frame = panel1Rect; splitView.Subviews[1].Frame = panel2Rect; diff --git a/src/Eto.Mac/Forms/MacView.cs b/src/Eto.Mac/Forms/MacView.cs index 2a618f6ed7..a4453fcf8d 100644 --- a/src/Eto.Mac/Forms/MacView.cs +++ b/src/Eto.Mac/Forms/MacView.cs @@ -750,6 +750,11 @@ public virtual void InvalidateMeasure() Widget.VisualParent.GetMacControl()?.InvalidateMeasure(); } + protected override void Initialize() + { + base.Initialize(); + } + protected virtual SizeF GetNaturalSize(SizeF availableSize) { var naturalSize = NaturalSize; @@ -1729,6 +1734,22 @@ public virtual void OnViewDidMoveToWindow() public bool IsMouseCaptured => MacView.CapturedControl == this; + public LayoutDirection LayoutDirection + { + get => EventControl.UserInterfaceLayoutDirection switch + { + NSUserInterfaceLayoutDirection.LeftToRight => LayoutDirection.LeftToRight, + NSUserInterfaceLayoutDirection.RightToLeft => LayoutDirection.RightToLeft, + _ => LayoutDirection.LeftToRight + }; + set => EventControl.UserInterfaceLayoutDirection = value switch + { + LayoutDirection.LeftToRight => NSUserInterfaceLayoutDirection.LeftToRight, + LayoutDirection.RightToLeft => NSUserInterfaceLayoutDirection.RightToLeft, + _ => NSUserInterfaceLayoutDirection.LeftToRight, + }; + } + public bool CaptureMouse() { if (!Widget.Loaded || !Widget.Visible) diff --git a/src/Eto.Mac/Forms/TableLayoutHandler.cs b/src/Eto.Mac/Forms/TableLayoutHandler.cs index 8dd3c01ef5..4601ca913d 100644 --- a/src/Eto.Mac/Forms/TableLayoutHandler.cs +++ b/src/Eto.Mac/Forms/TableLayoutHandler.cs @@ -276,6 +276,7 @@ void PerformLayout() #endif float starty = Padding.Top; + var isrtl = Control.UserInterfaceLayoutDirection == NSUserInterfaceLayoutDirection.RightToLeft; for (int y = 0; y < final_heights.Length; y++) { float startx = Padding.Left; @@ -290,6 +291,9 @@ void PerformLayout() frame.Width = final_widths[x]; frame.Height = final_heights[y]; frame.X = (nfloat)Math.Round(Math.Max(0, startx)); + if (isrtl) + frame.X = controlSize.Width - frame.X - frame.Width; + frame.Y = (nfloat)Math.Round(flipped ? starty : controlSize.Height - starty - frame.Height); if (frame != oldframe) macView.SetAlignmentFrame(frame); diff --git a/src/Eto.Mac/MacConversions.cs b/src/Eto.Mac/MacConversions.cs index 5ccfe01042..44058dd11b 100644 --- a/src/Eto.Mac/MacConversions.cs +++ b/src/Eto.Mac/MacConversions.cs @@ -463,19 +463,25 @@ public static TextAlignment ToEto(this NSTextAlignment align) } } - public static NSTextAlignment ToNS(this TextAlignment align) + public static NSTextAlignment ToNS(this TextAlignment align, NSUserInterfaceLayoutDirection? direction = null) { - switch (align) - { - case TextAlignment.Left: - return NSTextAlignment.Left; - case TextAlignment.Center: - return NSTextAlignment.Center; - case TextAlignment.Right: - return NSTextAlignment.Right; - default: - throw new NotSupportedException(); - } + var dir = direction ?? (NSUserInterfaceLayoutDirection)NSApplication.SharedApplication.UserInterfaceLayoutDirection; + if (dir == NSUserInterfaceLayoutDirection.RightToLeft) + return align switch + { + TextAlignment.Left => NSTextAlignment.Right, + TextAlignment.Right => NSTextAlignment.Left, + TextAlignment.Center => NSTextAlignment.Center, + _ => throw new NotSupportedException() + }; + else + return align switch + { + TextAlignment.Left => NSTextAlignment.Left, + TextAlignment.Right => NSTextAlignment.Right, + TextAlignment.Center => NSTextAlignment.Center, + _ => throw new NotSupportedException() + }; } public static Font ToEto(this NSFont font) diff --git a/src/Eto/Forms/Application.cs b/src/Eto/Forms/Application.cs index 272591321b..37cde64862 100644 --- a/src/Eto/Forms/Application.cs +++ b/src/Eto/Forms/Application.cs @@ -539,6 +539,19 @@ public string BadgeLabel /// /// public bool IsActive => Handler.IsActive; + + + /// + /// Gets or sets the default layout direction for the user interface of the application. + /// + /// + /// + /// + public LayoutDirection DefaultLayoutDirection + { + get => Handler.DefaultLayoutDirection; + set => Handler.DefaultLayoutDirection = value; + } /// /// Advanced. Runs an iteration of the main UI loop when you are blocking the UI thread with logic. @@ -767,5 +780,6 @@ public void OnIsActiveChanged(Application widget, EventArgs e) /// Gets a value indicating that the application is currently the active application /// bool IsActive { get; } + LayoutDirection DefaultLayoutDirection { get; set; } } } \ No newline at end of file diff --git a/src/Eto/Forms/Controls/Control.cs b/src/Eto/Forms/Controls/Control.cs index 8aca9333c6..5b9d9d78b9 100755 --- a/src/Eto/Forms/Controls/Control.cs +++ b/src/Eto/Forms/Controls/Control.cs @@ -1,5 +1,20 @@ namespace Eto.Forms; +/// +/// Specifies the layout direction of the user interface. +/// +public enum LayoutDirection +{ + /// + /// Indicates the user interface should be laid out from left to right + /// + LeftToRight, + /// + /// Indicates the user interface should be laid out from right to left + /// + RightToLeft +} + /// /// Base for all visual UI elements /// @@ -885,6 +900,18 @@ internal virtual void InternalEnsureLayout() /// Releases the mouse capture after a call to . /// public void ReleaseMouseCapture() => Handler.ReleaseMouseCapture(); + + /// + /// Gets or sets the layout direction for this control explicitly. + /// + /// + /// The default layout direction of a control is based on the . + /// + public LayoutDirection LayoutDirection + { + get => Handler.LayoutDirection; + set => Handler.LayoutDirection = value; + } /// /// Gets or sets the width of the control size. @@ -2030,6 +2057,7 @@ public void OnEnabledChanged(Control widget, EventArgs e) /// or it can be captured explicitly via . /// bool IsMouseCaptured { get; } + LayoutDirection LayoutDirection { get; set; } /// /// Captures all mouse events to this control. diff --git a/src/Eto/Forms/Controls/ThemedControlHandler.cs b/src/Eto/Forms/Controls/ThemedControlHandler.cs index 61d373e809..f2e03348f0 100755 --- a/src/Eto/Forms/Controls/ThemedControlHandler.cs +++ b/src/Eto/Forms/Controls/ThemedControlHandler.cs @@ -468,6 +468,14 @@ public override void AttachEvent(string id) /// public bool IsMouseCaptured => Control.IsMouseCaptured; + + /// + public LayoutDirection LayoutDirection + { + get => Control.LayoutDirection; + set => Control.LayoutDirection = value; + } + /// public bool CaptureMouse() => Control.CaptureMouse(); /// diff --git a/test/Eto.Test.Gtk/Startup.cs b/test/Eto.Test.Gtk/Startup.cs index fc8c7d480f..c03a6e42a9 100644 --- a/test/Eto.Test.Gtk/Startup.cs +++ b/test/Eto.Test.Gtk/Startup.cs @@ -11,6 +11,7 @@ static void Main(string[] args) platform.Add(() => new NativeHostControls()); var app = new TestApplication(platform); + global::Gtk.Widget.DefaultDirection = global::Gtk.TextDirection.Rtl; app.TestAssemblies.Add(typeof(Startup).Assembly); app.Run(); } diff --git a/test/Eto.Test.Mac/Info.plist b/test/Eto.Test.Mac/Info.plist index 0e1f39b60d..f6a9953d98 100644 --- a/test/Eto.Test.Mac/Info.plist +++ b/test/Eto.Test.Mac/Info.plist @@ -5,7 +5,7 @@ CFBundleIconFile TestIcon.icns CFBundleIdentifier - com.yourcompany.Eto.Test.Mac + ca.picoe.Eto.Test.Mac CFBundleName Eto.Test.Mac CFBundleVersion diff --git a/test/Eto.Test.Mac/Startup.cs b/test/Eto.Test.Mac/Startup.cs index c0055df1d6..dd81da1d53 100644 --- a/test/Eto.Test.Mac/Startup.cs +++ b/test/Eto.Test.Mac/Startup.cs @@ -8,6 +8,9 @@ class Startup { static void Main(string[] args) { + // NSUserDefaults.StandardUserDefaults[new NSString("AppleLanguage")] = new NSString("AR"); + // NSUserDefaults.StandardUserDefaults.SetValueForKey(NSNumber.FromBoolean(true), new NSString("NSForceRightToLeftWritingDirection")); + // NSUserDefaults.StandardUserDefaults.SetValueForKey(NSNumber.FromBoolean(true), new NSString("AppleTextDirection")); AddStyles(); var stopwatch = new Stopwatch(); @@ -19,7 +22,7 @@ static void Main(string[] args) var app = new TestApplication(platform); app.AsyncInvoke(() => Log.Write(null, $"Startup: {stopwatch.Elapsed}")); app.TestAssemblies.Add(typeof(Startup).Assembly); - + // use this to use your own app delegate: // ApplicationHandler.Instance.AppDelegate = new MyAppDelegate(); @@ -36,6 +39,8 @@ static void AddStyles() Style.Add("application", handler => { + + // handler.Control.UserInterfaceLayoutDirection = NSUserInterfaceLayoutDirection.RightToLeft; handler.EnableFullScreen(); }); diff --git a/test/Eto.Test/Sections/Layouts/LayoutDirectionSection.cs b/test/Eto.Test/Sections/Layouts/LayoutDirectionSection.cs new file mode 100644 index 0000000000..a72953b198 --- /dev/null +++ b/test/Eto.Test/Sections/Layouts/LayoutDirectionSection.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Eto.Test.Sections.Layouts +{ + [Section("Layouts", "LayoutDirection")] + public class LayoutDirectionSection : Panel + { + public LayoutDirectionSection() + { + var dd = new EnumDropDown(); + dd.Bind(c => c.SelectedValue, Application.Instance, m => m.DefaultLayoutDirection); + + Content = new TableLayout + { + Rows = { + "Application.DefaultLayoutDirection:", dd + } + }; + + } + } +} \ No newline at end of file diff --git a/test/Eto.Test/TestApplication.cs b/test/Eto.Test/TestApplication.cs index d75a9c8a46..5379a7ead9 100644 --- a/test/Eto.Test/TestApplication.cs +++ b/test/Eto.Test/TestApplication.cs @@ -32,6 +32,8 @@ public TestApplication(Platform platform) this.Name = "Test Application"; this.Style = "application"; + this.DefaultLayoutDirection = LayoutDirection.RightToLeft; + if (Platform.Supports()) { NotificationActivated += (sender, e) => Log.Write(this, $"Notification: {e.ID}, userData: {e.UserData}");