Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Right To Left support #2734

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Eto.Mac/AppDelegate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ public override NSApplicationTerminateReply ApplicationShouldTerminate(NSApplica
}
return args.Cancel ? NSApplicationTerminateReply.Cancel : NSApplicationTerminateReply.Now;
}

public override void WillTerminate(NSNotification notification)
{
ApplicationHandler.ResetRtlPreference();
}
}
}

59 changes: 49 additions & 10 deletions src/Eto.Mac/Forms/ApplicationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand All @@ -129,7 +129,7 @@ public void Invoke(Action action)
else
Control.InvokeOnMainThread(action);
}

public void AsyncInvoke(Action action)
{
Control.BeginInvokeOnMainThread(action);
Expand All @@ -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);
}
Expand Down Expand Up @@ -196,7 +196,7 @@ public void Run()


EtoBundle.Init();

EtoFontManager.Install();

if (Control.Delegate == null)
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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"));
}
}
}
2 changes: 1 addition & 1 deletion src/Eto.Mac/Forms/Cells/ImageTextCellHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/Eto.Mac/Forms/Cells/TextBoxCellHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down
6 changes: 5 additions & 1 deletion src/Eto.Mac/Forms/Controls/MacImageTextView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

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

Expand Down
6 changes: 6 additions & 0 deletions src/Eto.Mac/Forms/Controls/SplitterHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 21 additions & 0 deletions src/Eto.Mac/Forms/MacView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions src/Eto.Mac/Forms/TableLayoutHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
30 changes: 18 additions & 12 deletions src/Eto.Mac/MacConversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions src/Eto/Forms/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,19 @@
/// </summary>
/// <seealso cref="IsActiveChanged"/>
public bool IsActive => Handler.IsActive;


/// <summary>
/// Gets or sets the default layout direction for the user interface of the application.
/// </summary>
/// <remarks>
///
/// </remarks>
public LayoutDirection DefaultLayoutDirection
{
get => Handler.DefaultLayoutDirection;
set => Handler.DefaultLayoutDirection = value;
}

/// <summary>
/// Advanced. Runs an iteration of the main UI loop when you are blocking the UI thread with logic.
Expand Down Expand Up @@ -767,5 +780,6 @@
/// Gets a value indicating that the application is currently the active application
/// </summary>
bool IsActive { get; }
LayoutDirection DefaultLayoutDirection { get; set; }

Check failure on line 783 in src/Eto/Forms/Application.cs

View workflow job for this annotation

GitHub Actions / build-windows

Missing XML comment for publicly visible type or member 'Application.IHandler.DefaultLayoutDirection'

Check failure on line 783 in src/Eto/Forms/Application.cs

View workflow job for this annotation

GitHub Actions / build-windows

Missing XML comment for publicly visible type or member 'Application.IHandler.DefaultLayoutDirection'

Check failure on line 783 in src/Eto/Forms/Application.cs

View workflow job for this annotation

GitHub Actions / build-mac

Missing XML comment for publicly visible type or member 'Application.IHandler.DefaultLayoutDirection'

Check failure on line 783 in src/Eto/Forms/Application.cs

View workflow job for this annotation

GitHub Actions / build-mac

Missing XML comment for publicly visible type or member 'Application.IHandler.DefaultLayoutDirection'

Check failure on line 783 in src/Eto/Forms/Application.cs

View workflow job for this annotation

GitHub Actions / build-mac

Missing XML comment for publicly visible type or member 'Application.IHandler.DefaultLayoutDirection'
}
}
28 changes: 28 additions & 0 deletions src/Eto/Forms/Controls/Control.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
namespace Eto.Forms;

/// <summary>
/// Specifies the layout direction of the user interface.
/// </summary>
public enum LayoutDirection
{
/// <summary>
/// Indicates the user interface should be laid out from left to right
/// </summary>
LeftToRight,
/// <summary>
/// Indicates the user interface should be laid out from right to left
/// </summary>
RightToLeft
}

/// <summary>
/// Base for all visual UI elements
/// </summary>
Expand Down Expand Up @@ -885,6 +900,18 @@
/// Releases the mouse capture after a call to <see cref="CaptureMouse"/>.
/// </summary>
public void ReleaseMouseCapture() => Handler.ReleaseMouseCapture();

/// <summary>
/// Gets or sets the layout direction for this control explicitly.
/// </summary>
/// <remarks>
/// The default layout direction of a control is based on the <see cref="Application.LayoutDirection"/>.

Check failure on line 908 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-windows

XML comment has cref attribute 'LayoutDirection' that could not be resolved

Check failure on line 908 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-windows

XML comment has cref attribute 'LayoutDirection' that could not be resolved

Check failure on line 908 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-mac

XML comment has cref attribute 'LayoutDirection' that could not be resolved

Check failure on line 908 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-mac

XML comment has cref attribute 'LayoutDirection' that could not be resolved

Check failure on line 908 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-mac

XML comment has cref attribute 'LayoutDirection' that could not be resolved
/// </remarks>
public LayoutDirection LayoutDirection
{
get => Handler.LayoutDirection;
set => Handler.LayoutDirection = value;
}

/// <summary>
/// Gets or sets the width of the control size.
Expand Down Expand Up @@ -2030,6 +2057,7 @@
/// or it can be captured explicitly via <see cref="CaptureMouse"/>.
/// </remarks>
bool IsMouseCaptured { get; }
LayoutDirection LayoutDirection { get; set; }

Check failure on line 2060 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-windows

Missing XML comment for publicly visible type or member 'Control.IHandler.LayoutDirection'

Check failure on line 2060 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-windows

Missing XML comment for publicly visible type or member 'Control.IHandler.LayoutDirection'

Check failure on line 2060 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-mac

Missing XML comment for publicly visible type or member 'Control.IHandler.LayoutDirection'

Check failure on line 2060 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-mac

Missing XML comment for publicly visible type or member 'Control.IHandler.LayoutDirection'

Check failure on line 2060 in src/Eto/Forms/Controls/Control.cs

View workflow job for this annotation

GitHub Actions / build-mac

Missing XML comment for publicly visible type or member 'Control.IHandler.LayoutDirection'

/// <summary>
/// Captures all mouse events to this control.
Expand Down
8 changes: 8 additions & 0 deletions src/Eto/Forms/Controls/ThemedControlHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,14 @@ public override void AttachEvent(string id)

/// <inheritdoc />
public bool IsMouseCaptured => Control.IsMouseCaptured;

/// <inheritdoc />
public LayoutDirection LayoutDirection
{
get => Control.LayoutDirection;
set => Control.LayoutDirection = value;
}

/// <inheritdoc />
public bool CaptureMouse() => Control.CaptureMouse();
/// <inheritdoc />
Expand Down
1 change: 1 addition & 0 deletions test/Eto.Test.Gtk/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ static void Main(string[] args)
platform.Add<INativeHostControls>(() => new NativeHostControls());

var app = new TestApplication(platform);
global::Gtk.Widget.DefaultDirection = global::Gtk.TextDirection.Rtl;
app.TestAssemblies.Add(typeof(Startup).Assembly);
app.Run();
}
Expand Down
2 changes: 1 addition & 1 deletion test/Eto.Test.Mac/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<key>CFBundleIconFile</key>
<string>TestIcon.icns</string>
<key>CFBundleIdentifier</key>
<string>com.yourcompany.Eto.Test.Mac</string>
<string>ca.picoe.Eto.Test.Mac</string>
<key>CFBundleName</key>
<string>Eto.Test.Mac</string>
<key>CFBundleVersion</key>
Expand Down
Loading
Loading