1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

Accessibility: Add AccessibilityHandler::postSystemNotification() function for posting an OS-specific accessible notification

This commit is contained in:
reuk 2024-11-21 21:26:55 +00:00
parent 6d10eb536f
commit 269ebbb525
No known key found for this signature in database
11 changed files with 308 additions and 149 deletions

View file

@ -35,8 +35,6 @@
namespace juce
{
AccessibilityHandler* AccessibilityHandler::currentlyFocusedHandler = nullptr;
class NativeChildHandler
{
public:
@ -403,10 +401,24 @@ void AccessibilityHandler::setNativeChildForComponent (Component& component, voi
NativeChildHandler::getInstance().setNativeChild (component, nativeChild);
}
#if JUCE_MODULE_AVAILABLE_juce_gui_extra
void privatePostSystemNotification (const String&, const String&);
#endif
void AccessibilityHandler::postSystemNotification ([[maybe_unused]] const String& notificationTitle,
[[maybe_unused]] const String& notificationBody)
{
#if JUCE_MODULE_AVAILABLE_juce_gui_extra
if (areAnyAccessibilityClientsActive())
privatePostSystemNotification (notificationTitle, notificationBody);
#endif
}
#if ! JUCE_NATIVE_ACCESSIBILITY_INCLUDED
void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent) const {}
void AccessibilityHandler::postAnnouncement (const String&, AnnouncementPriority) {}
AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const { return nullptr; }
bool AccessibilityHandler::areAnyAccessibilityClientsActive() { return false; }
#endif
} // namespace juce

View file

@ -295,6 +295,30 @@ public:
*/
static void postAnnouncement (const String& announcementString, AnnouncementPriority priority);
/** Posts a local system notification.
In order for this to do anything, the following conditions must be met.
- At build time:
- The juce_gui_extra module must be included in the project.
- Push notifications must be enabled by setting the preprocessor definition
JUCE_PUSH_NOTIFICATIONS=1
- At run time:
- An accessibility client (narrator, voiceover etc.) must be active.
Additionally, on Android, an icon is required for notifications.
This must be specified by adding the path to the icon file called
"accessibilitynotificationicon" in the "Extra Android Raw Resources" setting
in the Projucer.
This will use the push notification client on macOS, iOS and Android.
On Windows this will create a system tray icon to post the notification.
@param notificationTitle the title of the notification
@param notificationBody the main body text of the notification
*/
static void postSystemNotification (const String& notificationTitle,
const String& notificationBody);
//==============================================================================
/** @internal */
AccessibilityNativeHandle* getNativeImplementation() const;
@ -329,8 +353,9 @@ private:
void grabFocusInternal (bool);
void giveAwayFocusInternal() const;
void takeFocus();
static bool areAnyAccessibilityClientsActive();
static AccessibilityHandler* currentlyFocusedHandler;
static inline AccessibilityHandler* currentlyFocusedHandler = nullptr;
//==============================================================================
Component& component;

View file

@ -1055,4 +1055,9 @@ void AccessibilityHandler::postAnnouncement (const String& announcementString,
javaString (announcementString).get());
}
bool AccessibilityHandler::areAnyAccessibilityClientsActive()
{
return AccessibilityNativeHandle::areAnyAccessibilityClientsActive();
}
} // namespace juce

View file

@ -666,4 +666,9 @@ void AccessibilityHandler::postAnnouncement (const String& announcementString, A
sendAccessibilityEvent (UIAccessibilityAnnouncementNotification, juceStringToNS (announcementString));
}
bool AccessibilityHandler::areAnyAccessibilityClientsActive()
{
return juce::areAnyAccessibilityClientsActive();
}
} // namespace juce

View file

@ -939,4 +939,9 @@ void AccessibilityHandler::postAnnouncement (const String& announcementString, A
NSAccessibilityPriorityKey: @(nsPriority) });
}
bool AccessibilityHandler::areAnyAccessibilityClientsActive()
{
return juce::areAnyAccessibilityClientsActive();
}
} // namespace juce

View file

