Unverified Commit a8471a61 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Material Desktop Context Menu (#74286)

Very simple right-click context menu for Windows and Linux in a Material-esque style.
parent aed45185
......@@ -57,6 +57,7 @@ export 'src/material/date.dart';
export 'src/material/date_picker.dart';
export 'src/material/date_picker_deprecated.dart';
export 'src/material/debug.dart';
export 'src/material/desktop_text_selection.dart';
export 'src/material/dialog.dart';
export 'src/material/dialog_theme.dart';
export 'src/material/divider.dart';
......
......@@ -307,7 +307,7 @@ class _CupertinoDesktopTextSelectionToolbar extends StatelessWidget {
_kToolbarScreenPadding,
),
child: CustomSingleChildLayout(
delegate: _DesktopTextSelectionToolbarLayoutDelegate(
delegate: DesktopTextSelectionToolbarLayoutDelegate(
anchor: anchor - localAdjustment,
),
child: toolbarBuilder(context, Column(
......@@ -319,48 +319,6 @@ class _CupertinoDesktopTextSelectionToolbar extends StatelessWidget {
}
}
// Positions the toolbar at [anchor] if it fits, otherwise moves it so that it
// just fits fully on-screen.
//
// See also:
//
// * [CupertinoDesktopTextSelectionToolbar], which uses this to position itself.
// * [TextSelectionToolbarLayoutDelegate], which does a similar layout for
// the mobile text selection toolbars.
class _DesktopTextSelectionToolbarLayoutDelegate extends SingleChildLayoutDelegate {
/// Creates an instance of TextSelectionToolbarLayoutDelegate.
_DesktopTextSelectionToolbarLayoutDelegate({
required this.anchor,
});
/// The point at which to render the menu, if possible.
///
/// Should be provided in local coordinates.
final Offset anchor;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.loosen();
}
@override
Offset getPositionForChild(Size size, Size childSize) {
final Offset overhang = Offset(
anchor.dx + childSize.width - size.width,
anchor.dy + childSize.height - size.height,
);
return Offset(
overhang.dx > 0.0 ? anchor.dx - overhang.dx : anchor.dx,
overhang.dy > 0.0 ? anchor.dy - overhang.dy : anchor.dy,
);
}
@override
bool shouldRelayout(_DesktopTextSelectionToolbarLayoutDelegate oldDelegate) {
return anchor != oldDelegate.anchor;
}
}
// These values were measured from a screenshot of TextEdit on MacOS 10.15.7 on
// a Macbook Pro.
const TextStyle _kToolbarButtonFontStyle = TextStyle(
......
This diff is collapsed.
......@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'desktop_text_selection.dart';
import 'feedback.dart';
import 'text_selection.dart';
import 'text_selection_theme.dart';
......@@ -606,10 +607,18 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
case TargetPlatform.android:
case TargetPlatform.fuchsia:
forcePressEnabled = false;
textSelectionControls ??= materialTextSelectionControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
break;
case TargetPlatform.linux:
case TargetPlatform.windows:
forcePressEnabled = false;
textSelectionControls ??= materialTextSelectionControls;
textSelectionControls ??= desktopTextSelectionControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
......
......@@ -13,6 +13,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'debug.dart';
import 'desktop_text_selection.dart';
import 'feedback.dart';
import 'input_decorator.dart';
import 'material.dart';
......@@ -1169,10 +1170,18 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
case TargetPlatform.android:
case TargetPlatform.fuchsia:
forcePressEnabled = false;
textSelectionControls ??= materialTextSelectionControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
break;
case TargetPlatform.linux:
case TargetPlatform.windows:
forcePressEnabled = false;
textSelectionControls ??= materialTextSelectionControls;
textSelectionControls ??= desktopTextSelectionControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
......
......@@ -38,12 +38,16 @@ class TextSelectionToolbarTextButton extends StatelessWidget {
static const double _kMiddlePadding = 9.5;
static const double _kEndPadding = 14.5;
/// {@template flutter.material.TextSelectionToolbarTextButton.child}
/// The child of this button.
///
/// Usually a [Text].
/// {@endtemplate}
final Widget child;
/// {@template flutter.material.TextSelectionToolbarTextButton.onPressed}
/// Called when this button is pressed.
/// {@endtemplate}
final VoidCallback? onPressed;
/// The padding between the button's edge and its child.
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
/// Positions the toolbar at [anchor] if it fits, otherwise moves it so that it
/// just fits fully on-screen.
///
/// See also:
///
/// * [desktopTextSelectionControls], which uses this to position
/// itself.
/// * [cupertinoDesktopTextSelectionControls], which uses this to position
/// itself.
/// * [TextSelectionToolbarLayoutDelegate], which does a similar layout for
/// the mobile text selection toolbars.
class DesktopTextSelectionToolbarLayoutDelegate extends SingleChildLayoutDelegate {
/// Creates an instance of TextSelectionToolbarLayoutDelegate.
DesktopTextSelectionToolbarLayoutDelegate({
required this.anchor,
});
/// The point at which to render the menu, if possible.
///
/// Should be provided in local coordinates.
final Offset anchor;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.loosen();
}
@override
Offset getPositionForChild(Size size, Size childSize) {
final Offset overhang = Offset(
anchor.dx + childSize.width - size.width,
anchor.dy + childSize.height - size.height,
);
return Offset(
overhang.dx > 0.0 ? anchor.dx - overhang.dx : anchor.dx,
overhang.dy > 0.0 ? anchor.dy - overhang.dy : anchor.dy,
);
}
@override
bool shouldRelayout(DesktopTextSelectionToolbarLayoutDelegate oldDelegate) {
return anchor != oldDelegate.anchor;
}
}
......@@ -33,6 +33,7 @@ export 'src/widgets/bottom_navigation_bar_item.dart';
export 'src/widgets/color_filter.dart';
export 'src/widgets/container.dart';
export 'src/widgets/debug.dart';
export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart';
export 'src/widgets/dismissible.dart';
export 'src/widgets/disposable_build_context.dart';
export 'src/widgets/drag_target.dart';
......
......@@ -238,7 +238,7 @@ void main() {
expect(controller.text, ' blah2blah1');
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), skip: kIsWeb);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), skip: kIsWeb);
testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
final VoidCallback onEditingComplete = () { };
......@@ -7296,7 +7296,7 @@ void main() {
);
// The text selection toolbar isn't shown on Mac without a right click.
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), skip: kIsWeb);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), skip: kIsWeb);
testWidgets('double tap chains work', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
......@@ -7445,7 +7445,7 @@ void main() {
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), skip: kIsWeb);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), skip: kIsWeb);
testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
......@@ -7639,7 +7639,7 @@ void main() {
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 0);
expect(controller.value.selection.extentOffset, 1);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), skip: kIsWeb);
testWidgets('force press does not select a word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
......
......@@ -112,7 +112,7 @@ void main() {
expect(controller.text, ' blah2blah1');
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), skip: kIsWeb);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), skip: kIsWeb);
testWidgets('Passes textAlign to underlying TextField', (WidgetTester tester) async {
const TextAlign alignment = TextAlign.center;
......
......@@ -255,7 +255,7 @@ void main() {
expect(controller.text, ' blah2blah1');
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 0));
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), skip: kIsWeb);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), skip: kIsWeb);
testWidgets('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(
......@@ -4329,8 +4329,6 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
TargetPlatform.windows,
}),
);
......@@ -4367,8 +4365,49 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
}),
);
testWidgets('The Select All calls on selection changed with a mouse on windows and linux', (WidgetTester tester) async {
const String string = 'abc def ghi';
TextSelection? newSelection;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SelectableText(
string,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
expect(newSelection, isNull);
newSelection = selection;
},
),
),
),
);
// Right-click on the 'e' in 'def'.
final Offset ePos = textOffsetToPosition(tester, 5);
final TestGesture gesture = await tester.startGesture(
ePos,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(newSelection!.baseOffset, 4);
expect(newSelection!.extentOffset, 7);
newSelection = null;
await tester.tap(find.text('Select all'));
await tester.pump();
expect(newSelection!.baseOffset, 0);
expect(newSelection!.extentOffset, 11);
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.windows,
TargetPlatform.linux,
}),
);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment