Skip to content

Commit

Permalink
feat: add screen reader support to Win32 toast notifications (#13834)
Browse files Browse the repository at this point in the history
  • Loading branch information
miniak authored and MarshallOfSound committed Sep 12, 2018
1 parent 2cd03bf commit 932f6c8
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 0 deletions.
2 changes: 2 additions & 0 deletions brightray/BUILD.gn
Expand Up @@ -103,6 +103,8 @@ static_library("brightray") {
"browser/win/win32_desktop_notifications/common.h",
"browser/win/win32_desktop_notifications/desktop_notification_controller.cc",
"browser/win/win32_desktop_notifications/desktop_notification_controller.h",
"browser/win/win32_desktop_notifications/toast_uia.cc",
"browser/win/win32_desktop_notifications/toast_uia.h",
"browser/win/win32_desktop_notifications/toast.cc",
"browser/win/win32_desktop_notifications/toast.h",
"browser/win/win32_notification.cc",
Expand Down
43 changes: 43 additions & 0 deletions brightray/browser/win/win32_desktop_notifications/toast.cc
Expand Up @@ -2,11 +2,17 @@
#define NOMINMAX
#endif
#include "brightray/browser/win/win32_desktop_notifications/toast.h"

#include <combaseapi.h>

#include <UIAutomation.h>
#include <uxtheme.h>
#include <windowsx.h>
#include <algorithm>

#include "base/logging.h"
#include "brightray/browser/win/win32_desktop_notifications/common.h"
#include "brightray/browser/win/win32_desktop_notifications/toast_uia.h"

#pragma comment(lib, "msimg32.lib")
#pragma comment(lib, "uxtheme.lib")
Expand Down Expand Up @@ -196,6 +202,22 @@ DesktopNotificationController::Toast::Toast(HWND hwnd,
}

DesktopNotificationController::Toast::~Toast() {
if (uia_) {
auto* UiaDisconnectProvider =
reinterpret_cast<decltype(&::UiaDisconnectProvider)>(GetProcAddress(
GetModuleHandle(L"uiautomationcore.dll"), "UiaDisconnectProvider"));
// first detach from the toast, then call UiaDisconnectProvider;
// UiaDisconnectProvider may call WM_GETOBJECT and we don't want
// it to return the object that we're disconnecting
uia_->DetachToast();

if (UiaDisconnectProvider)
UiaDisconnectProvider(uia_);

uia_->Release();
uia_ = nullptr;
}

DeleteDC(hdc_);
if (bitmap_)
DeleteBitmap(bitmap_);
Expand Down Expand Up @@ -232,6 +254,13 @@ LRESULT DesktopNotificationController::Toast::WndProc(HWND hwnd,
SetWindowLongPtr(hwnd, 0, 0);
return 0;

case WM_DESTROY:
if (Get(hwnd)->uia_) {
// free UI Automation resources associated with this window
UiaReturnRawElementProvider(hwnd, 0, 0, nullptr);
}
break;

case WM_MOUSEACTIVATE:
return MA_NOACTIVATE;

Expand Down Expand Up @@ -295,6 +324,20 @@ LRESULT DesktopNotificationController::Toast::WndProc(HWND hwnd,
Get(hwnd)->is_highlighted_ = false;
}
} break;

case WM_GETOBJECT:
if (lparam == UiaRootObjectId) {
auto* inst = Get(hwnd);
if (!inst->uia_) {
inst->uia_ = new UIAutomationInterface(inst);
inst->uia_->AddRef();
}
// don't return the interface if it's being disconnected
if (!inst->uia_->IsDetached()) {
return UiaReturnRawElementProvider(hwnd, wparam, lparam, inst->uia_);
}
}
break;
}

return DefWindowProc(hwnd, message, wparam, lparam);
Expand Down
3 changes: 3 additions & 0 deletions brightray/browser/win/win32_desktop_notifications/toast.h
Expand Up @@ -70,6 +70,9 @@ class DesktopNotificationController::Toast {
HDC hdc_;
HBITMAP bitmap_ = NULL;

class UIAutomationInterface;
UIAutomationInterface* uia_ = nullptr;

const std::shared_ptr<NotificationData> data_; // never null

SIZE toast_size_ = {};
Expand Down
250 changes: 250 additions & 0 deletions brightray/browser/win/win32_desktop_notifications/toast_uia.cc
@@ -0,0 +1,250 @@
#include "brightray/browser/win/win32_desktop_notifications/toast_uia.h"
#include <UIAutomation.h>
#include "brightray/browser/win/win32_desktop_notifications/common.h"

#pragma comment(lib, "uiautomationcore.lib")

namespace brightray {

DesktopNotificationController::Toast::UIAutomationInterface::
UIAutomationInterface(Toast* toast)
: hwnd_(toast->hwnd_) {
text_ = toast->data_->caption;
if (!toast->data_->body_text.empty()) {
if (!text_.empty())
text_.append(L", ");
text_.append(toast->data_->body_text);
}
}

ULONG DesktopNotificationController::Toast::UIAutomationInterface::AddRef() {
return InterlockedIncrement(&cref_);
}

ULONG DesktopNotificationController::Toast::UIAutomationInterface::Release() {
LONG ret = InterlockedDecrement(&cref_);
if (ret == 0) {
delete this;
return 0;
}
_ASSERT(ret > 0);
return ret;
}

STDMETHODIMP
DesktopNotificationController::Toast::UIAutomationInterface::QueryInterface(
REFIID riid,
LPVOID* ppv) {
if (!ppv)
return E_INVALIDARG;

if (riid == IID_IUnknown) {
*ppv =
static_cast<IUnknown*>(static_cast<IRawElementProviderSimple*>(this));
} else if (riid == __uuidof(IRawElementProviderSimple)) {
*ppv = static_cast<IRawElementProviderSimple*>(this);
} else if (riid == __uuidof(IWindowProvider)) {
*ppv = static_cast<IWindowProvider*>(this);
} else if (riid == __uuidof(IInvokeProvider)) {
*ppv = static_cast<IInvokeProvider*>(this);
} else if (riid == __uuidof(ITextProvider)) {
*ppv = static_cast<ITextProvider*>(this);
} else {
*ppv = nullptr;
return E_NOINTERFACE;
}

this->AddRef();
return S_OK;
}

HRESULT DesktopNotificationController::Toast::UIAutomationInterface::
get_ProviderOptions(ProviderOptions* retval) {
*retval = ProviderOptions_ServerSideProvider;
return S_OK;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::GetPatternProvider(
PATTERNID pattern_id,
IUnknown** retval) {
switch (pattern_id) {
case UIA_WindowPatternId:
*retval = static_cast<IWindowProvider*>(this);
break;
case UIA_InvokePatternId:
*retval = static_cast<IInvokeProvider*>(this);
break;
case UIA_TextPatternId:
*retval = static_cast<ITextProvider*>(this);
break;
default:
*retval = nullptr;
return S_OK;
}
this->AddRef();
return S_OK;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::GetPropertyValue(
PROPERTYID property_id,
VARIANT* retval) {
// Note: In order to have the toast read by the NVDA screen reader, we
// pretend that we're a Windows 8 native toast notification by reporting
// these property values:
// ClassName: ToastContentHost
// ControlType: UIA_ToolTipControlTypeId

retval->vt = VT_EMPTY;
switch (property_id) {
case UIA_NamePropertyId:
retval->vt = VT_BSTR;
retval->bstrVal = SysAllocString(text_.c_str());
break;

case UIA_ClassNamePropertyId:
retval->vt = VT_BSTR;
retval->bstrVal = SysAllocString(L"ToastContentHost");
break;

case UIA_ControlTypePropertyId:
retval->vt = VT_I4;
retval->lVal = UIA_ToolTipControlTypeId;
break;

case UIA_LiveSettingPropertyId:
retval->vt = VT_I4;
retval->lVal = Assertive;
break;

case UIA_IsContentElementPropertyId:
case UIA_IsControlElementPropertyId:
case UIA_IsPeripheralPropertyId:
retval->vt = VT_BOOL;
retval->lVal = VARIANT_TRUE;
break;

case UIA_HasKeyboardFocusPropertyId:
case UIA_IsKeyboardFocusablePropertyId:
case UIA_IsOffscreenPropertyId:
retval->vt = VT_BOOL;
retval->lVal = VARIANT_FALSE;
break;
}
return S_OK;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::
get_HostRawElementProvider(IRawElementProviderSimple** retval) {
if (!hwnd_)
return E_FAIL;
return UiaHostProviderFromHwnd(hwnd_, retval);
}

HRESULT DesktopNotificationController::Toast::UIAutomationInterface::Invoke() {
return E_NOTIMPL;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::SetVisualState(
WindowVisualState state) {
// setting the visual state is not supported
return E_FAIL;
}

HRESULT DesktopNotificationController::Toast::UIAutomationInterface::Close() {
return E_NOTIMPL;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::WaitForInputIdle(
int milliseconds,
BOOL* retval) {
return E_NOTIMPL;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::get_CanMaximize(
BOOL* retval) {
*retval = FALSE;
return S_OK;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::get_CanMinimize(
BOOL* retval) {
*retval = FALSE;
return S_OK;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::get_IsModal(
BOOL* retval) {
*retval = FALSE;
return S_OK;
}

HRESULT DesktopNotificationController::Toast::UIAutomationInterface::
get_WindowVisualState(WindowVisualState* retval) {
*retval = WindowVisualState_Normal;
return S_OK;
}

HRESULT DesktopNotificationController::Toast::UIAutomationInterface::
get_WindowInteractionState(WindowInteractionState* retval) {
if (!hwnd_)
*retval = WindowInteractionState_Closing;
else
*retval = WindowInteractionState_ReadyForUserInteraction;

return S_OK;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::get_IsTopmost(
BOOL* retval) {
*retval = TRUE;
return S_OK;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::GetSelection(
SAFEARRAY** retval) {
return E_NOTIMPL;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::GetVisibleRanges(
SAFEARRAY** retval) {
return E_NOTIMPL;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::RangeFromChild(
IRawElementProviderSimple* child_element,
ITextRangeProvider** retval) {
return E_NOTIMPL;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::RangeFromPoint(
UiaPoint point,
ITextRangeProvider** retval) {
return E_NOTIMPL;
}

HRESULT
DesktopNotificationController::Toast::UIAutomationInterface::get_DocumentRange(
ITextRangeProvider** retval) {
return E_NOTIMPL;
}

HRESULT DesktopNotificationController::Toast::UIAutomationInterface::
get_SupportedTextSelection(SupportedTextSelection* retval) {
*retval = SupportedTextSelection_None;
return S_OK;
}

} // namespace brightray

0 comments on commit 932f6c8

Please sign in to comment.