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}");