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

Regression: Focus scope API no longer accessible #11613

Closed
mgarstenauer opened this issue Jun 1, 2023 · 7 comments
Closed

Regression: Focus scope API no longer accessible #11613

mgarstenauer opened this issue Jun 1, 2023 · 7 comments

Comments

@mgarstenauer
Copy link
Contributor

Describe the bug
The focus scope API (FocusManager.Scope, FocusManager.GetFocusedElement(IFocusScope)) is no longer accessible as of #11407, which is breaking our app/UI libraries.

Our current app implements ToolBars, RoutedCommands (similar to WPF), a "Quick Launch Box"/"Command palette" and docking windows (like Visual Studio). The application depends on focus scopes. Details:

  • Windows, Menus, ToolBarTrays and the "Quick Launch Box" implement IFocusScope.
  • The Avalonia FocusManager keeps track of the last focused element in each focus scope.
  • Common commands (Cut, Copy, Paste, etc.) are implemented as RoutedCommands. Similar to WPF's ApplicationCommands.
  • When a command in Menu, ToolBar, or "Quick Launch Box" is triggered it needs to be routed to the previously focused element. This is done by determining the current focus scope (FocusManager.Scope), finding the parent focus scope, and routing the event to the last focused element (FocusManager.GetFocusedElement(IFocusScope)).
  • After the command is executed the toolbar item or "Quick Launch Box" needs to give up the focus and move it back to the previously focused element.

To Reproduce
Third-party apps or UI control libraries can no longer access FocusManager. The focus scope API is not exposed in IFocusManager.

Expected behavior
Third-party apps or UI control libraries should have a way to access focus scopes.

Solution
This can be solved by pulling the following members from FocusManager into IFocusManager:

/// <summary>
/// Gets the current focus scope.
/// </summary>
IFocusScope? Scope { get; }

/// <summary>
/// Gets the currently focused element in the given focus scope.
/// </summary>
public IInputElement? GetFocusedElement(IFocusScope scope);

Focus scopes are an important concept needed for command routing in complex scenarios and for porting WPF applications to Avalonia UI. I strongly recommend keeping the functionality and not removing it as part of any future changes (e.g. #7607).

Screenshots
n.a.

Desktop (please complete the following information):

  • OS: Windows
  • Version 11.0.0-rc1.1

Additional context
n.a.

@mgarstenauer
Copy link
Contributor Author

I could provide a PR, however, I don't want to interfere with any ongoing focus manager work. (Also, I am open for alternative solutions.)

@maxkatz6 maxkatz6 added enhancement and removed bug labels Jun 1, 2023
@maxkatz6
Copy link
Member

maxkatz6 commented Jun 1, 2023

Third-party apps or UI control libraries can no longer access FocusManager

They can, but you are right - there are no focus scopes in public API anymore.
(Window|TopLevel).FocusManager.GetFocusedElement()

I strongly recommend keeping the functionality and not removing it as part of any future changes (e.g. #7607).

There is a high chance of removing custom focus scopes (i.e. implementation of IFocusScope), as there is no WPF/UWP. FocusManager.IsFocusScope="True" is a possible alternative though.
But focus scopes are likely to be kept at least as an internal concept.

Windows, Menus, ToolBarTrays and the "Quick Launch Box" implement IFocusScope.

While generally, TopLevel has aspects of IFocusScope, I don't see how it can be used on Menu. But I guess I am missing some points about routed commands.

When a command in Menu, ToolBar, or "Quick Launch Box" is triggered it needs to be routed to the previously focused element.

I expect it should be triggered from the currently focused element up to the visual tree. Am I wrong?

This is done by determining the current focus scope (FocusManager.Scope), finding the parent focus scope

So, do I understand it correctly, you record focus scope on menu navigation, save it, and use it when any nested command is raised which should propagate from the upper focus scope up the tree?

@maxkatz6
Copy link
Member

maxkatz6 commented Jun 1, 2023

Either way, it doesn't sound like something that can't be done with existing API without making it more complex:

  • it's possible to get the currently focused element
  • it's possible to track focus movements globally, i.e. static GotFocus.Raised.Subscribe()

But I might be missing something as well.

@mgarstenauer
Copy link
Contributor Author

True, using GotFocus.Raised.Subscribe() I can re-implement the focus tracking myself. I'd be duplicating some FocusManager code but that's acceptable. (Should you decide to remove IFocusScope then I'll add an MyFocusManager.IsFocusScope attached property as suggested.)


Just to clarify the questions above:

While generally, TopLevel has aspects of IFocusScope, I don't see how it can be used on Menu.

MenuBase already implements IFocusScope and therefore works for routed commands out-of-the-box. ;)

I expect it should be triggered from the currently focused element up to the visual tree. Am I wrong?

Say you have a Window with a main Menu and a TextBox.

  1. You click the TextBox. The TextBox is now the focused element in the parent focus scope (Window).
  2. Then you click the click the menu item Edit | Paste. MenuItems are focusable and therefore receive focus when clicked! The MenuItem is now the focused element in the parent focus scope (Menu) and the active focus element.
  3. When we route the Paste command from the MenuItem up the visual tree to the window, then it never reaches the TextBox because it is not between MenuItem and Window in the hierarchy. So instead, we route the command up the visual tree until we reach the parent focus scope (Menu).
  4. When we reach the Menu, we get its parent focus scope (Window) and get the last focused element in this focus scope (TextBox). We raise the command again on the TextBox.

That's generally how the routed commands work in WPF and in my Avalonia app.

So, do I understand it correctly, you record focus scope on menu navigation, save it, and use it when any nested command is raised which should propagate from the upper focus scope up the tree?

Yes, except that currently I don't have to manually record anything. The Avalonia FocusManager already tracks everything as needed. I just need to access it. (But re-implementing parts of the FocusManager in my UI library is fine.)

@mgarstenauer
Copy link
Contributor Author

Closing the issue for now. Will open another issue if I hit any blockers.

@kekekeks
Copy link
Member

kekekeks commented Jun 2, 2023

IIRC there were hard to resolve issues with the way focus scopes are implemented. There are some plans to have a full WPF-like implementation, but no ETA yet.

@mgarstenauer
Copy link
Contributor Author

No worries, I am making headway using my own implementation – manually tracking focus and focus scopes as @maxkatz6 suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants