Skip to content

Commit

Permalink
Windows FlowDirection (#4936)
Browse files Browse the repository at this point in the history
* RTL_Window

* - setup flow direction handlers

* - set flow directions from other platforms

* - RTL Reading check

* - add tests

* - remove setdevice

* - remove setFlowDirection

* - RTL

* - additional RTL settings

* - change to current culture

* - move flowdirection check to Essentials

* - fix tests

* - if/def device for now

* - fix flowdirection

* - change NS implementation to return unknown

* - fix mock platform services

Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
  • Loading branch information
PureWeen and mattleibow authored Mar 1, 2022
1 parent 5913b32 commit 439b52c
Show file tree
Hide file tree
Showing 28 changed files with 262 additions and 123 deletions.
2 changes: 0 additions & 2 deletions src/Compatibility/Core/src/Android/Forms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,6 @@ static void SetupInit(

Profile.FramePartition("Epilog");

Device.SetFlowDirection(activity.Resources.Configuration.LayoutDirection.ToFlowDirection());

if (ExpressionSearch.Default == null)
ExpressionSearch.Default = new AndroidExpressionSearch();

Expand Down
2 changes: 0 additions & 2 deletions src/Compatibility/Core/src/Windows/Forms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ static void SetupInit(
var accent = (WSolidColorBrush)Microsoft.UI.Xaml.Application.Current.Resources["SystemColorControlAccentBrush"];
KnownColor.SetAccent(accent.ToColor());

Device.SetFlowDirection(mauiContext.GetFlowDirection());

ExpressionSearch.Default = new WindowsExpressionSearch();

Registrar.ExtraAssemblies = rendererAssemblies?.ToArray();
Expand Down
28 changes: 0 additions & 28 deletions src/Compatibility/Core/src/iOS/Forms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,34 +145,6 @@ static void SetupInit(IMauiContext context, InitializationOptions? maybeOptions

Application.AccentColor = Color.FromRgba(50, 79, 133, 255);

#if __MOBILE__
Device.SetFlowDirection(UIApplication.SharedApplication.UserInterfaceLayoutDirection.ToFlowDirection());
#else
if (!IsInitialized)
{
// Only need to do this once
// Subscribe to notifications in OS Theme changes
NSDistributedNotificationCenter.GetDefaultCenter().AddObserver((NSString)"AppleInterfaceThemeChangedNotification", (n) =>
{
var interfaceStyle = NSUserDefaults.StandardUserDefaults.StringForKey("AppleInterfaceStyle");

var aquaAppearance = NSAppearance.GetAppearance(interfaceStyle == "Dark" ? NSAppearance.NameDarkAqua : NSAppearance.NameAqua);
NSApplication.SharedApplication.Appearance = aquaAppearance;

Application.Current?.TriggerThemeChanged(new AppThemeChangedEventArgs(interfaceStyle == "Dark" ? AppTheme.Dark : AppTheme.Light));
});
}

Device.SetFlowDirection(NSApplication.SharedApplication.UserInterfaceLayoutDirection.ToFlowDirection());

if (IsMojaveOrNewer)
{
var interfaceStyle = NSUserDefaults.StandardUserDefaults.StringForKey("AppleInterfaceStyle");
var aquaAppearance = NSAppearance.GetAppearance(interfaceStyle == "Dark" ? NSAppearance.NameDarkAqua : NSAppearance.NameAqua);
NSApplication.SharedApplication.Appearance = aquaAppearance;
}
#endif

Device.DefaultRendererAssembly = typeof(Forms).Assembly;

if (maybeOptions?.Flags.HasFlag(InitializationFlags.SkipRenderers) != true)
Expand Down
32 changes: 0 additions & 32 deletions src/Controls/docs/Microsoft.Maui.Controls/Device.xml
Original file line number Diff line number Diff line change
Expand Up @@ -692,38 +692,6 @@ button.HeightRequest = Device.OnPlatform (20,30,30);
<remarks>To be added.</remarks>
</Docs>
</Member>
<Member MemberName="SetFlowDirection">
<MemberSignature Language="C#" Value="public static void SetFlowDirection (Microsoft.Maui.Controls.FlowDirection value);" />
<MemberSignature Language="ILAsm" Value=".method public static hidebysig void SetFlowDirection(valuetype Microsoft.Maui.Controls.FlowDirection value) cil managed" />
<MemberSignature Language="DocId" Value="M:Microsoft.Maui.Controls.Device.SetFlowDirection(Microsoft.Maui.Controls.FlowDirection)" />
<MemberSignature Language="F#" Value="static member SetFlowDirection : Microsoft.Maui.Controls.FlowDirection -&gt; unit" Usage="Microsoft.Maui.Controls.Device.SetFlowDirection value" />
<MemberType>Method</MemberType>
<AssemblyInfo>
<AssemblyName>Microsoft.Maui.Controls.Core</AssemblyName>
<AssemblyVersion>0.0.0.0</AssemblyVersion>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<Attributes>
<Attribute>
<AttributeName>System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)</AttributeName>
</Attribute>
</Attributes>
<ReturnValue>
<ReturnType>System.Void</ReturnType>
</ReturnValue>
<Parameters>
<Parameter Name="value" Type="Microsoft.Maui.Controls.FlowDirection" />
</Parameters>
<Docs>
<param name="value">The new flow direction value to set.</param>
<summary>Sets the flow direction on the device.</summary>
<remarks>
<para>The following contains a few important points about flow direction from <format type="text/html"><a href="https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/localization/rtl">Right-to-Left Localization</a></format>. Developers should consult that document for more information about limitations of right-to-left support, and for requirements to implement right-to-left support on various target platforms.</para>
<para>The default value of <see cref="T:Microsoft.Maui.Controls.FlowDirection" /> for a visual element that has no parent is <see cref="F:Microsoft.Maui.Controls.FlowDirection.LeftToRight" />, even on platforms where <see cref="P:Microsoft.Maui.Controls.Device.FlowDirection" /> is <see cref="F:Microsoft.Maui.Controls.FlowDirection.RightToLeft" />. Therefore, developers must deliberately opt in to right-to-left layout. Developers can choose right-to-left layout by setting the <see cref="P:Microsoft.Maui.Controls.VisualElement.FlowDirection" /> property of the root element to <see cref="F:Microsoft.Maui.Controls.FlowDirection.RightToLeft" /> to chosse right-to-left layout, or to <see cref="F:Microsoft.Maui.Controls.FlowDirection.MatchParent" /> to match the device layout.</para>
<para>All <see cref="T:Microsoft.Maui.Controls.VisualElement" />s that have a parent default to <see cref="F:Microsoft.Maui.Controls.FlowDirection.MatchParent" />.</para>
</remarks>
</Docs>
</Member>
<Member MemberName="StartTimer">
<MemberSignature Language="C#" Value="public static void StartTimer (TimeSpan interval, Func&lt;bool&gt; callback);" />
<MemberSignature Language="ILAsm" Value=".method public static hidebysig void StartTimer(valuetype System.TimeSpan interval, class System.Func`1&lt;bool&gt; callback) cil managed" />
Expand Down
13 changes: 9 additions & 4 deletions src/Controls/src/Core/Device.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,16 @@ public static TargetIdiom Idiom
/// <include file="../../docs/Microsoft.Maui.Controls/Device.xml" path="//Member[@MemberName='RuntimePlatform']/Docs" />
public static string RuntimePlatform => DeviceInfo.Platform.ToString();

/// <include file="../../docs/Microsoft.Maui.Controls/Device.xml" path="//Member[@MemberName='SetFlowDirection']/Docs" />
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetFlowDirection(FlowDirection value) => FlowDirection = value;
// [Obsolete]
/// <include file="../../docs/Microsoft.Maui.Controls/Device.xml" path="//Member[@MemberName='FlowDirection']/Docs" />
public static FlowDirection FlowDirection { get; internal set; }
public static FlowDirection FlowDirection
{
get
{
return AppInfo.RequestedLayoutDirection == LayoutDirection.RightToLeft
? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
}
}

//[Obsolete("Use BindableObject.Dispatcher instead.")]
/// <include file="../../docs/Microsoft.Maui.Controls/Device.xml" path="//Member[@MemberName='IsInvokeRequired']/Docs" />
Expand Down
89 changes: 85 additions & 4 deletions src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@
namespace Microsoft.Maui.Controls
{
[ContentProperty(nameof(Page))]
public partial class Window : NavigableElement, IWindow, IVisualTreeElement, IToolbarElement, IMenuBarElement
public partial class Window : NavigableElement, IWindow, IVisualTreeElement, IToolbarElement, IMenuBarElement, IFlowDirectionController
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title), typeof(string), typeof(Window), default(string?));

public static readonly BindableProperty MenuBarProperty = BindableProperty.Create(
nameof(MenuBar), typeof(MenuBar), typeof(Window), default(MenuBar));

public static readonly BindableProperty PageProperty = BindableProperty.Create(
nameof(Page), typeof(Page), typeof(Window), default(Page?),
propertyChanged: OnPageChanged);

public static readonly BindableProperty FlowDirectionProperty = BindableProperty.Create(nameof(FlowDirection), typeof(FlowDirection), typeof(Window), FlowDirection.MatchParent, propertyChanging: FlowDirectionChanging, propertyChanged: FlowDirectionChanged);

HashSet<IWindowOverlay> _overlays = new HashSet<IWindowOverlay>();
ReadOnlyCollection<Element>? _logicalChildren;
List<IVisualTreeElement> _visualChildren;
Expand Down Expand Up @@ -149,6 +148,56 @@ public bool RemoveOverlay(IWindowOverlay overlay)
internal IMauiContext MauiContext =>
Handler?.MauiContext ?? throw new InvalidOperationException("MauiContext is null.");

IFlowDirectionController FlowController => this;

public FlowDirection FlowDirection
{
get { return (FlowDirection)GetValue(FlowDirectionProperty); }
set { SetValue(FlowDirectionProperty, value); }
}

EffectiveFlowDirection _effectiveFlowDirection = default(EffectiveFlowDirection);
EffectiveFlowDirection IFlowDirectionController.EffectiveFlowDirection
{
get => _effectiveFlowDirection;
set => SetEffectiveFlowDirection(value, true);
}

double IFlowDirectionController.Width => (Page as VisualElement)?.Width ?? 0;

void SetEffectiveFlowDirection(EffectiveFlowDirection value, bool fireFlowDirectionPropertyChanged)
{
if (value == _effectiveFlowDirection)
return;

_effectiveFlowDirection = value;

if (fireFlowDirectionPropertyChanged)
OnPropertyChanged(FlowDirectionProperty.PropertyName);

}

static void FlowDirectionChanging(BindableObject bindable, object oldValue, object newValue)
{
var self = (IFlowDirectionController)bindable;

if (self.EffectiveFlowDirection.IsExplicit() && oldValue == newValue)
return;

var newFlowDirection = ((FlowDirection)newValue).ToEffectiveFlowDirection(isExplicit: true);
self.EffectiveFlowDirection = newFlowDirection;
}

static void FlowDirectionChanged(BindableObject bindable, object oldValue, object newValue)
{
PropertyPropagationExtensions.PropagatePropertyChanged(
FlowDirectionProperty.PropertyName,
(Element)bindable,
((IElementController)bindable).LogicalChildren);
}

bool IFlowDirectionController.ApplyEffectiveFlowDirectionToChildContainer => true;

IView IWindow.Content =>
Page ?? throw new InvalidOperationException("No page was set on the window.");

Expand Down Expand Up @@ -277,6 +326,36 @@ void IWindow.Backgrounding(IPersistedState state)
OnBackgrounding(state);
}

FlowDirection IWindow.FlowDirection
{
get
{
// If the user has set the root page to be RTL
// Then we want the window to also reflect RTL
// We don't want to force the user to reach into the window
// in order to enable RTL Window features on WinUI
if (FlowDirection == FlowDirection.MatchParent &&
Page is IFlowDirectionController controller &&
controller.EffectiveFlowDirection.IsExplicit())
{
return controller.EffectiveFlowDirection.ToFlowDirection();
}

return _effectiveFlowDirection.ToFlowDirection();
}
}

private protected override void OnHandlerChangingCore(HandlerChangingEventArgs args)
{
base.OnHandlerChangingCore(args);
var mauiContext = args?.NewHandler?.MauiContext;

if (FlowDirection == FlowDirection.MatchParent && mauiContext != null)
{
FlowController.EffectiveFlowDirection = mauiContext.GetFlowDirection().ToEffectiveFlowDirection(true);
}
}

// Currently this returns MainPage + ModalStack
// Depending on how we want this to show up inside LVT
// we might want to change this to only return the currently visible page
Expand Down Expand Up @@ -315,6 +394,8 @@ static void OnPageChanged(BindableObject bindable, object oldValue, object newVa
OnPageHandlerChanged(newPage, EventArgs.Empty);
}

window?.Handler?.UpdateValue(nameof(IWindow.FlowDirection));

void OnPageHandlerChanged(object? sender, EventArgs e)
{
window.ModalNavigationManager.PageAttachedHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
[TestFixture]
public class EffectiveFlowDirectionExtensions : BaseTestFixture
{
public override void Setup()
{
base.Setup();
Device.FlowDirection = FlowDirection.LeftToRight;
}

[Test]
public void LeftToRightImplicit()
{
Expand Down
8 changes: 0 additions & 8 deletions src/Controls/tests/Core.UnitTests/FlowDirectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,14 +527,6 @@ public void ShellPropagatesRightToLeftChangetoNewElements()
Assert.IsTrue(stacklayoutVisualController.IsRightToLeft(), "EffectiveFlowDirection should be RightToLeft");
}


[SetUp]
public override void Setup()
{
base.Setup();
Device.FlowDirection = FlowDirection.LeftToRight;
}

static void AddExplicitLTRToScrollView(ScrollView parent, View child)
{
parent.Content = child;
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/tests/Core.UnitTests/MockPlatformServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ class MockAppInfo : IAppInfo

public string BuildString { get; set; }

public LayoutDirection RequestedLayoutDirection { get; set; }

public void ShowSettingsUI()
{
}
Expand Down
29 changes: 29 additions & 0 deletions src/Controls/tests/Core.UnitTests/WindowsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ namespace Microsoft.Maui.Controls.Core.UnitTests
[TestFixture]
public class WindowsTests : BaseTestFixture
{
[Test]
public void ContentPageFlowDirectionSetsOnIWindow()
{
var app = new TestApp();
var window = app.CreateWindow();
window.Page.FlowDirection = FlowDirection.RightToLeft;

Assert.IsTrue((window as IWindow)
.FlowDirection == FlowDirection.RightToLeft);
}

[Test]
public void WindowFlowDirectionSetsOnPage()
{
var app = new TestApp();
var window = app.CreateWindow();
window.FlowDirection = FlowDirection.RightToLeft;

Assert.IsTrue((window.Page as IFlowDirectionController)
.EffectiveFlowDirection
.IsRightToLeft());

window.Page = new ContentPage();

Assert.IsTrue((window.Page as IFlowDirectionController)
.EffectiveFlowDirection
.IsRightToLeft());
}

[Test]
public void AddWindow()
{
Expand Down
2 changes: 2 additions & 0 deletions src/Core/src/Core/IWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,7 @@ public interface IWindow : ITitledElement
/// </summary>
/// <returns>Whether or not the back navigation was handled.</returns>
bool BackButtonClicked();

FlowDirection FlowDirection { get; }
}
}
20 changes: 18 additions & 2 deletions src/Core/src/Handlers/Window/WindowHandler.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ protected override void ConnectHandler(UI.Xaml.Window platformView)
}

protected override void DisconnectHandler(UI.Xaml.Window platformView)
{
{
MauiContext
?.GetNavigationRootManager()
?.Disconnect();
?.Disconnect();

_rootPanel?.Children?.Clear();
platformView.Content = null;
Expand Down Expand Up @@ -73,5 +73,21 @@ public static void MapMenuBar(IWindowHandler handler, IWindow view)
windowManager.SetMenuBar(mb.MenuBar);
}
}

public static void MapFlowDirection(IWindowHandler handler, IWindow view)
{
var WindowHandle = handler.PlatformView.GetWindowHandle();

// Retrieve current extended style
var extended_style = PlatformMethods.GetWindowLongPtr(WindowHandle, PlatformMethods.WindowLongFlags.GWL_EXSTYLE);
long updated_style;
if (view.FlowDirection == FlowDirection.RightToLeft)
updated_style = extended_style | (long)PlatformMethods.ExtendedWindowStyles.WS_EX_LAYOUTRTL;
else
updated_style = extended_style & ~((long)PlatformMethods.ExtendedWindowStyles.WS_EX_LAYOUTRTL);

if (updated_style != extended_style)
PlatformMethods.SetWindowLongPtr(WindowHandle, PlatformMethods.WindowLongFlags.GWL_EXSTYLE, updated_style);
}
}
}
5 changes: 4 additions & 1 deletion src/Core/src/Handlers/Window/WindowHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ public partial class WindowHandler : IWindowHandler
[nameof(IWindow.Content)] = MapContent,
#if ANDROID || WINDOWS
[nameof(IToolbarElement.Toolbar)] = MapToolbar,
#endif
#endif
#if WINDOWS || IOS
[nameof(IMenuBarElement.MenuBar)] = MapMenuBar,
#endif
#if WINDOWS
[nameof(IWindow.FlowDirection)] = MapFlowDirection,
#endif
};

Expand Down
11 changes: 11 additions & 0 deletions src/Core/src/MauiContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Animations;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Essentials;
using Microsoft.Maui.Hosting;

#if WINDOWS
Expand Down Expand Up @@ -67,5 +68,15 @@ public static void InitializeScopedServices(this IMauiContext scopedContext)
foreach (var service in scopedServices)
service.Initialize(scopedContext.Services);
}

public static FlowDirection GetFlowDirection(this IMauiContext mauiContext)
{
var appInfo = AppInfo.Current;

if (appInfo.RequestedLayoutDirection == LayoutDirection.RightToLeft)
return FlowDirection.RightToLeft;

return FlowDirection.LeftToRight;
}
}
}
Loading

0 comments on commit 439b52c

Please sign in to comment.