Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

476 lines
14 KiB
Dart

// 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 'dart:collection';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MaterialApp(title: 'Actions Demo', home: FocusDemo()));
}
/// A class that can hold invocation information that an [UndoableAction] can
/// use to undo/redo itself.
///
/// Instances of this class are returned from [UndoableAction]s and placed on
/// the undo stack when they are invoked.
class Memento extends Object with Diagnosticable {
const Memento({required this.name, required this.undo, required this.redo});
/// Returns true if this Memento can be used to undo.
///
/// Subclasses could override to provide their own conditions when a command is
/// undoable.
bool get canUndo => true;
/// Returns true if this Memento can be used to redo.
///
/// Subclasses could override to provide their own conditions when a command is
/// redoable.
bool get canRedo => true;
final String name;
final VoidCallback undo;
final ValueGetter<Memento> redo;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('name', name));
}
}
/// Undoable Actions
/// An [ActionDispatcher] subclass that manages the invocation of undoable
/// actions.
class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
// A stack of actions that have been performed. The most recent action
// performed is at the end of the list.
final DoubleLinkedQueue<Memento> _completedActions = DoubleLinkedQueue<Memento>();
// A stack of actions that can be redone. The most recent action performed is
// at the end of the list.
final List<Memento> _undoneActions = <Memento>[];
/// The maximum number of undo levels allowed.
///
/// If this value is set to a value smaller than the number of completed
/// actions, then the stack of completed actions is truncated to only include
/// the last [maxUndoLevels] actions.
int get maxUndoLevels => 1000;
final Set<VoidCallback> _listeners = <VoidCallback>{};
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
/// Notifies listeners that the [ActionDispatcher] has changed state.
///
/// May only be called by subclasses.
@protected
void notifyListeners() {
for (final VoidCallback callback in _listeners) {
callback();
}
}
@override
Object? invokeAction(Action<Intent> action, Intent intent, [BuildContext? context]) {
final Object? result = super.invokeAction(action, intent, context);
print('Invoking ${action is UndoableAction ? 'undoable ' : ''}$intent as $action: $this ');
if (action is UndoableAction) {
_completedActions.addLast(result! as Memento);
_undoneActions.clear();
_pruneActions();
notifyListeners();
}
return result;
}
// Enforces undo level limit.
void _pruneActions() {
while (_completedActions.length > maxUndoLevels) {
_completedActions.removeFirst();
}
}
/// Returns true if there is an action on the stack that can be undone.
bool get canUndo {
if (_completedActions.isNotEmpty) {
return _completedActions.first.canUndo;
}
return false;
}
/// Returns true if an action that has been undone can be re-invoked.
bool get canRedo {
if (_undoneActions.isNotEmpty) {
return _undoneActions.first.canRedo;
}
return false;
}
/// Undoes the last action executed if possible.
///
/// Returns true if the action was successfully undone.
bool undo() {
print('Undoing. $this');
if (!canUndo) {
return false;
}
final Memento memento = _completedActions.removeLast();
memento.undo();
_undoneActions.add(memento);
notifyListeners();
return true;
}
/// Re-invokes a previously undone action, if possible.
///
/// Returns true if the action was successfully invoked.
bool redo() {
print('Redoing. $this');
if (!canRedo) {
return false;
}
final Memento memento = _undoneActions.removeLast();
final Memento replacement = memento.redo();
_completedActions.add(replacement);
_pruneActions();
notifyListeners();
return true;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('undoable items', _completedActions.length));
properties.add(IntProperty('redoable items', _undoneActions.length));
properties.add(IterableProperty<Memento>('undo stack', _completedActions));
properties.add(IterableProperty<Memento>('redo stack', _undoneActions));
}
}
class UndoIntent extends Intent {
const UndoIntent();
}
class UndoAction extends Action<UndoIntent> {
@override
bool isEnabled(UndoIntent intent) {
final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
if (buildContext == null) {
return false;
}
final manager = Actions.of(buildContext) as UndoableActionDispatcher;
return manager.canUndo;
}
@override
void invoke(UndoIntent intent) {
final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
if (buildContext == null) {
return;
}
final manager =
Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext!)
as UndoableActionDispatcher;
manager.undo();
}
}
class RedoIntent extends Intent {
const RedoIntent();
}
class RedoAction extends Action<RedoIntent> {
@override
bool isEnabled(RedoIntent intent) {
final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
if (buildContext == null) {
return false;
}
final manager = Actions.of(buildContext) as UndoableActionDispatcher;
return manager.canRedo;
}
@override
RedoAction invoke(RedoIntent intent) {
final BuildContext? buildContext = primaryFocus?.context ?? FocusDemo.appKey.currentContext;
if (buildContext == null) {
return this;
}
final manager = Actions.of(buildContext) as UndoableActionDispatcher;
manager.redo();
return this;
}
}
/// An action that can be undone.
abstract class UndoableAction<T extends Intent> extends Action<T> {}
class UndoableFocusActionBase<T extends Intent> extends UndoableAction<T> {
@override
@mustCallSuper
Memento invoke(T intent) {
final FocusNode? previousFocus = primaryFocus;
return Memento(
name: previousFocus!.debugLabel!,
undo: () {
previousFocus.requestFocus();
},
redo: () {
return invoke(intent);
},
);
}
}
class UndoableRequestFocusAction extends UndoableFocusActionBase<RequestFocusIntent> {
@override
Memento invoke(RequestFocusIntent intent) {
final Memento memento = super.invoke(intent);
intent.focusNode.requestFocus();
return memento;
}
}
/// Actions for manipulating focus.
class UndoableNextFocusAction extends UndoableFocusActionBase<NextFocusIntent> {
@override
Memento invoke(NextFocusIntent intent) {
final Memento memento = super.invoke(intent);
primaryFocus?.nextFocus();
return memento;
}
}
class UndoablePreviousFocusAction extends UndoableFocusActionBase<PreviousFocusIntent> {
@override
Memento invoke(PreviousFocusIntent intent) {
final Memento memento = super.invoke(intent);
primaryFocus?.previousFocus();
return memento;
}
}
class UndoableDirectionalFocusAction extends UndoableFocusActionBase<DirectionalFocusIntent> {
@override
Memento invoke(DirectionalFocusIntent intent) {
final Memento memento = super.invoke(intent);
primaryFocus?.focusInDirection(intent.direction);
return memento;
}
}
/// A button class that takes focus when clicked.
class DemoButton extends StatefulWidget {
const DemoButton({super.key, required this.name});
final String name;
@override
State<DemoButton> createState() => _DemoButtonState();
}
class _DemoButtonState extends State<DemoButton> {
late final FocusNode _focusNode = FocusNode(debugLabel: widget.name);
final GlobalKey _nameKey = GlobalKey();
void _handleOnPressed() {
print('Button ${widget.name} pressed.');
setState(() {
Actions.invoke(_nameKey.currentContext!, RequestFocusIntent(_focusNode));
});
}
@override
void dispose() {
super.dispose();
_focusNode.dispose();
}
@override
Widget build(BuildContext context) {
return TextButton(
focusNode: _focusNode,
style: ButtonStyle(
foregroundColor: const MaterialStatePropertyAll<Color>(Colors.black),
overlayColor: WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
if (states.contains(WidgetState.focused)) {
return Colors.red;
}
if (states.contains(WidgetState.hovered)) {
return Colors.blue;
}
return Colors.transparent;
}),
),
onPressed: () => _handleOnPressed(),
child: Text(widget.name, key: _nameKey),
);
}
}
class FocusDemo extends StatefulWidget {
const FocusDemo({super.key});
static GlobalKey appKey = GlobalKey();
@override
State<FocusDemo> createState() => _FocusDemoState();
}
class _FocusDemoState extends State<FocusDemo> {
final FocusNode outlineFocus = FocusNode(debugLabel: 'Demo Focus Node');
late final UndoableActionDispatcher dispatcher = UndoableActionDispatcher();
bool canUndo = false;
bool canRedo = false;
@override
void initState() {
super.initState();
canUndo = dispatcher.canUndo;
canRedo = dispatcher.canRedo;
dispatcher.addListener(_handleUndoStateChange);
}
void _handleUndoStateChange() {
if (dispatcher.canUndo != canUndo) {
setState(() {
canUndo = dispatcher.canUndo;
});
}
if (dispatcher.canRedo != canRedo) {
setState(() {
canRedo = dispatcher.canRedo;
});
}
}
@override
void dispose() {
dispatcher.removeListener(_handleUndoStateChange);
outlineFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return Actions(
dispatcher: dispatcher,
actions: <Type, Action<Intent>>{
RequestFocusIntent: UndoableRequestFocusAction(),
NextFocusIntent: UndoableNextFocusAction(),
PreviousFocusIntent: UndoablePreviousFocusAction(),
DirectionalFocusIntent: UndoableDirectionalFocusAction(),
UndoIntent: UndoAction(),
RedoIntent: RedoAction(),
},
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: Shortcuts(
shortcuts: <ShortcutActivator, Intent>{
SingleActivator(
LogicalKeyboardKey.keyZ,
meta: Platform.isMacOS,
control: !Platform.isMacOS,
shift: true,
): const RedoIntent(),
SingleActivator(
LogicalKeyboardKey.keyZ,
meta: Platform.isMacOS,
control: !Platform.isMacOS,
): const UndoIntent(),
},
child: FocusScope(
key: FocusDemo.appKey,
debugLabel: 'Scope',
autofocus: true,
child: DefaultTextStyle(
style: textTheme.headlineMedium!,
child: Scaffold(
appBar: AppBar(title: const Text('Actions Demo')),
body: Center(
child: Builder(
builder: (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DemoButton(name: 'One'),
DemoButton(name: 'Two'),
DemoButton(name: 'Three'),
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DemoButton(name: 'Four'),
DemoButton(name: 'Five'),
DemoButton(name: 'Six'),
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DemoButton(name: 'Seven'),
DemoButton(name: 'Eight'),
DemoButton(name: 'Nine'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: canUndo
? () {
Actions.invoke(context, const UndoIntent());
}
: null,
child: const Text('UNDO'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: canRedo
? () {
Actions.invoke(context, const RedoIntent());
}
: null,
child: const Text('REDO'),
),
),
],
),
],
);
},
),
),
),
),
),
),
),
);
}
}