@ -39,26 +39,83 @@ namespace juce
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
static bool isStartingUpOrShuttingDown()
//==============================================================================
struct WindowsAccessibility
{
if (auto* app = JUCEApplicationBase::getInstance())
if (app->isInitialising())
WindowsAccessibility() = delete;
static long getUiaRootObjectId()
{
return static_cast<long> (UiaRootObjectId);
}
static bool handleWmGetObject (AccessibilityHandler* handler, WPARAM wParam, LPARAM lParam, LRESULT* res)
{
if (isStartingUpOrShuttingDown() || (handler == nullptr || ! isHandlerValid (*handler)))
return false;
if (auto* uiaWrapper = WindowsUIAWrapper::getInstance())
{
ComSmartPtr<IRawElementProviderSimple> provider;
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress()));
if (! uiaWrapper->isProviderDisconnecting (provider))
*res = uiaWrapper->returnRawElementProvider ((HWND) handler->getComponent().getWindowHandle(), wParam, lParam, provider);
return true;
}
if (auto* mm = MessageManager::getInstanceWithoutCreating())
if (mm->hasStopMessageBeenSent())
return true;
return false;
}
return false;
}
static void revokeUIAMapEntriesForWindow (HWND hwnd)
{
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
uiaWrapper->returnRawElementProvider (hwnd, 0, 0, nullptr);
}
static bool isHandlerValid (const AccessibilityHandler& handler)
{
if (auto* provider = handler.getNativeImplementation())
return provider->isElementValid();
static bool isStartingUpOrShuttingDown()
{
if (auto* app = JUCEApplicationBase::getInstance())
if (app->isInitialising())
return true;
return false;
}
if (auto* mm = MessageManager::getInstanceWithoutCreating())
if (mm->hasStopMessageBeenSent())
return true;
return false;
}
static bool isHandlerValid (const AccessibilityHandler& handler)
{
if (auto* provider = handler.getNativeImplementation())
return provider->isElementValid();
return false;
}
static bool areAnyAccessibilityClientsActive()
{
const auto areClientsListening = []
{
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
return uiaWrapper->clientsAreListening() != 0;
return false;
};
const auto isScreenReaderRunning = []
{
BOOL isRunning = FALSE;
SystemParametersInfo (SPI_GETSCREENREADER, 0, (PVOID) &isRunning, 0);
return isRunning != 0;
};
return areClientsListening() || isScreenReaderRunning();
}
};
//==============================================================================
class AccessibilityHandler::AccessibilityNativeImpl
@ -103,31 +160,12 @@ AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const
return nativeImpl->accessibilityElement;
}
static bool areAnyAccessibilityClientsActive()
{
const auto areClientsListening = []
{
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
return uiaWrapper->clientsAreListening() != 0;
return false;
};
const auto isScreenReaderRunning = []
{
BOOL isRunning = FALSE;
SystemParametersInfo (SPI_GETSCREENREADER, 0, (PVOID) &isRunning, 0);
return isRunning != 0;
};
return areClientsListening() || isScreenReaderRunning();
}
template <typename Callback>
void getProviderWithCheckedWrapper (const AccessibilityHandler& handler, Callback&& callback)
{
if (! areAnyAccessibilityClientsActive() || isStartingUpOrShuttingDown() || ! isHandlerValid (handler))
if (! WindowsAccessibility::areAnyAccessibilityClientsActive()
|| WindowsAccessibility::isStartingUpOrShuttingDown()
|| ! WindowsAccessibility::isHandlerValid (handler))
return;
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
@ -292,41 +330,11 @@ void AccessibilityHandler::postAnnouncement (const String& announcementString, A
}
}
//==============================================================================
namespace WindowsAccessibility
bool AccessibilityHandler::areAnyAccessibilityClientsActive()
{
static long getUiaRootObjectId()
{
return static_cast<long> (UiaRootObjectId);
}
static bool handleWmGetObject (AccessibilityHandler* handler, WPARAM wParam, LPARAM lParam, LRESULT* res)
{
if (isStartingUpOrShuttingDown() || (handler == nullptr || ! isHandlerValid (*handler)))
return false;
if (auto* uiaWrapper = WindowsUIAWrapper::getInstance())
{
ComSmartPtr<IRawElementProviderSimple> provider;
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress()));
if (! uiaWrapper->isProviderDisconnecting (provider))
*res = uiaWrapper->returnRawElementProvider ((HWND) handler->getComponent().getWindowHandle(), wParam, lParam, provider);
return true;
}
return false;
}
static void revokeUIAMapEntriesForWindow (HWND hwnd)
{
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
uiaWrapper->returnRawElementProvider (hwnd, 0, 0, nullptr);
}
return WindowsAccessibility::areAnyAccessibilityClientsActive();
}
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
} // namespace juce

View file

@ -36,10 +36,50 @@ namespace juce
{
//==============================================================================
#if ! JUCE_ANDROID && ! JUCE_IOS && ! JUCE_MAC
#if ! JUCE_PUSH_NOTIFICATIONS_IMPL
struct PushNotifications::Impl
{
explicit Impl (PushNotifications& o) : owner (o) {}
void requestPermissionsWithSettings (const Settings&) const
{
owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
}
void requestSettingsUsed() const
{
owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
}
bool areNotificationsEnabled() const { return false; }
void getDeliveredNotifications() const {}
void removeAllDeliveredNotifications() const {}
String getDeviceToken() const { return {}; }
void setupChannels (const Array<ChannelGroup>&, const Array<Channel>&) const {}
void getPendingLocalNotifications() const {}
void removeAllPendingLocalNotifications() const {}
void subscribeToTopic (const String&) const {}
void unsubscribeFromTopic (const String&) const {}
void sendLocalNotification (const Notification&) const {}
void removeDeliveredNotification (const String&) const {}
void removePendingLocalNotification (const String&) const {}
void sendUpstreamMessage (const String&,
const String&,
const String&,
const String&,
int,
const StringPairArray&) const {}
private:
PushNotifications& owner;
};
bool PushNotifications::Notification::isValid() const noexcept { return true; }
#endif
//==============================================================================
PushNotifications::Notification::Notification (const Notification& other)
: identifier (other.identifier),
title (other.title),
@ -82,9 +122,7 @@ PushNotifications::Notification::Notification (const Notification& other)
//==============================================================================
PushNotifications::PushNotifications()
#if JUCE_PUSH_NOTIFICATIONS
: pimpl (new Pimpl (*this))
#endif
: pimpl (new Impl (*this))
{
}
@ -93,128 +131,90 @@ PushNotifications::~PushNotifications() { clearSingletonInstance(); }
void PushNotifications::addListener (Listener* l) { listeners.add (l); }
void PushNotifications::removeListener (Listener* l) { listeners.remove (l); }
void PushNotifications::requestPermissionsWithSettings ([[maybe_unused]] const PushNotifications::Settings& settings)
void PushNotifications::requestPermissionsWithSettings (const Settings& settings)
{
#if JUCE_PUSH_NOTIFICATIONS && (JUCE_IOS || JUCE_MAC)
pimpl->requestPermissionsWithSettings (settings);
#else
listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
#endif
}
void PushNotifications::requestSettingsUsed()
{
#if JUCE_PUSH_NOTIFICATIONS && (JUCE_IOS || JUCE_MAC)
pimpl->requestSettingsUsed();
#else
listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
#endif
}
bool PushNotifications::areNotificationsEnabled() const
{
#if JUCE_PUSH_NOTIFICATIONS
return pimpl->areNotificationsEnabled();
#else
return false;
#endif
}
void PushNotifications::getDeliveredNotifications() const
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->getDeliveredNotifications();
#endif
}
void PushNotifications::removeAllDeliveredNotifications()
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->removeAllDeliveredNotifications();
#endif
}
String PushNotifications::getDeviceToken() const
{
#if JUCE_PUSH_NOTIFICATIONS
return pimpl->getDeviceToken();
#else
return {};
#endif
}
void PushNotifications::setupChannels ([[maybe_unused]] const Array<ChannelGroup>& groups, [[maybe_unused]] const Array<Channel>& channels)
void PushNotifications::setupChannels (const Array<ChannelGroup>& groups,
const Array<Channel>& channels)
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->setupChannels (groups, channels);
#endif
}
void PushNotifications::getPendingLocalNotifications() const
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->getPendingLocalNotifications();
#endif
}
void PushNotifications::removeAllPendingLocalNotifications()
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->removeAllPendingLocalNotifications();
#endif
}
void PushNotifications::subscribeToTopic ([[maybe_unused]] const String& topic)
void PushNotifications::subscribeToTopic (const String& topic)
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->subscribeToTopic (topic);
#endif
}
void PushNotifications::unsubscribeFromTopic ([[maybe_unused]] const String& topic)
void PushNotifications::unsubscribeFromTopic (const String& topic)
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->unsubscribeFromTopic (topic);
#endif
}
void PushNotifications::sendLocalNotification ([[maybe_unused]] const Notification& n)
void PushNotifications::sendLocalNotification (const Notification& n)
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->sendLocalNotification (n);
#endif
}
void PushNotifications::removeDeliveredNotification ([[maybe_unused]] const String& identifier)
void PushNotifications::removeDeliveredNotification (const String& identifier)
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->removeDeliveredNotification (identifier);
#endif
}
void PushNotifications::removePendingLocalNotification ([[maybe_unused]] const String& identifier)
void PushNotifications::removePendingLocalNotification (const String& identifier)
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->removePendingLocalNotification (identifier);
#endif
}
void PushNotifications::sendUpstreamMessage ([[maybe_unused]] const String& serverSenderId,
[[maybe_unused]] const String& collapseKey,
[[maybe_unused]] const String& messageId,
[[maybe_unused]] const String& messageType,
[[maybe_unused]] int timeToLive,
[[maybe_unused]] const StringPairArray& additionalData)
void PushNotifications::sendUpstreamMessage (const String& serverSenderId,
const String& collapseKey,
const String& messageId,
const String& messageType,
int timeToLive,
const StringPairArray& additionalData)
{
#if JUCE_PUSH_NOTIFICATIONS
pimpl->sendUpstreamMessage (serverSenderId,
collapseKey,
messageId,
messageType,
timeToLive,
additionalData);
#endif
}
//==============================================================================
@ -234,4 +234,92 @@ void PushNotifications::Listener::upstreamMessageSent ([[maybe_unused]] const St
void PushNotifications::Listener::upstreamMessageSendingError ([[maybe_unused]] const String& messageId,
[[maybe_unused]] const String& error) {}
//==============================================================================
void privatePostSystemNotification (const String& notificationTitle, const String& notificationBody);
void privatePostSystemNotification ([[maybe_unused]] const String& notificationTitle,
[[maybe_unused]] const String& notificationBody)
{
#if JUCE_PUSH_NOTIFICATIONS
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
auto* notificationsInstance = PushNotifications::getInstance();
if (notificationsInstance == nullptr)
return;
#if JUCE_ANDROID
notificationsInstance->requestPermissionsWithSettings ({});
static auto channels = std::invoke ([]() -> Array<PushNotifications::Channel>
{
PushNotifications::Channel chan;
chan.identifier = "1";
chan.name = "Notifications";
chan.description = "Accessibility notifications";
chan.groupId = "accessibility";
chan.ledColour = Colours::yellow;
chan.canShowBadge = true;
chan.enableLights = true;
chan.enableVibration = true;
chan.soundToPlay = URL ("default_os_sound");
chan.vibrationPattern = { 1000, 1000 };
return { chan };
});
notificationsInstance->setupChannels ({ PushNotifications::ChannelGroup { "accessibility", "accessibility" } },
channels);
#else
static auto settings = std::invoke ([]
{
PushNotifications::Settings s;
s.allowAlert = true;
s.allowBadge = true;
s.allowSound = true;
#if JUCE_IOS
PushNotifications::Settings::Category c;
c.identifier = "Accessibility";
s.categories = { c };
#endif
return s;
});
notificationsInstance->requestPermissionsWithSettings (settings);
#endif
const auto notification = std::invoke ([&notificationTitle, &notificationBody]
{
PushNotifications::Notification n;
n.identifier = String (Random::getSystemRandom().nextInt());
n.title = notificationTitle;
n.body = notificationBody;
#if JUCE_IOS
n.category = "Accessibility";
#elif JUCE_ANDROID
n.channelId = "1";
n.icon = "accessibilitynotificationicon";
#endif
return n;
});
if (notification.isValid())
notificationsInstance->sendLocalNotification (notification);
#else
SystemTrayIconComponent systemTrayIcon;
Image im (Image::ARGB, 128, 128, true);
systemTrayIcon.setIconImage (im, im);
systemTrayIcon.showInfoBubble (notificationTitle, notificationBody);
#endif
#endif
}
} // namespace juce

View file

@ -403,8 +403,8 @@ public:
*/
struct Category
{
juce::String identifier; /**< unique identifier */
juce::Array<Action> actions; /**< optional list of actions within this category */
String identifier; /**< unique identifier */
Array<Action> actions; /**< optional list of actions within this category */
bool sendDismissAction = false; /**< whether dismiss action will be sent to the app */
};
@ -703,12 +703,8 @@ private:
friend struct JuceFirebaseMessagingService;
#endif
#if JUCE_PUSH_NOTIFICATIONS
struct Pimpl;
friend struct Pimpl;
std::unique_ptr<Pimpl> pimpl;
#endif
struct Impl;
std::unique_ptr<Impl> pimpl;
};
} // namespace juce

View file

@ -35,6 +35,8 @@
namespace juce
{
#define JUCE_PUSH_NOTIFICATIONS_IMPL 1
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (constructor, "<init>", "(Ljava/lang/String;Ljava/lang/CharSequence;I)V") \
METHOD (enableLights, "enableLights", "(Z)V") \
@ -274,9 +276,9 @@ bool PushNotifications::Notification::isValid() const noexcept
}
//==============================================================================
struct PushNotifications::Pimpl
struct PushNotifications::Impl
{
explicit Pimpl (PushNotifications& p)
explicit Impl (PushNotifications& p)
: owner (p)
{}
@ -306,7 +308,7 @@ struct PushNotifications::Pimpl
const auto notifyListeners = []
{
if (auto* instance = PushNotifications::getInstance())
instance->listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
instance->listeners.call ([] (Listener& l) { l.notificationSettingsReceived (makeDefaultSettings()); });
};
if (MessageManager::getInstance()->isThisTheMessageThread())
@ -318,7 +320,7 @@ struct PushNotifications::Pimpl
void requestSettingsUsed()
{
owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived (makeDefaultSettings()); });
}
void sendLocalNotification (const Notification& n)
@ -1573,6 +1575,15 @@ struct PushNotifications::Pimpl
&& env->CallBooleanMethod (extras, AndroidBundle.containsKey, javaString ("google.message_id").get());
}
static Settings makeDefaultSettings()
{
Settings settings;
settings.allowAlert = true;
settings.allowBadge = true;
settings.allowSound = true;
return settings;
}
PushNotifications& owner;
};
@ -1640,14 +1651,14 @@ bool juce_handleNotificationIntent (void* intent)
{
auto* instance = PushNotifications::getInstanceWithoutCreating();
if (PushNotifications::Pimpl::isDeleteNotificationIntent ((jobject) intent))
if (PushNotifications::Impl::isDeleteNotificationIntent ((jobject) intent))
{
if (instance)
instance->pimpl->notifyListenersAboutLocalNotificationDeleted (LocalRef<jobject> ((jobject) intent));
return true;
}
else if (PushNotifications::Pimpl::isLocalNotificationIntent ((jobject) intent))
else if (PushNotifications::Impl::isLocalNotificationIntent ((jobject) intent))
{
if (instance)
instance->pimpl->notifyListenersAboutLocalNotification (LocalRef<jobject> ((jobject) intent));
@ -1655,7 +1666,7 @@ bool juce_handleNotificationIntent (void* intent)
return true;
}
#if defined (JUCE_FIREBASE_MESSAGING_SERVICE_CLASSNAME)
else if (PushNotifications::Pimpl::isRemoteNotificationIntent ((jobject) intent))
else if (PushNotifications::Impl::isRemoteNotificationIntent ((jobject) intent))
{
if (instance)
instance->pimpl->notifyListenersAboutRemoteNotificationFromSystemTray (LocalRef<jobject> ((jobject) intent));

View file

@ -35,6 +35,8 @@
namespace juce
{
#define JUCE_PUSH_NOTIFICATIONS_IMPL 1
struct PushNotificationsDelegateDetails
{
//==============================================================================
@ -301,9 +303,9 @@ bool PushNotifications::Notification::isValid() const noexcept
}
//==============================================================================
struct PushNotifications::Pimpl
struct PushNotifications::Impl
{
Pimpl (PushNotifications& p)
explicit Impl (PushNotifications& p)
: owner (p)
{
Class::setThis (delegate.get(), this);
@ -555,7 +557,7 @@ private:
Class()
: ObjCClass ("JucePushNotificationsDelegate_")
{
addIvar<Pimpl*> ("self");
addIvar<Impl*> ("self");
addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), [] (id self, SEL, UIApplication*, NSData* data)
{
@ -596,8 +598,8 @@ private:
}
//==============================================================================
static Pimpl& getThis (id self) { return *getIvar<Pimpl*> (self, "self"); }
static void setThis (id self, Pimpl* d) { object_setInstanceVariable (self, "self", d); }
static Impl& getThis (id self) { return *getIvar<Impl*> (self, "self"); }
static void setThis (id self, Impl* d) { object_setInstanceVariable (self, "self", d); }
};
//==============================================================================

View file

@ -35,6 +35,8 @@
namespace juce
{
#define JUCE_PUSH_NOTIFICATIONS_IMPL 1
JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS
namespace PushNotificationsDelegateDetailsOsx
@ -343,9 +345,9 @@ private:
bool PushNotifications::Notification::isValid() const noexcept { return true; }
//==============================================================================
struct PushNotifications::Pimpl : private PushNotificationsDelegate
struct PushNotifications::Impl : private PushNotificationsDelegate
{
Pimpl (PushNotifications& p)
explicit Impl (PushNotifications& p)
: owner (p)
{
}