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

JBR-4478 Implement support for native accessible caret events on Windows #456

Open
wants to merge 1 commit into
base: jbr21
Choose a base branch
from

Conversation

dmitrii-drobotov
Copy link
Member

The feature adds caret tracking support for assistive tools that don't work with Java Access Bridge, specifically, for the built-in Windows Magnifier. Caret tracking will be supported on a similar level to the macOS one added in #255. Here's a quick demo: https://github.com/user-attachments/assets/9c0ef675-364c-40ba-94af-63f98e6809aa

In short, it works by implementing Win32 IAccessible interface for the text caret and sending EVENT_OBJECT_LOCATIONCHANGE events whenever it changes, which is very close to the way Windows system caret behaves from the accessibility side. It's also somewhat similar to the way it's implemented in chromium, and to XInputMethod.ClientComponentCaretPositionTracker.

For the proper Magnifier support, we would need to implement UI Automation API, which is a modern accessibility API on Windows. I initially started implementing it, but it turned out to be quite complex, so I decided to go with this approach of implementing IAccessible API (which is legacy, but still well supported) only for the text caret.

The feature is enabled by default and can be disabled by setting sun.awt.windows.use.native.caret.accessibility.events property to false.

Comment on lines +261 to +268
// This is a workaround to make sure the foreground window is set to the actual focused window.
// Otherwise, in some cases, e.g., when opening a popup,
// the root frame can still stay as the foreground window instead of the popup,
// and Magnifier will be focused on it instead of the popup.
// We only do it if the caret object is actually used to minimize risks.
if (AccessibleCaret::isCaretUsed && ::GetForegroundWindow() != hwnd) {
::SetForegroundWindow(hwnd);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, this is how opening and closing popups work with this workaround: https://github.com/user-attachments/assets/a99211ec-41b8-4b60-bc2f-8a675a806d09, and without: https://github.com/user-attachments/assets/b62a9c16-f6a0-4fde-a5e7-f8b1d4dc7b31.
The current behavior for popups is the way it works for JFrames and JDialogs (e.g., Find in Files dialog) even without any changes, and now will also apply to popups. But it is somewhat a regression, because this is how popups worked without this PR: https://github.com/user-attachments/assets/fe8fa24e-e13c-4461-9408-75748fc81be7, so no viewport jumping at all. This is apparently affected by the presense of an accessible object that magnifier interacted with in a window, and a proper fix would probably require implementing hierarchy (parent and child methods of IAccessible) and location of accessible objects. I think we can keep it this way for now, and maybe improve later, because I don't think it's a major problem, and can also be completely avoided by turning off keyboard focus tracking in Magnifier settings.

Copy link
Member

@NikitkoCent NikitkoCent Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I don't feel like touching the focus subsystem here because it's a really fragile system. WDYT if we drop this part of the patch for now and will investigate under a separate ticket later how to fix it? Especially if taking into account, that this part is causing IJPL-162581.

Secondly, have you tested how it works after the magnifier gets closed? If I understand the current revision correctly, SetForegroundWindow will be called even after the magnifier is closed because isUsed isn't reset to false anywhere.

And finally, maybe we should just play with NotifyWinEvent here instead? Like to force the magnifier to re-ask all the information.

@dmitrii-drobotov dmitrii-drobotov force-pushed the ddrobotov/JBR-4478 branch 2 times, most recently from f386b63 to c46b34a Compare September 13, 2024 14:29
The feature adds caret tracking support for assistive tools that don't work with Java Access Bridge, specifically, for the built-in Windows Magnifier.
It works by implementing Win32 IAccessible interface for the text caret, and sending EVENT_OBJECT_LOCATIONCHANGE events whenever it changes.
It's enabled by default and can be disabled by setting `sun.awt.windows.use.native.caret.accessibility.events` property to false.
Copy link
Member

@NikitkoCent NikitkoCent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the patch! I have several topics to discuss

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2024, JetBrains s.r.o.. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Copyright (c) 2024, JetBrains s.r.o.. All rights reserved.
* Copyright (c) 2024, JetBrains s.r.o.. All rights reserved.

@@ -0,0 +1,270 @@
/*
* Copyright (c) 2024, JetBrains s.r.o.. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Copyright (c) 2024, JetBrains s.r.o.. All rights reserved.
* Copyright (c) 2024, JetBrains s.r.o.. All rights reserved.

: m_refCount(1), m_x(0), m_y(0), m_width(0), m_height(0) {
}

AccessibleCaret *AccessibleCaret::instance = NULL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use nullptr instead of NULL since we're using C++ :)


class AccessibleCaret : public IAccessible {
public:
AccessibleCaret();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the responsibilities point of view, the Release method now controls how exactly objects of this class should be destroyed, and there appears a little asymmetry - the class controls how to destroy its objects but doesn't have control over objects creation. In practice it also would lead to UB if someone created an object of the class not via the new expression and then invoked the Release method.
Thus I propose to get the class control over object creation via the following steps:

  • Make all the constructors private or protected ;
  • Instead, provide a public fabric method for objects creation ;
  • Make the destructor protected or private so that the only way to destroy an object is to call the Release() method.

Comment on lines +33 to +34
static AccessibleCaret *instance;
static bool isCaretUsed;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the class actually represents a singleton, let's restrict the access to its state via additional methods to avoid misusages in the future

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, as I see, the state is read and/or written from different threads (the Toolkit and Swing), so the changes to it have to be somehow guarded. Especially when the instance pointer gets destroyed and then set to nullptr.
I'd first try to design and implement API in a lock-free way (like on atomics), not sure if it's possible though.

The same for read/writes accesses to m_x, m_y, m_width, m_height.

return E_POINTER;
}
*ppdispParent = NULL;
return S_OK;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

pvarState->vt = VT_I4;
pvarState->lVal = 0; // The state without any flags, corresponds to "normal".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://learn.microsoft.com/en-us/windows/win32/winauto/caret, there also may be the state STATE_SYSTEM_INVISIBLE. I think it happens when the keyboard focus belongs to a non-text component (e.g. a button) or e.g. to a non-editable text component

IFACEMETHODIMP_(ULONG) AccessibleCaret::Release() {
ULONG count = InterlockedDecrement(&m_refCount);
if (count == 0) {
delete this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we add isCaretUsed = false; to here (or to the destructor)?

Comment on lines +261 to +268
// This is a workaround to make sure the foreground window is set to the actual focused window.
// Otherwise, in some cases, e.g., when opening a popup,
// the root frame can still stay as the foreground window instead of the popup,
// and Magnifier will be focused on it instead of the popup.
// We only do it if the caret object is actually used to minimize risks.
if (AccessibleCaret::isCaretUsed && ::GetForegroundWindow() != hwnd) {
::SetForegroundWindow(hwnd);
}
Copy link
Member

@NikitkoCent NikitkoCent Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I don't feel like touching the focus subsystem here because it's a really fragile system. WDYT if we drop this part of the patch for now and will investigate under a separate ticket later how to fix it? Especially if taking into account, that this part is causing IJPL-162581.

Secondly, have you tested how it works after the magnifier gets closed? If I understand the current revision correctly, SetForegroundWindow will be called even after the magnifier is closed because isUsed isn't reset to false anywhere.

And finally, maybe we should just play with NotifyWinEvent here instead? Like to force the magnifier to re-ask all the information.

Comment on lines +80 to +82
if (nativeCaretEventsEnabled && caretNotifier == null) {
SwingUtilities.invokeLater(() -> {
caretNotifier = new AccessibleCaretLocationNotifier(hwnd);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since caretNotifier is read and written from different threads, we should:

  1. Have memory barriers around the access operations
  2. Add additional check that caretNotifier is still null on the EDT

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

Successfully merging this pull request may close these issues.

2 participants