From 4fc11db5cc50345b7d64cb4015eb9a92229f6384 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Thu, 29 Aug 2019 10:07:48 -0700 Subject: [PATCH] Add IterableFlagsProperty and use it on proxy box classes (#39354) * Add FlagsSummary and implement Listener --- .../lib/src/foundation/diagnostics.dart | 106 +++++++++++++++++- .../flutter/lib/src/rendering/proxy_box.dart | 46 ++++---- .../test/foundation/diagnostics_test.dart | 97 ++++++++++++++++ .../flutter/test/widgets/listener_test.dart | 44 ++++++++ .../test/widgets/mouse_region_test.dart | 39 +++++++ 5 files changed, 305 insertions(+), 27 deletions(-) diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart index a43bb1790a..5a04e7f788 100644 --- a/packages/flutter/lib/src/foundation/diagnostics.dart +++ b/packages/flutter/lib/src/foundation/diagnostics.dart @@ -2308,13 +2308,17 @@ class EnumProperty extends DiagnosticsProperty { /// omitted, that is taken to mean that [level] should be /// [DiagnosticLevel.hidden] when [value] is non-null or null respectively. /// -/// This kind of diagnostics property is typically used for values mostly opaque +/// This kind of diagnostics property is typically used for opaque /// values, like closures, where presenting the actual object is of dubious /// value but where reporting the presence or absence of the value is much more /// useful. /// /// See also: /// +/// +/// * [FlagsSummary], which provides similar functionality but accepts multiple +/// flags under the same name, and is preferred if there are multiple such +/// values that can fit into a same category (such as "listeners"). /// * [FlagProperty], which provides similar functionality describing whether /// a [value] is true or false. class ObjectFlagProperty extends DiagnosticsProperty { @@ -2415,6 +2419,106 @@ class ObjectFlagProperty extends DiagnosticsProperty { } } +/// A summary of multiple properties, indicating whether each of them is present +/// (non-null) or absent (null). +/// +/// Each entry of [value] is described by its key. The eventual description will +/// be a list of keys of non-null entries. +/// +/// The [ifEmpty] describes the entire collection of [value] when it contains no +/// non-null entries. If [ifEmpty] is omitted, [level] will be +/// [DiagnosticLevel.hidden] when [value] contains no non-null entries. +/// +/// This kind of diagnostics property is typically used for opaque +/// values, like closures, where presenting the actual object is of dubious +/// value but where reporting the presence or absence of the value is much more +/// useful. +/// +/// See also: +/// +/// * [ObjectFlagSummary], which provides similar functionality but accepts +/// only one flag, and is preferred if there is only one entry. +/// * [IterableProperty], which provides similar functionality describing +/// the values a collection of objects. +class FlagsSummary extends DiagnosticsProperty> { + /// Create a summary for multiple properties, indicating whether each of them + /// is present (non-null) or absent (null). + /// + /// The [value], [showName], [showSeparator] and [level] arguments must not be + /// null. + FlagsSummary( + String name, + Map value, { + String ifEmpty, + bool showName = true, + bool showSeparator = true, + DiagnosticLevel level = DiagnosticLevel.info, + }) : assert(value != null), + assert(showName != null), + assert(showSeparator != null), + assert(level != null), + super( + name, + value, + ifEmpty: ifEmpty, + showName: showName, + showSeparator: showSeparator, + level: level, + ); + + @override + String valueToString({TextTreeConfiguration parentConfiguration}) { + assert(value != null); + if (!_hasNonNullEntry() && ifEmpty != null) + return ifEmpty; + + final Iterable formattedValues = _formattedValues(); + if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) { + // Always display the value as a single line and enclose the iterable + // value in brackets to avoid ambiguity. + return '[${formattedValues.join(', ')}]'; + } + + return formattedValues.join(_isSingleLine(style) ? ', ' : '\n'); + } + + /// Priority level of the diagnostic used to control which diagnostics should + /// be shown and filtered. + /// + /// If [ifEmpty] is null and the [value] contains no non-null entries, then + /// level [DiagnosticLevel.hidden] is returned. + @override + DiagnosticLevel get level { + if (!_hasNonNullEntry() && ifEmpty == null) + return DiagnosticLevel.hidden; + return super.level; + } + + @override + Map toJsonMap(DiagnosticsSerializationDelegate delegate) { + final Map json = super.toJsonMap(delegate); + if (value.isNotEmpty) + json['values'] = _formattedValues().toList(); + return json; + } + + bool _hasNonNullEntry() => value.values.any((Object o) => o != null); + + // An iterable of each entry's description in [value]. + // + // For a non-null value, its description is its key. + // + // For a null value, it is omitted unless `includeEmtpy` is true and + // [ifEntryNull] contains a corresponding description. + Iterable _formattedValues() sync* { + for (MapEntry entry in value.entries) { + if (entry.value != null) { + yield entry.key; + } + } + } +} + /// Signature for computing the value of a property. /// /// May throw exception if accessing the property would throw an exception diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index a54c7d94f4..bc37669c56 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2575,21 +2575,17 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - final List listeners = []; - if (onPointerDown != null) - listeners.add('down'); - if (onPointerMove != null) - listeners.add('move'); - if (onPointerUp != null) - listeners.add('up'); - if (onPointerCancel != null) - listeners.add('cancel'); - if (onPointerSignal != null) - listeners.add('signal'); - if (listeners.isEmpty) - listeners.add(''); - properties.add(IterableProperty('listeners', listeners)); - // TODO(jacobr): add raw listeners to the diagnostics data. + properties.add(FlagsSummary( + 'listeners', + { + 'down': onPointerDown, + 'move': onPointerMove, + 'up': onPointerUp, + 'cancel': onPointerCancel, + 'signal': onPointerSignal, + }, + ifEmpty: '', + )); } } @@ -2773,17 +2769,15 @@ class RenderMouseRegion extends RenderProxyBox { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - final List listeners = []; - if (onEnter != null) - listeners.add('enter'); - if (onHover != null) - listeners.add('hover'); - if (onExit != null) - listeners.add('exit'); - if (listeners.isEmpty) - listeners.add(''); - properties.add(IterableProperty('listeners', listeners)); - // TODO(jacobr): add raw listeners to the diagnostics data. + properties.add(FlagsSummary( + 'listeners', + { + 'enter': onEnter, + 'hover': onHover, + 'exit': onExit, + }, + ifEmpty: '', + )); } } diff --git a/packages/flutter/test/foundation/diagnostics_test.dart b/packages/flutter/test/foundation/diagnostics_test.dart index 31d7607e38..3a1937d9bb 100644 --- a/packages/flutter/test/foundation/diagnostics_test.dart +++ b/packages/flutter/test/foundation/diagnostics_test.dart @@ -123,6 +123,21 @@ void validateObjectFlagPropertyJsonSerialization(ObjectFlagProperty prop validatePropertyJsonSerializationHelper(json, property); } +void validateIterableFlagsPropertyJsonSerialization(FlagsSummary property) { + final Map json = simulateJsonSerialization(property); + if (property.value.isNotEmpty) { + expect(json['values'], equals( + property.value.entries + .where((MapEntry entry) => entry.value != null) + .map((MapEntry entry) => entry.key).toList(), + )); + } else { + expect(json.containsKey('values'), isFalse); + } + + validatePropertyJsonSerializationHelper(json, property); +} + void validateIterablePropertyJsonSerialization(IterableProperty property) { final Map json = simulateJsonSerialization(property); if (property.value != null) { @@ -1601,6 +1616,88 @@ void main() { validateObjectFlagPropertyJsonSerialization(missing); }); + test('iterable flags property test', () { + // Normal property + { + final Function onClick = () { }; + final Function onMove = () { }; + final Map value = { + 'click': onClick, + 'move': onMove, + }; + final FlagsSummary flags = FlagsSummary( + 'listeners', + value, + ); + expect(flags.name, equals('listeners')); + expect(flags.value, equals(value)); + expect(flags.isFiltered(DiagnosticLevel.info), isFalse); + expect(flags.toString(), equals('listeners: click, move')); + validateIterableFlagsPropertyJsonSerialization(flags); + } + + // Reversed-order property + { + final Function onClick = () { }; + final Function onMove = () { }; + final Map value = { + 'move': onMove, + 'click': onClick, + }; + final FlagsSummary flags = FlagsSummary( + 'listeners', + value, + ); + expect(flags.toString(), equals('listeners: move, click')); + expect(flags.isFiltered(DiagnosticLevel.info), isFalse); + validateIterableFlagsPropertyJsonSerialization(flags); + } + + // Partially empty property + { + final Function onClick = () { }; + final Map value = { + 'move': null, + 'click': onClick, + }; + final FlagsSummary flags = FlagsSummary( + 'listeners', + value, + ); + expect(flags.toString(), equals('listeners: click')); + expect(flags.isFiltered(DiagnosticLevel.info), isFalse); + validateIterableFlagsPropertyJsonSerialization(flags); + } + + // Empty property (without ifEmpty) + { + final Map value = { + 'enter': null, + }; + final FlagsSummary flags = FlagsSummary( + 'listeners', + value, + ); + expect(flags.isFiltered(DiagnosticLevel.info), isTrue); + validateIterableFlagsPropertyJsonSerialization(flags); + } + + // Empty property (without ifEmpty) + { + final Map value = { + 'enter': null, + }; + final FlagsSummary flags = FlagsSummary( + 'listeners', + value, + ifEmpty: '', + ); + expect(flags.toString(), equals('listeners: ')); + expect(flags.isFiltered(DiagnosticLevel.info), isFalse); + validateIterableFlagsPropertyJsonSerialization(flags); + } + }); + test('iterable property test', () { final List ints = [1,2,3]; final IterableProperty intsProperty = IterableProperty( diff --git a/packages/flutter/test/widgets/listener_test.dart b/packages/flutter/test/widgets/listener_test.dart index 25aeec4029..1897ab6d91 100644 --- a/packages/flutter/test/widgets/listener_test.dart +++ b/packages/flutter/test/widgets/listener_test.dart @@ -353,6 +353,50 @@ void main() { expect(events.single.transform, expectedTransform); }); }); + + testWidgets('RenderPointerListener\'s debugFillProperties when default', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + RenderPointerListener().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'parentData: MISSING', + 'constraints: MISSING', + 'size: MISSING', + 'behavior: deferToChild', + 'listeners: ' + ]); + }); + + testWidgets('RenderPointerListener\'s debugFillProperties when full', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + RenderPointerListener( + onPointerDown: (PointerDownEvent event) {}, + onPointerUp: (PointerUpEvent event) {}, + onPointerMove: (PointerMoveEvent event) {}, + onPointerCancel: (PointerCancelEvent event) {}, + onPointerSignal: (PointerSignalEvent event) {}, + behavior: HitTestBehavior.opaque, + child: RenderErrorBox(), + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'parentData: MISSING', + 'constraints: MISSING', + 'size: MISSING', + 'behavior: opaque', + 'listeners: down, move, up, cancel, signal' + ]); + }); } Future scrollAt(Offset position, WidgetTester tester) { diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index 5d62bbffbd..dc57a011a5 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -689,6 +689,45 @@ void main() { await gesture.removePointer(); }); }); + + testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + RenderMouseRegion().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'parentData: MISSING', + 'constraints: MISSING', + 'size: MISSING', + 'listeners: ' + ]); + }); + + testWidgets('RenderMouseRegion\'s debugFillProperties when full', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + RenderMouseRegion( + onEnter: (PointerEnterEvent event) {}, + onExit: (PointerExitEvent event) {}, + onHover: (PointerHoverEvent event) {}, + child: RenderErrorBox(), + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'parentData: MISSING', + 'constraints: MISSING', + 'size: MISSING', + 'listeners: enter, hover, exit' + ]); + }); } // This widget allows you to send a callback that is called during `onPaint.