-
Notifications
You must be signed in to change notification settings - Fork 200
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
base: jbr21
Are you sure you want to change the base?
JBR-4478 Implement support for native accessible caret events on Windows #456
Conversation
// 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); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
8131fb5
to
c647762
Compare
src/java.desktop/windows/classes/sun/awt/windows/AccessibleCaretLocationNotifier.java
Show resolved
Hide resolved
f386b63
to
c46b34a
Compare
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.
There was a problem hiding this 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
orprotected
; - Instead, provide a public fabric method for objects creation ;
- Make the destructor
protected
orprivate
so that the only way to destroy an object is to call theRelease()
method.
static AccessibleCaret *instance; | ||
static bool isCaretUsed; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
return S_OK; | |
return S_FALSE; |
?
} | ||
|
||
pvarState->vt = VT_I4; | ||
pvarState->lVal = 0; // The state without any flags, corresponds to "normal". |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)?
// 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); | ||
} |
There was a problem hiding this comment.
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.
if (nativeCaretEventsEnabled && caretNotifier == null) { | ||
SwingUtilities.invokeLater(() -> { | ||
caretNotifier = new AccessibleCaretLocationNotifier(hwnd); |
There was a problem hiding this comment.
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:
- Have memory barriers around the access operations
- Add additional check that
caretNotifier
is stillnull
on the EDT
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.