Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
29aa8c9
Update the AccessibilityPlugin::Announce method to account for the view
mattkae Jul 24, 2025
6d98c0a
Various bits of PR feedback
mattkae Jul 25, 2025
0de9a8e
Missed a spot!
mattkae Jul 25, 2025
6e9a70d
Formatting
mattkae Jul 25, 2025
d62d1eb
Remove erroneous viewId accessor
mattkae Jul 25, 2025
118d90a
PR feedback
mattkae Jul 25, 2025
d1d01ef
Remove erroneous return statement
mattkae Jul 25, 2025
d22a5fd
Minor nit on comment spacing
mattkae Jul 28, 2025
a19e5b3
Merge branch 'master' into announce-event-multi-view
mattkae Jul 28, 2025
802bfab
Merge branch 'master' into announce-event-multi-view
mattkae Jul 31, 2025
4ec6112
Refactoring SemanticsService.announce so that it defaults to sending …
mattkae Aug 4, 2025
5f658d9
Merge branch 'master' into announce-event-multi-view
mattkae Aug 4, 2025
b2e94ec
Remove unnecessary null initializer from AnnounceSemanticsEvent
mattkae Aug 4, 2025
b11f8dc
Updating an erroneous client
mattkae Aug 4, 2025
39a0cc9
Merge branch 'announce-event-multi-view' of github.com:canonical/flut…
mattkae Aug 4, 2025
eb6030f
Revert "Updating an erroneous client"
mattkae Aug 15, 2025
bd64992
Revert "Remove unnecessary null initializer from AnnounceSemanticsEvent"
mattkae Aug 15, 2025
9411e54
Revert "Refactoring SemanticsService.announce so that it defaults to …
mattkae Aug 15, 2025
89b7b26
Update per the current understanding
mattkae Aug 15, 2025
97da60b
Update flutter_test tets
mattkae Aug 15, 2025
af94bf8
Merge branch 'master' into announce-event-multi-view
mattkae Aug 15, 2025
99eac77
Pull request feedback
mattkae Aug 15, 2025
06ccc12
Merge branch 'announce-event-multi-view' of github.com:canonical/flut…
mattkae Aug 15, 2025
5e35a77
Fix Deprecation notice
mattkae Aug 15, 2025
b5f5c08
Using viewId in semantic event
mattkae Aug 15, 2025
832464d
Further pull request feedback
mattkae Aug 19, 2025
0900f69
Ensuring that the viewId key is present in the announce event
mattkae Aug 19, 2025
7feec64
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
cacccdd
Minor comment nit
mattkae Aug 19, 2025
2385e37
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
3c4be84
Avoid using 'you' in docs
mattkae Aug 19, 2025
2989524
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
04f3ea0
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion dev/integration_tests/new_gallery/lib/pages/backdrop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ class _SettingsIcon extends AnimatedWidget {
child: InkWell(
onTap: () {
toggleSettings();
SemanticsService.announce(
SemanticsService.sendAnnouncement(
View.of(context),
_settingsSemanticLabel(isSettingsOpenNotifier.value, context),
GalleryOptions.of(context).resolvedTextDirection()!,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ static constexpr char kAccessibilityChannelName[] = "flutter/accessibility";
static constexpr char kTypeKey[] = "type";
static constexpr char kDataKey[] = "data";
static constexpr char kMessageKey[] = "message";
static constexpr char kViewIdKey[] = "viewId";
static constexpr char kAnnounceValue[] = "announce";

// Handles messages like:
Expand Down Expand Up @@ -61,7 +62,20 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) {
return;
}

plugin->Announce(*message);
const auto& view_itr = data->find(EncodableValue{kViewIdKey});
Comment thread
mattkae marked this conversation as resolved.
if (view_itr == data->end()) {
FML_LOG(ERROR) << "Announce message 'viewId' property is missing.";
return;
Comment thread
loic-sharma marked this conversation as resolved.
}

const auto* view_id_val = std::get_if<FlutterViewId>(&view_itr->second);
if (!view_id_val) {
FML_LOG(ERROR)
<< "Announce message 'viewId' property must be a FlutterViewId.";
return;
}

plugin->Announce(*view_id_val, *message);
} else {
FML_LOG(WARNING) << "Accessibility message type '" << *type
<< "' is not supported.";
Expand Down Expand Up @@ -89,14 +103,13 @@ void AccessibilityPlugin::SetUp(BinaryMessenger* binary_messenger,
});
}

void AccessibilityPlugin::Announce(const std::string_view message) {
void AccessibilityPlugin::Announce(const FlutterViewId view_id,
const std::string_view message) {
if (!engine_->semantics_enabled()) {
return;
}

// TODO(loicsharma): Remove implicit view assumption.
// https://github.com/flutter/flutter/issues/142845
auto view = engine_->view(kImplicitViewId);
auto view = engine_->view(view_id);
if (!view) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace flutter {

using FlutterViewId = int64_t;
class FlutterWindowsEngine;

// Handles messages on the flutter/accessibility channel.
Expand All @@ -27,7 +28,8 @@ class AccessibilityPlugin {
AccessibilityPlugin* plugin);

// Announce a message through the assistive technology.
virtual void Announce(const std::string_view message);
virtual void Announce(const FlutterViewId view_id,
const std::string_view message);

private:
// The engine that owns this plugin.
Expand Down
12 changes: 8 additions & 4 deletions engine/src/flutter/shell/platform/windows/fixtures/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ Future<void> sendAccessibilityAnnouncement() async {

// Standard message codec magic number identifiers.
// See: https://github.com/flutter/flutter/blob/ee94fe262b63b0761e8e1f889ae52322fef068d2/packages/flutter/lib/src/services/message_codecs.dart#L262
const int valueMap = 13, valueString = 7;
const int valueMap = 13, valueString = 7, valueInt64 = 4;

// Corresponds to: {"type": "announce", "data": {"message": "hello"}}
// Corresponds to: {"type": "announce", "data": {"viewId": 0, "message": "hello"}}
// See: https://github.com/flutter/flutter/blob/b781da9b5822de1461a769c3b245075359f5464d/packages/flutter/lib/src/semantics/semantics_event.dart#L86
final Uint8List data = Uint8List.fromList([
// Map with 2 entries
Expand All @@ -73,8 +73,12 @@ Future<void> sendAccessibilityAnnouncement() async {
valueString, 'announce'.length, ...'announce'.codeUnits,
// Map key: "data"
valueString, 'data'.length, ...'data'.codeUnits,
// Map value: map with 1 entry
valueMap, 1,
// Map value: map with 2 entries
valueMap, 2,
// Map key: "viewId"
valueString, 'viewId'.length, ...'viewId'.codeUnits,
// Map value: 0
valueInt64, 0, 0, 0, 0, 0, 0, 0, 0,
// Map key: "message"
valueString, 'message'.length, ...'message'.codeUnits,
// Map value: "hello"
Expand Down
11 changes: 7 additions & 4 deletions packages/flutter/lib/src/material/calendar_date_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
_announcedInitialDate = true;
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate);
final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : '';
SemanticsService.announce(
SemanticsService.sendAnnouncement(
View.of(context),
'${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix',
_textDirection,
);
Expand Down Expand Up @@ -265,7 +266,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations),
DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations),
};
SemanticsService.announce(message, _textDirection);
SemanticsService.sendAnnouncement(View.of(context), message, _textDirection);
}
});
}
Expand Down Expand Up @@ -315,7 +316,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
case TargetPlatform.windows:
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate);
final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : '';
SemanticsService.announce(
SemanticsService.sendAnnouncement(
View.of(context),
'${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix',
_textDirection,
);
Expand Down Expand Up @@ -665,7 +667,8 @@ class _MonthPickerState extends State<_MonthPicker> {
// the same day of the month.
_focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day);
}
SemanticsService.announce(
SemanticsService.sendAnnouncement(
View.of(context),
widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations),
_textDirection,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter/lib/src/material/expansion_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -538,12 +538,12 @@ class _ExpansionTileState extends State<ExpansionTile> {
// semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101.
_timer?.cancel();
_timer = Timer(const Duration(seconds: 1), () {
SemanticsService.announce(stateHint, textDirection);
SemanticsService.sendAnnouncement(View.of(context), stateHint, textDirection);
_timer?.cancel();
_timer = null;
});
} else {
SemanticsService.announce(stateHint, textDirection);
SemanticsService.sendAnnouncement(View.of(context), stateHint, textDirection);
}
widget.onExpansionChanged?.call(_tileController.isExpanded);
}
Expand Down
10 changes: 8 additions & 2 deletions packages/flutter/lib/src/semantics/semantics_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,18 @@ abstract class SemanticsEvent {
/// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence)
///
class AnnounceSemanticsEvent extends SemanticsEvent {
/// Constructs an event that triggers an announcement by the platform.
/// Constructs an event that triggers an announcement by the platform
/// for the provided view.
const AnnounceSemanticsEvent(
this.message,
this.textDirection, {
this.textDirection,
this.viewId, {
this.assertiveness = Assertiveness.polite,
}) : super('announce');

/// The view that this announcement is on.
final int viewId;

/// The message to announce.
final String message;

Expand All @@ -117,6 +122,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent {
@override
Map<String, dynamic> getDataMap() {
return <String, dynamic>{
'viewId': viewId,
'message': message,
'textDirection': textDirection.index,
if (assertiveness != Assertiveness.polite) 'assertiveness': assertiveness.index,
Expand Down
38 changes: 37 additions & 1 deletion packages/flutter/lib/src/semantics/semantics_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// @docImport 'package:flutter/widgets.dart';
library;

import 'dart:ui' show TextDirection;
import 'dart:ui' show FlutterView, PlatformDispatcher, TextDirection;

import 'package:flutter/services.dart' show SystemChannels;

Expand All @@ -23,6 +23,9 @@ export 'dart:ui' show TextDirection;
abstract final class SemanticsService {
/// Sends a semantic announcement.
///
/// This method is deprecated. Prefer using [sendAnnouncement] instead.
///
/// {@template flutter.semantics.service.announce}
/// This should be used for announcement that are not seamlessly announced by
/// the system as a result of a UI state change.
///
Expand All @@ -43,15 +46,48 @@ abstract final class SemanticsService {
/// trigger announcements.
///
/// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence)
/// {@endtemplate}
///
@Deprecated(
'Use sendAnnouncement instead. '
'This API is incompatible with multiple windows. '
'This feature was deprecated after v3.35.0-0.1.pre.',
)
static Future<void> announce(
String message,
TextDirection textDirection, {
Assertiveness assertiveness = Assertiveness.polite,
}) async {
final FlutterView? view = PlatformDispatcher.instance.implicitView;
assert(
view != null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

will the PlatformDispatcher.instance.implicitView be null in multiwindow app?

@loic-sharma loic-sharma Aug 19, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For now no, but in the future (likely at least 1 year from now) yes.

Currently, the runner app creates the initial window and gives it to Flutter to render into - this is the implicit view. This app with an implicit view can have multiple windows.

In the future - after Flutter supports multiple windows - Flutter will create all windows on desktop, including the initial window. Thus, there'll be no implicit view. Existing apps will need to migrate their native entry point to opt-in to this new behavior.

I expect that by the time the implicit view can be null, this API will have been deprecated long enough that it'll be reasonable for us to trigger this assert.

'SemanticsService.announce is incompatible with multiple windows. '
'Use SemanticsService.sendAnnouncement instead.',
);
final AnnounceSemanticsEvent event = AnnounceSemanticsEvent(
message,
textDirection,
view!.viewId,
assertiveness: assertiveness,
);
await SystemChannels.accessibility.send(event.toMap());
}

/// Sends a semantic announcement for a particular view.
///
/// One can use [View.of] to get the current [FlutterView].
///
Comment thread
mattkae marked this conversation as resolved.
/// {@macro flutter.semantics.service.announce}
static Future<void> sendAnnouncement(
Comment thread
loic-sharma marked this conversation as resolved.
FlutterView view,
String message,
TextDirection textDirection, {
Assertiveness assertiveness = Assertiveness.polite,
}) async {
final AnnounceSemanticsEvent event = AnnounceSemanticsEvent(
message,
textDirection,
view.viewId,
assertiveness: assertiveness,
);
await SystemChannels.accessibility.send(event.toMap());
Expand Down
18 changes: 11 additions & 7 deletions packages/flutter/lib/src/widgets/form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
library;

import 'dart:async';
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
Expand All @@ -22,6 +23,7 @@ import 'pop_scope.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
import 'routes.dart';
import 'view.dart';
import 'will_pop_scope.dart';

// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
Expand Down Expand Up @@ -271,10 +273,10 @@ class FormState extends State<Form> {
Widget build(BuildContext context) {
switch (widget.autovalidateMode) {
case AutovalidateMode.always:
_validate();
_validate(View.of(context));
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate();
_validate(View.of(context));
}
case AutovalidateMode.onUnfocus:
case AutovalidateMode.disabled:
Expand Down Expand Up @@ -335,7 +337,7 @@ class FormState extends State<Form> {
bool validate() {
_hasInteractedByUser = true;
_forceRebuild();
return _validate();
return _validate(View.of(context));
}

/// Validates every [FormField] that is a descendant of this [Form], and
Expand All @@ -352,11 +354,11 @@ class FormState extends State<Form> {
final Set<FormFieldState<Object?>> invalidFields = <FormFieldState<Object?>>{};
_hasInteractedByUser = true;
_forceRebuild();
_validate(invalidFields);
_validate(View.of(context), invalidFields);
return invalidFields;
}

bool _validate([Set<FormFieldState<Object?>>? invalidFields]) {
bool _validate(FlutterView view, [Set<FormFieldState<Object?>>? invalidFields]) {
bool hasError = false;
String errorMessage = '';
final bool validateOnFocusChange = widget.autovalidateMode == AutovalidateMode.onUnfocus;
Expand All @@ -383,15 +385,17 @@ class FormState extends State<Form> {
unawaited(
Future<void>(() async {
await Future<void>.delayed(_kIOSAnnouncementDelayDuration);
SemanticsService.announce(
SemanticsService.sendAnnouncement(
view,
errorMessage,
directionality,
assertiveness: Assertiveness.assertive,
);
}),
);
} else {
SemanticsService.announce(
SemanticsService.sendAnnouncement(
view,
errorMessage,
directionality,
assertiveness: Assertiveness.assertive,
Expand Down
14 changes: 10 additions & 4 deletions packages/flutter/test/semantics/semantics_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();

test('Semantic announcement', () async {
testWidgets('Semantic announcement', (WidgetTester tester) async {
final List<Map<dynamic, dynamic>> log = <Map<dynamic, dynamic>>[];

Future<dynamic> handleMessage(dynamic mockMessage) async {
Expand All @@ -20,8 +20,9 @@ void main() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage);

await SemanticsService.announce('announcement 1', TextDirection.ltr);
await SemanticsService.announce(
await SemanticsService.sendAnnouncement(tester.view, 'announcement 1', TextDirection.ltr);
await SemanticsService.sendAnnouncement(
tester.view,
'announcement 2',
TextDirection.rtl,
assertiveness: Assertiveness.assertive,
Expand All @@ -31,11 +32,16 @@ void main() {
equals(<Map<String, dynamic>>[
<String, dynamic>{
'type': 'announce',
'data': <String, dynamic>{'message': 'announcement 1', 'textDirection': 1},
'data': <String, dynamic>{
'viewId': tester.view.viewId,
'message': 'announcement 1',
'textDirection': 1,
},
},
<String, dynamic>{
'type': 'announce',
'data': <String, dynamic>{
'viewId': tester.view.viewId,
'message': 'announcement 2',
'textDirection': 0,
'assertiveness': 1,
Expand Down
Loading