Skip to content

Commit 55fafd2

Browse files
authored
[Property Editor] Can filter properties in the Property Editor (#9022)
1 parent 0be2e1f commit 55fafd2

4 files changed

Lines changed: 179 additions & 14 deletions

File tree

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import '../../../shared/analytics/analytics.dart' as ga;
1111
import '../../../shared/analytics/constants.dart' as gac;
1212
import '../../../shared/editor/api_classes.dart';
1313
import '../../../shared/editor/editor_client.dart';
14+
import '../../../shared/ui/filter.dart';
1415
import '../../../shared/utils/utils.dart';
16+
import 'property_editor_types.dart';
1517

1618
typedef EditableWidgetData =
17-
({List<EditableArgument> args, String? name, String? documentation});
19+
({List<EditableProperty> properties, String? name, String? documentation});
1820

1921
typedef EditArgumentFunction =
2022
Future<EditArgumentResponse?> Function<T>({
@@ -23,7 +25,7 @@ typedef EditArgumentFunction =
2325
});
2426

2527
class PropertyEditorController extends DisposableController
26-
with AutoDisposeControllerMixin {
28+
with AutoDisposeControllerMixin, FilterControllerMixin<EditableProperty> {
2729
PropertyEditorController(this.editorClient) {
2830
init();
2931
}
@@ -61,6 +63,7 @@ class PropertyEditorController extends DisposableController
6163
_checkConnectionInterval,
6264
);
6365

66+
// Update in response to ActiveLocationChanged events.
6467
autoDisposeStreamSubscription(
6568
editorClient.activeLocationChangedStream.listen((event) async {
6669
if (_waitingForFirstEvent) _waitingForFirstEvent = false;
@@ -98,6 +101,17 @@ class PropertyEditorController extends DisposableController
98101
super.dispose();
99102
}
100103

104+
@override
105+
void filterData(Filter<EditableProperty> filter) {
106+
super.filterData(filter);
107+
final filtered = (_editableWidgetData.value?.properties ?? []).where(
108+
(property) => property.matchesQuery(filter.queryFilter.query),
109+
);
110+
filteredData
111+
..clear()
112+
..addAll(filtered);
113+
}
114+
101115
Future<EditArgumentResponse?> editArgument<T>({
102116
required String name,
103117
required T value,
@@ -124,13 +138,18 @@ class PropertyEditorController extends DisposableController
124138
textDocument: textDocument,
125139
position: cursorPosition,
126140
);
127-
final args = result?.args ?? <EditableArgument>[];
141+
final properties =
142+
(result?.args ?? <EditableArgument>[])
143+
.map(argToProperty)
144+
.nonNulls
145+
.toList();
128146
final name = result?.name;
129147
_editableWidgetData.value = (
130-
args: args,
148+
properties: properties,
131149
name: name,
132150
documentation: result?.documentation,
133151
);
152+
filterData(activeFilter.value);
134153
// Register impression.
135154
ga.impression(
136155
gaId,
@@ -154,9 +173,11 @@ class PropertyEditorController extends DisposableController
154173
TextDocument? document,
155174
CursorPosition? cursorPosition,
156175
}) {
176+
setActiveFilter();
157177
if (editableArgsResult != null) {
158178
_editableWidgetData.value = (
159-
args: editableArgsResult.args,
179+
properties:
180+
editableArgsResult.args.map(argToProperty).nonNulls.toList(),
160181
name: editableArgsResult.name,
161182
documentation: editableArgsResult.documentation,
162183
);
@@ -167,5 +188,6 @@ class PropertyEditorController extends DisposableController
167188
if (cursorPosition != null) {
168189
_currentCursorPosition = cursorPosition;
169190
}
191+
filterData(activeFilter.value);
170192
}
171193
}

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:devtools_app_shared/utils.dart';
77
import 'package:meta/meta.dart';
88

99
import '../../../shared/editor/api_classes.dart';
10+
import '../../../shared/primitives/utils.dart';
1011

1112
/// Record representing an option for an [EditableProperty].
1213
typedef PropertyOption = ({String text, bool isDefault});
@@ -167,6 +168,13 @@ class EditableProperty extends EditableArgument {
167168
Object? convertFromInputString(String? _) {
168169
throw UnimplementedError();
169170
}
171+
172+
bool matchesQuery(String query) {
173+
final regExpQuery = RegExp(query, caseSensitive: false);
174+
return name.caseInsensitiveContains(regExpQuery) ||
175+
valueDisplay.caseInsensitiveContains(regExpQuery) ||
176+
type.caseInsensitiveContains(regExpQuery);
177+
}
170178
}
171179

172180
mixin NumericProperty on EditableProperty {

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
88

99
import '../../../shared/primitives/utils.dart';
1010
import '../../../shared/ui/common_widgets.dart';
11+
import '../../../shared/ui/filter.dart';
1112
import 'property_editor_controller.dart';
1213
import 'property_editor_inputs.dart';
1314
import 'property_editor_types.dart';
@@ -25,6 +26,7 @@ class PropertyEditorView extends StatelessWidget {
2526
controller.editorClient.editArgumentMethodName,
2627
controller.editorClient.editableArgumentsMethodName,
2728
controller.editableWidgetData,
29+
controller.filteredData,
2830
],
2931
builder: (_, values, _) {
3032
final editArgumentMethodName = values.first as String?;
@@ -48,7 +50,8 @@ class PropertyEditorView extends StatelessWidget {
4850
);
4951
}
5052

51-
final (:args, :name, :documentation) = editableWidgetData;
53+
final filteredProperties = values.fourth as List<EditableProperty>;
54+
final (:properties, :name, :documentation) = editableWidgetData;
5255
return Column(
5356
crossAxisAlignment: CrossAxisAlignment.start,
5457
children: [
@@ -57,11 +60,11 @@ class PropertyEditorView extends StatelessWidget {
5760
name: name,
5861
documentation: documentation,
5962
),
60-
args.isEmpty
63+
properties.isEmpty
6164
? _NoEditablePropertiesMessage(name: name)
6265
: _PropertiesList(
63-
editableProperties: args.map(argToProperty).nonNulls.toList(),
64-
editProperty: controller.editArgument,
66+
controller: controller,
67+
editableProperties: filteredProperties,
6568
),
6669
],
6770
);
@@ -72,12 +75,12 @@ class PropertyEditorView extends StatelessWidget {
7275

7376
class _PropertiesList extends StatefulWidget {
7477
const _PropertiesList({
78+
required this.controller,
7579
required this.editableProperties,
76-
required this.editProperty,
7780
});
7881

82+
final PropertyEditorController controller;
7983
final List<EditableProperty> editableProperties;
80-
final EditArgumentFunction editProperty;
8184

8285
static const defaultItemPadding = borderPadding;
8386
static const denseItemPadding = defaultItemPadding / 2;
@@ -105,10 +108,13 @@ class _PropertiesListState extends State<_PropertiesList> {
105108
Widget build(BuildContext context) {
106109
return Column(
107110
children: <Widget>[
111+
_FilterControls(controller: widget.controller),
112+
if (widget.editableProperties.isEmpty)
113+
const _NoMatchingPropertiesMessage(),
108114
for (final property in widget.editableProperties)
109115
_EditablePropertyItem(
110116
property: property,
111-
editProperty: widget.editProperty,
117+
editProperty: widget.controller.editArgument,
112118
),
113119
].joinWith(const PaddedDivider.noPadding()),
114120
);
@@ -148,6 +154,29 @@ class _EditablePropertyItem extends StatelessWidget {
148154
}
149155
}
150156

157+
class _FilterControls extends StatelessWidget {
158+
const _FilterControls({required this.controller});
159+
160+
final PropertyEditorController controller;
161+
162+
@override
163+
Widget build(BuildContext context) {
164+
return Padding(
165+
padding: const EdgeInsets.all(_PropertiesList.defaultItemPadding),
166+
child: Row(
167+
children: [
168+
Expanded(
169+
child: StandaloneFilterField<EditableProperty>(
170+
controller: controller,
171+
filteredItem: 'property',
172+
),
173+
),
174+
],
175+
),
176+
);
177+
}
178+
}
179+
151180
class _PropertyLabels extends StatelessWidget {
152181
const _PropertyLabels({required this.property});
153182

@@ -295,6 +324,15 @@ class _NoEditablePropertiesMessage extends StatelessWidget {
295324
}
296325
}
297326

327+
class _NoMatchingPropertiesMessage extends StatelessWidget {
328+
const _NoMatchingPropertiesMessage();
329+
330+
@override
331+
Widget build(BuildContext context) {
332+
return const Text('No properties matching the current filter.');
333+
}
334+
}
335+
298336
class _WidgetNameAndDocumentation extends StatelessWidget {
299337
const _WidgetNameAndDocumentation({required this.name, this.documentation});
300338

@@ -326,7 +364,7 @@ class _WidgetNameAndDocumentation extends StatelessWidget {
326364
),
327365
],
328366
),
329-
const PaddedDivider(),
367+
const PaddedDivider.noPadding(),
330368
],
331369
);
332370
}

packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:async';
77
import 'package:devtools_app/devtools_app.dart';
88
import 'package:devtools_app/src/shared/editor/api_classes.dart';
99
import 'package:devtools_app/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart';
10+
import 'package:devtools_app/src/standalone_ui/ide_shared/property_editor/property_editor_types.dart';
1011
import 'package:devtools_app/src/standalone_ui/ide_shared/property_editor/property_editor_view.dart';
1112
import 'package:devtools_app_shared/ui.dart';
1213
import 'package:devtools_app_shared/utils.dart';
@@ -61,7 +62,9 @@ void main() {
6162
final argsCompleter = Completer<List<EditableArgument>>();
6263
listener = () {
6364
if (!argsCompleter.isCompleted) {
64-
argsCompleter.complete(controller.editableWidgetData.value!.args);
65+
argsCompleter.complete(
66+
controller.editableWidgetData.value?.properties,
67+
);
6568
}
6669
};
6770
controller.editableWidgetData.addListener(listener!);
@@ -288,6 +291,95 @@ void main() {
288291
});
289292
});
290293

294+
group('filtering editable arguments', () {
295+
testWidgets('can filter by name', (tester) async {
296+
// Load the property editor.
297+
await tester.pumpWidget(wrap(propertyEditor));
298+
299+
// Change the editable args.
300+
controller.initForTestsOnly(editableArgsResult: result1);
301+
await tester.pumpAndSettle();
302+
303+
final titleInput = _findTextFormField('String? title');
304+
final widthInput = _findTextFormField('double width');
305+
final heightInput = _findTextFormField('double? height');
306+
307+
// Verify all inputs are visible.
308+
expect(_findNoPropertiesMessage, findsNothing);
309+
expect(titleInput, findsOneWidget);
310+
expect(widthInput, findsOneWidget);
311+
expect(heightInput, findsOneWidget);
312+
313+
// Filter by the "width" property.
314+
final filterField = _findFilterField();
315+
expect(filterField, findsOneWidget);
316+
await _inputText(filterField, text: 'width', tester: tester);
317+
318+
// Verify only the "width" property is visible.
319+
expect(widthInput, findsOneWidget);
320+
expect(titleInput, findsNothing);
321+
expect(heightInput, findsNothing);
322+
});
323+
324+
testWidgets('can filter by type', (tester) async {
325+
// Load the property editor.
326+
await tester.pumpWidget(wrap(propertyEditor));
327+
328+
// Change the editable args.
329+
controller.initForTestsOnly(editableArgsResult: result1);
330+
await tester.pumpAndSettle();
331+
332+
final titleInput = _findTextFormField('String? title');
333+
final widthInput = _findTextFormField('double width');
334+
final heightInput = _findTextFormField('double? height');
335+
336+
// Verify all inputs are visible.
337+
expect(_findNoPropertiesMessage, findsNothing);
338+
expect(titleInput, findsOneWidget);
339+
expect(widthInput, findsOneWidget);
340+
expect(heightInput, findsOneWidget);
341+
342+
// Filter by the "double" type.
343+
final filterField = _findFilterField();
344+
expect(filterField, findsOneWidget);
345+
await _inputText(filterField, text: 'double', tester: tester);
346+
347+
// Verify only the "width" and "height" properties are visible.
348+
expect(widthInput, findsOneWidget);
349+
expect(heightInput, findsOneWidget);
350+
expect(titleInput, findsNothing);
351+
});
352+
353+
testWidgets('can filter by value', (tester) async {
354+
// Load the property editor.
355+
await tester.pumpWidget(wrap(propertyEditor));
356+
357+
// Change the editable args.
358+
controller.initForTestsOnly(editableArgsResult: result1);
359+
await tester.pumpAndSettle();
360+
361+
final titleInput = _findTextFormField('String? title');
362+
final widthInput = _findTextFormField('double width');
363+
final heightInput = _findTextFormField('double? height');
364+
365+
// Verify all inputs are visible.
366+
expect(_findNoPropertiesMessage, findsNothing);
367+
expect(titleInput, findsOneWidget);
368+
expect(widthInput, findsOneWidget);
369+
expect(heightInput, findsOneWidget);
370+
371+
// Filter by the "Hello world!" value.
372+
final filterField = _findFilterField();
373+
expect(filterField, findsOneWidget);
374+
await _inputText(filterField, text: 'Hello world!', tester: tester);
375+
376+
// Verify only the "title" property is visible.
377+
expect(titleInput, findsOneWidget);
378+
expect(widthInput, findsNothing);
379+
expect(heightInput, findsNothing);
380+
});
381+
});
382+
291383
group('editing arguments', () {
292384
late Completer<String> nextEditCompleter;
293385

@@ -713,6 +805,11 @@ final _findNoPropertiesMessage = find.text(
713805
'No widget properties at current cursor location.',
714806
);
715807

808+
Finder _findFilterField() => find.descendant(
809+
of: find.byType(StandaloneFilterField<EditableProperty>),
810+
matching: find.byType(TextField),
811+
);
812+
716813
Finder _findTextFormField(String inputName) => find.ancestor(
717814
of: find.richTextContaining(inputName),
718815
matching: find.byType(TextFormField),

0 commit comments

Comments
 (0)