mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
PopupMenu: add themeable mouse cursor v2 (#96567)
This commit is contained in:
parent
5012c99df5
commit
1612310513
|
@ -1087,6 +1087,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
|||
if (_hasFocus) MaterialState.focused,
|
||||
},
|
||||
);
|
||||
|
||||
return _ParentInkResponseProvider(
|
||||
state: this,
|
||||
child: Actions(
|
||||
|
|
|
@ -264,15 +264,20 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
|||
/// of [ThemeData.textTheme] is used.
|
||||
final TextStyle? textStyle;
|
||||
|
||||
/// {@template flutter.material.popupmenu.mouseCursor}
|
||||
/// The cursor for a mouse pointer when it enters or is hovering over the
|
||||
/// widget.
|
||||
///
|
||||
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
|
||||
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]:
|
||||
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
|
||||
///
|
||||
/// * [MaterialState.hovered].
|
||||
/// * [MaterialState.focused].
|
||||
/// * [MaterialState.disabled].
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
|
||||
/// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If
|
||||
/// that is also null, then [MaterialStateMouseCursor.clickable] is used.
|
||||
final MouseCursor? mouseCursor;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
|
@ -355,12 +360,6 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||
child: item,
|
||||
);
|
||||
}
|
||||
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
|
||||
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
|
||||
<MaterialState>{
|
||||
if (!widget.enabled) MaterialState.disabled,
|
||||
},
|
||||
);
|
||||
|
||||
return MergeSemantics(
|
||||
child: Semantics(
|
||||
|
@ -369,7 +368,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||
child: InkWell(
|
||||
onTap: widget.enabled ? handleTap : null,
|
||||
canRequestFocus: widget.enabled,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
mouseCursor: _EffectiveMouseCursor(widget.mouseCursor, popupMenuTheme.mouseCursor),
|
||||
child: item,
|
||||
),
|
||||
),
|
||||
|
@ -1185,3 +1184,23 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This MaterialStateProperty is passed along to the menu item's InkWell which
|
||||
// resolves the property against MaterialState.disabled, MaterialState.hovered,
|
||||
// MaterialState.focused.
|
||||
class _EffectiveMouseCursor extends MaterialStateMouseCursor {
|
||||
const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor);
|
||||
|
||||
final MouseCursor? widgetCursor;
|
||||
final MaterialStateProperty<MouseCursor?>? themeCursor;
|
||||
|
||||
@override
|
||||
MouseCursor resolve(Set<MaterialState> states) {
|
||||
return MaterialStateProperty.resolveAs<MouseCursor?>(widgetCursor, states)
|
||||
?? themeCursor?.resolve(states)
|
||||
?? MaterialStateMouseCursor.clickable.resolve(states);
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)';
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'dart:ui' show lerpDouble;
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'material_state.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Defines the visual properties of the routes used to display popup menus
|
||||
|
@ -38,6 +39,7 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
this.elevation,
|
||||
this.textStyle,
|
||||
this.enableFeedback,
|
||||
this.mouseCursor,
|
||||
});
|
||||
|
||||
/// The background color of the popup menu.
|
||||
|
@ -57,6 +59,11 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
/// If [PopupMenuButton.enableFeedback] is provided, [enableFeedback] is ignored.
|
||||
final bool? enableFeedback;
|
||||
|
||||
/// {@macro flutter.material.popupmenu.mouseCursor}
|
||||
///
|
||||
/// If specified, overrides the default value of [PopupMenuItem.mouseCursor].
|
||||
final MaterialStateProperty<MouseCursor?>? mouseCursor;
|
||||
|
||||
/// Creates a copy of this object with the given fields replaced with the
|
||||
/// new values.
|
||||
PopupMenuThemeData copyWith({
|
||||
|
@ -65,6 +72,7 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
double? elevation,
|
||||
TextStyle? textStyle,
|
||||
bool? enableFeedback,
|
||||
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
||||
}) {
|
||||
return PopupMenuThemeData(
|
||||
color: color ?? this.color,
|
||||
|
@ -72,6 +80,7 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
elevation: elevation ?? this.elevation,
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
enableFeedback: enableFeedback ?? this.enableFeedback,
|
||||
mouseCursor: mouseCursor ?? this.mouseCursor,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -90,6 +99,7 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
elevation: lerpDouble(a?.elevation, b?.elevation, t),
|
||||
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
|
||||
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
|
||||
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -101,6 +111,7 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
elevation,
|
||||
textStyle,
|
||||
enableFeedback,
|
||||
mouseCursor
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -115,7 +126,8 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
&& other.color == color
|
||||
&& other.shape == shape
|
||||
&& other.textStyle == textStyle
|
||||
&& other.enableFeedback == enableFeedback;
|
||||
&& other.enableFeedback == enableFeedback
|
||||
&& other.mouseCursor == mouseCursor;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -126,6 +138,7 @@ class PopupMenuThemeData with Diagnosticable {
|
|||
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
@ -27,6 +28,7 @@ void main() {
|
|||
expect(popupMenuTheme.shape, null);
|
||||
expect(popupMenuTheme.elevation, null);
|
||||
expect(popupMenuTheme.textStyle, null);
|
||||
expect(popupMenuTheme.mouseCursor, null);
|
||||
});
|
||||
|
||||
testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async {
|
||||
|
@ -48,6 +50,7 @@ void main() {
|
|||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
|
||||
elevation: 2.0,
|
||||
textStyle: TextStyle(color: Color(0xffffffff)),
|
||||
mouseCursor: MaterialStateMouseCursor.clickable,
|
||||
).debugFillProperties(builder);
|
||||
|
||||
final List<String> description = builder.properties
|
||||
|
@ -60,6 +63,7 @@ void main() {
|
|||
'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(2.0))',
|
||||
'elevation: 2.0',
|
||||
'text style: TextStyle(inherit: true, color: Color(0xffffffff))',
|
||||
'mouseCursor: MaterialStateMouseCursor(clickable)',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -251,7 +255,8 @@ void main() {
|
|||
testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async {
|
||||
final Key popupButtonKey = UniqueKey();
|
||||
final Key popupButtonApp = UniqueKey();
|
||||
final Key popupItemKey = UniqueKey();
|
||||
final Key enabledPopupItemKey = UniqueKey();
|
||||
final Key disabledPopupItemKey = UniqueKey();
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
key: popupButtonApp,
|
||||
|
@ -259,19 +264,31 @@ void main() {
|
|||
child: Column(
|
||||
children: <Widget>[
|
||||
PopupMenuTheme(
|
||||
data: const PopupMenuThemeData(
|
||||
data: PopupMenuThemeData(
|
||||
color: Colors.pink,
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 6.0,
|
||||
textStyle: TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic),
|
||||
textStyle: const TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic),
|
||||
mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
return SystemMouseCursors.contextMenu;
|
||||
}
|
||||
return SystemMouseCursors.alias;
|
||||
}),
|
||||
),
|
||||
child: PopupMenuButton<void>(
|
||||
key: popupButtonKey,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return <PopupMenuEntry<void>>[
|
||||
PopupMenuItem<void>(
|
||||
key: popupItemKey,
|
||||
child: const Text('Example'),
|
||||
key: disabledPopupItemKey,
|
||||
enabled: false,
|
||||
child: const Text('disabled'),
|
||||
),
|
||||
PopupMenuItem<void>(
|
||||
key: enabledPopupItemKey,
|
||||
onTap: () { },
|
||||
child: const Text('enabled'),
|
||||
),
|
||||
];
|
||||
},
|
||||
|
@ -299,16 +316,22 @@ void main() {
|
|||
expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))));
|
||||
expect(button.elevation, 6.0);
|
||||
|
||||
/// The last DefaultTextStyle widget under popupItemKey is the
|
||||
/// [PopupMenuItem] specified above, so by finding the last descendent of
|
||||
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the
|
||||
/// built [PopupMenuItem].
|
||||
final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
|
||||
find.descendant(
|
||||
of: find.byKey(popupItemKey),
|
||||
of: find.byKey(enabledPopupItemKey),
|
||||
matching: find.byType(DefaultTextStyle),
|
||||
).last,
|
||||
),
|
||||
);
|
||||
expect(text.style.color, const Color(0xfffff000));
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.contextMenu);
|
||||
await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.alias);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue