Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
2a67bf78
Unverified
Commit
2a67bf78
authored
Mar 08, 2023
by
Jami Couch
Committed by
GitHub
Mar 08, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add support for iOS UndoManager (#98294)
Add support for iOS UndoManager
parent
a16e620e
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
1429 additions
and
347 deletions
+1429
-347
undo_history_controller.0.dart
...i/lib/widgets/undo_history/undo_history_controller.0.dart
+81
-0
services.dart
packages/flutter/lib/services.dart
+1
-0
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+7
-0
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+6
-0
system_channels.dart
packages/flutter/lib/src/services/system_channels.dart
+6
-0
undo_manager.dart
packages/flutter/lib/src/services/undo_manager.dart
+131
-0
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+41
-283
undo_history.dart
packages/flutter/lib/src/widgets/undo_history.dart
+483
-0
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
undo_manager_test.dart
packages/flutter/test/services/undo_manager_test.dart
+67
-0
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+12
-64
editable_text_utils.dart
packages/flutter/test/widgets/editable_text_utils.dart
+64
-0
undo_history_test.dart
packages/flutter/test/widgets/undo_history_test.dart
+517
-0
test_text_input.dart
packages/flutter_test/lib/src/test_text_input.dart
+12
-0
No files found.
examples/api/lib/widgets/undo_history/undo_history_controller.0.dart
0 → 100644
View file @
2a67bf78
// 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.
// Flutter code sample for UndoHistoryController.
import
'package:flutter/material.dart'
;
void
main
(
)
{
runApp
(
const
MyApp
());
}
class
MyApp
extends
StatelessWidget
{
const
MyApp
({
super
.
key
});
static
const
String
_title
=
'Flutter Code Sample'
;
@override
Widget
build
(
BuildContext
context
)
{
return
const
MaterialApp
(
title:
_title
,
home:
MyHomePage
(),
);
}
}
class
MyHomePage
extends
StatefulWidget
{
const
MyHomePage
({
super
.
key
});
@override
State
<
MyHomePage
>
createState
()
=>
_MyHomePageState
();
}
class
_MyHomePageState
extends
State
<
MyHomePage
>
{
final
TextEditingController
_controller
=
TextEditingController
();
final
FocusNode
_focusNode
=
FocusNode
();
final
UndoHistoryController
_undoController
=
UndoHistoryController
();
TextStyle
?
get
enabledStyle
=>
Theme
.
of
(
context
).
textTheme
.
bodyMedium
;
TextStyle
?
get
disabledStyle
=>
Theme
.
of
(
context
).
textTheme
.
bodyMedium
?.
copyWith
(
color:
Colors
.
grey
);
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
body:
Center
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
<
Widget
>[
TextField
(
maxLines:
4
,
controller:
_controller
,
focusNode:
_focusNode
,
undoController:
_undoController
,
),
ValueListenableBuilder
<
UndoHistoryValue
>(
valueListenable:
_undoController
,
builder:
(
BuildContext
context
,
UndoHistoryValue
value
,
Widget
?
child
)
{
return
Row
(
children:
<
Widget
>[
TextButton
(
child:
Text
(
'Undo'
,
style:
value
.
canUndo
?
enabledStyle
:
disabledStyle
),
onPressed:
()
{
_undoController
.
undo
();
},
),
TextButton
(
child:
Text
(
'Redo'
,
style:
value
.
canRedo
?
enabledStyle
:
disabledStyle
),
onPressed:
()
{
_undoController
.
redo
();
},
),
],
);
},
),
],
),
),
);
}
}
packages/flutter/lib/services.dart
View file @
2a67bf78
...
@@ -52,3 +52,4 @@ export 'src/services/text_editing_delta.dart';
...
@@ -52,3 +52,4 @@ export 'src/services/text_editing_delta.dart';
export
'src/services/text_formatter.dart'
;
export
'src/services/text_formatter.dart'
;
export
'src/services/text_input.dart'
;
export
'src/services/text_input.dart'
;
export
'src/services/text_layout_metrics.dart'
;
export
'src/services/text_layout_metrics.dart'
;
export
'src/services/undo_manager.dart'
;
packages/flutter/lib/src/cupertino/text_field.dart
View file @
2a67bf78
...
@@ -214,6 +214,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -214,6 +214,7 @@ class CupertinoTextField extends StatefulWidget {
super
.
key
,
super
.
key
,
this
.
controller
,
this
.
controller
,
this
.
focusNode
,
this
.
focusNode
,
this
.
undoController
,
this
.
decoration
=
_kDefaultRoundedBorderDecoration
,
this
.
decoration
=
_kDefaultRoundedBorderDecoration
,
this
.
padding
=
const
EdgeInsets
.
all
(
7.0
),
this
.
padding
=
const
EdgeInsets
.
all
(
7.0
),
this
.
placeholder
,
this
.
placeholder
,
...
@@ -347,6 +348,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -347,6 +348,7 @@ class CupertinoTextField extends StatefulWidget {
super
.
key
,
super
.
key
,
this
.
controller
,
this
.
controller
,
this
.
focusNode
,
this
.
focusNode
,
this
.
undoController
,
this
.
decoration
,
this
.
decoration
,
this
.
padding
=
const
EdgeInsets
.
all
(
7.0
),
this
.
padding
=
const
EdgeInsets
.
all
(
7.0
),
this
.
placeholder
,
this
.
placeholder
,
...
@@ -780,6 +782,9 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -780,6 +782,9 @@ class CupertinoTextField extends StatefulWidget {
decorationStyle:
TextDecorationStyle
.
dotted
,
decorationStyle:
TextDecorationStyle
.
dotted
,
);
);
/// {@macro flutter.widgets.undoHistory.controller}
final
UndoHistoryController
?
undoController
;
@override
@override
State
<
CupertinoTextField
>
createState
()
=>
_CupertinoTextFieldState
();
State
<
CupertinoTextField
>
createState
()
=>
_CupertinoTextFieldState
();
...
@@ -788,6 +793,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -788,6 +793,7 @@ class CupertinoTextField extends StatefulWidget {
super
.
debugFillProperties
(
properties
);
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
TextEditingController
>(
'controller'
,
controller
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
TextEditingController
>(
'controller'
,
controller
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'focusNode'
,
focusNode
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'focusNode'
,
focusNode
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
UndoHistoryController
>(
'undoController'
,
undoController
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
BoxDecoration
>(
'decoration'
,
decoration
));
properties
.
add
(
DiagnosticsProperty
<
BoxDecoration
>(
'decoration'
,
decoration
));
properties
.
add
(
DiagnosticsProperty
<
EdgeInsetsGeometry
>(
'padding'
,
padding
));
properties
.
add
(
DiagnosticsProperty
<
EdgeInsetsGeometry
>(
'padding'
,
padding
));
properties
.
add
(
StringProperty
(
'placeholder'
,
placeholder
));
properties
.
add
(
StringProperty
(
'placeholder'
,
placeholder
));
...
@@ -1277,6 +1283,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
...
@@ -1277,6 +1283,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
child:
EditableText
(
child:
EditableText
(
key:
editableTextKey
,
key:
editableTextKey
,
controller:
controller
,
controller:
controller
,
undoController:
widget
.
undoController
,
readOnly:
widget
.
readOnly
,
readOnly:
widget
.
readOnly
,
toolbarOptions:
widget
.
toolbarOptions
,
toolbarOptions:
widget
.
toolbarOptions
,
showCursor:
widget
.
showCursor
,
showCursor:
widget
.
showCursor
,
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
2a67bf78
...
@@ -255,6 +255,7 @@ class TextField extends StatefulWidget {
...
@@ -255,6 +255,7 @@ class TextField extends StatefulWidget {
super
.
key
,
super
.
key
,
this
.
controller
,
this
.
controller
,
this
.
focusNode
,
this
.
focusNode
,
this
.
undoController
,
this
.
decoration
=
const
InputDecoration
(),
this
.
decoration
=
const
InputDecoration
(),
TextInputType
?
keyboardType
,
TextInputType
?
keyboardType
,
this
.
textInputAction
,
this
.
textInputAction
,
...
@@ -774,6 +775,9 @@ class TextField extends StatefulWidget {
...
@@ -774,6 +775,9 @@ class TextField extends StatefulWidget {
/// be possible to move the focus to the text field with tab key.
/// be possible to move the focus to the text field with tab key.
final
bool
canRequestFocus
;
final
bool
canRequestFocus
;
/// {@macro flutter.widgets.undoHistory.controller}
final
UndoHistoryController
?
undoController
;
static
Widget
_defaultContextMenuBuilder
(
BuildContext
context
,
EditableTextState
editableTextState
)
{
static
Widget
_defaultContextMenuBuilder
(
BuildContext
context
,
EditableTextState
editableTextState
)
{
return
AdaptiveTextSelectionToolbar
.
editableText
(
return
AdaptiveTextSelectionToolbar
.
editableText
(
editableTextState:
editableTextState
,
editableTextState:
editableTextState
,
...
@@ -834,6 +838,7 @@ class TextField extends StatefulWidget {
...
@@ -834,6 +838,7 @@ class TextField extends StatefulWidget {
super
.
debugFillProperties
(
properties
);
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
TextEditingController
>(
'controller'
,
controller
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
TextEditingController
>(
'controller'
,
controller
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'focusNode'
,
focusNode
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'focusNode'
,
focusNode
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
UndoHistoryController
>(
'undoController'
,
undoController
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enabled'
,
enabled
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enabled'
,
enabled
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
InputDecoration
>(
'decoration'
,
decoration
,
defaultValue:
const
InputDecoration
()));
properties
.
add
(
DiagnosticsProperty
<
InputDecoration
>(
'decoration'
,
decoration
,
defaultValue:
const
InputDecoration
()));
properties
.
add
(
DiagnosticsProperty
<
TextInputType
>(
'keyboardType'
,
keyboardType
,
defaultValue:
TextInputType
.
text
));
properties
.
add
(
DiagnosticsProperty
<
TextInputType
>(
'keyboardType'
,
keyboardType
,
defaultValue:
TextInputType
.
text
));
...
@@ -1313,6 +1318,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
...
@@ -1313,6 +1318,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
showSelectionHandles:
_showSelectionHandles
,
showSelectionHandles:
_showSelectionHandles
,
controller:
controller
,
controller:
controller
,
focusNode:
focusNode
,
focusNode:
focusNode
,
undoController:
widget
.
undoController
,
keyboardType:
widget
.
keyboardType
,
keyboardType:
widget
.
keyboardType
,
textInputAction:
widget
.
textInputAction
,
textInputAction:
widget
.
textInputAction
,
textCapitalization:
widget
.
textCapitalization
,
textCapitalization:
widget
.
textCapitalization
,
...
...
packages/flutter/lib/src/services/system_channels.dart
View file @
2a67bf78
...
@@ -244,6 +244,12 @@ class SystemChannels {
...
@@ -244,6 +244,12 @@ class SystemChannels {
'flutter/spellcheck'
,
'flutter/spellcheck'
,
);
);
/// A JSON [MethodChannel] for handling undo events.
static
const
MethodChannel
undoManager
=
OptionalMethodChannel
(
'flutter/undomanager'
,
JSONMethodCodec
(),
);
/// A JSON [BasicMessageChannel] for keyboard events.
/// A JSON [BasicMessageChannel] for keyboard events.
///
///
/// Each incoming message received on this channel (registered using
/// Each incoming message received on this channel (registered using
...
...
packages/flutter/lib/src/services/undo_manager.dart
0 → 100644
View file @
2a67bf78
// 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/foundation.dart'
;
import
'../../services.dart'
;
/// The direction in which an undo action should be performed, whether undo or redo.
enum
UndoDirection
{
/// Perform an undo action.
undo
,
/// Perform a redo action.
redo
}
/// A low-level interface to the system's undo manager.
///
/// To receive events from the system undo manager, create an
/// [UndoManagerClient] and set it as the [client] on [UndoManager].
///
/// The [setUndoState] method can be used to update the system's undo manager
/// using the [canUndo] and [canRedo] parameters.
///
/// When the system undo or redo button is tapped, the current
/// [UndoManagerClient] will receive [UndoManagerClient.handlePlatformUndo]
/// with an [UndoDirection] representing whether the event is "undo" or "redo".
///
/// Currently, only iOS has an UndoManagerPlugin implemented on the engine side.
/// On iOS, this can be used to listen to the keyboard undo/redo buttons and the
/// undo/redo gestures.
///
/// See also:
///
/// * [NSUndoManager](https://developer.apple.com/documentation/foundation/nsundomanager)
class
UndoManager
{
UndoManager
.
_
()
{
_channel
=
SystemChannels
.
undoManager
;
_channel
.
setMethodCallHandler
(
_handleUndoManagerInvocation
);
}
/// Set the [MethodChannel] used to communicate with the system's undo manager.
///
/// This is only meant for testing within the Flutter SDK. Changing this
/// will break the ability to set the undo status or receive undo and redo
/// events from the system. This has no effect if asserts are disabled.
@visibleForTesting
static
void
setChannel
(
MethodChannel
newChannel
)
{
assert
(()
{
_instance
.
_channel
=
newChannel
..
setMethodCallHandler
(
_instance
.
_handleUndoManagerInvocation
);
return
true
;
}());
}
static
final
UndoManager
_instance
=
UndoManager
.
_
();
/// Receive undo and redo events from the system's [UndoManager].
///
/// Setting the [client] will cause [UndoManagerClient.handlePlatformUndo]
/// to be called when a system undo or redo is triggered, such as by tapping
/// the undo/redo keyboard buttons or using the 3-finger swipe gestures.
static
set
client
(
UndoManagerClient
?
client
)
{
_instance
.
_currentClient
=
client
;
}
/// Return the current [UndoManagerClient].
static
UndoManagerClient
?
get
client
=>
_instance
.
_currentClient
;
/// Set the current state of the system UndoManager. [canUndo] and [canRedo]
/// control the respective "undo" and "redo" buttons of the system UndoManager.
static
void
setUndoState
({
bool
canUndo
=
false
,
bool
canRedo
=
false
})
{
_instance
.
_setUndoState
(
canUndo:
canUndo
,
canRedo:
canRedo
);
}
late
MethodChannel
_channel
;
UndoManagerClient
?
_currentClient
;
Future
<
dynamic
>
_handleUndoManagerInvocation
(
MethodCall
methodCall
)
async
{
final
String
method
=
methodCall
.
method
;
final
List
<
dynamic
>
args
=
methodCall
.
arguments
as
List
<
dynamic
>;
if
(
method
==
'UndoManagerClient.handleUndo'
)
{
assert
(
_currentClient
!=
null
,
'There must be a current UndoManagerClient.'
);
_currentClient
!.
handlePlatformUndo
(
_toUndoDirection
(
args
[
0
]
as
String
));
return
;
}
throw
MissingPluginException
();
}
void
_setUndoState
({
bool
canUndo
=
false
,
bool
canRedo
=
false
})
{
_channel
.
invokeMethod
<
void
>(
'UndoManager.setUndoState'
,
<
String
,
bool
>{
'canUndo'
:
canUndo
,
'canRedo'
:
canRedo
}
);
}
UndoDirection
_toUndoDirection
(
String
direction
)
{
switch
(
direction
)
{
case
'undo'
:
return
UndoDirection
.
undo
;
case
'redo'
:
return
UndoDirection
.
redo
;
}
throw
FlutterError
.
fromParts
(<
DiagnosticsNode
>[
ErrorSummary
(
'Unknown undo direction:
$direction
'
)]);
}
}
/// An interface to receive events from a native UndoManager.
mixin
UndoManagerClient
{
/// Requests that the client perform an undo or redo operation.
///
/// Currently only used on iOS 9+ when the undo or redo methods are invoked
/// by the platform. For example, when using three-finger swipe gestures,
/// the iPad keyboard, or voice control.
void
handlePlatformUndo
(
UndoDirection
direction
);
/// Reverts the value on the stack to the previous value.
void
undo
();
/// Updates the value on the stack to the next value.
void
redo
();
/// Will be true if there are past values on the stack.
bool
get
canUndo
;
/// Will be true if there are future values on the stack.
bool
get
canRedo
;
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
2a67bf78
...
@@ -43,6 +43,7 @@ import 'text_editing_intents.dart';
...
@@ -43,6 +43,7 @@ import 'text_editing_intents.dart';
import
'text_selection.dart'
;
import
'text_selection.dart'
;
import
'text_selection_toolbar_anchors.dart'
;
import
'text_selection_toolbar_anchors.dart'
;
import
'ticker_provider.dart'
;
import
'ticker_provider.dart'
;
import
'undo_history.dart'
;
import
'view.dart'
;
import
'view.dart'
;
import
'widget_span.dart'
;
import
'widget_span.dart'
;
...
@@ -806,10 +807,10 @@ class EditableText extends StatefulWidget {
...
@@ -806,10 +807,10 @@ class EditableText extends StatefulWidget {
this
.
contextMenuBuilder
,
this
.
contextMenuBuilder
,
this
.
spellCheckConfiguration
,
this
.
spellCheckConfiguration
,
this
.
magnifierConfiguration
=
TextMagnifierConfiguration
.
disabled
,
this
.
magnifierConfiguration
=
TextMagnifierConfiguration
.
disabled
,
this
.
undoController
,
})
:
assert
(
obscuringCharacter
.
length
==
1
),
})
:
assert
(
obscuringCharacter
.
length
==
1
),
smartDashesType
=
smartDashesType
??
(
obscureText
?
SmartDashesType
.
disabled
:
SmartDashesType
.
enabled
),
smartDashesType
=
smartDashesType
??
(
obscureText
?
SmartDashesType
.
disabled
:
SmartDashesType
.
enabled
),
smartQuotesType
=
smartQuotesType
??
(
obscureText
?
SmartQuotesType
.
disabled
:
SmartQuotesType
.
enabled
),
smartQuotesType
=
smartQuotesType
??
(
obscureText
?
SmartQuotesType
.
disabled
:
SmartQuotesType
.
enabled
),
assert
(
maxLines
==
null
||
maxLines
>
0
),
assert
(
minLines
==
null
||
minLines
>
0
),
assert
(
minLines
==
null
||
minLines
>
0
),
assert
(
assert
(
(
maxLines
==
null
)
||
(
minLines
==
null
)
||
(
maxLines
>=
minLines
),
(
maxLines
==
null
)
||
(
minLines
==
null
)
||
(
maxLines
>=
minLines
),
...
@@ -965,6 +966,11 @@ class EditableText extends StatefulWidget {
...
@@ -965,6 +966,11 @@ class EditableText extends StatefulWidget {
/// The text style to use for the editable text.
/// The text style to use for the editable text.
final
TextStyle
style
;
final
TextStyle
style
;
/// Controls the undo state of the current editable text.
///
/// If null, this widget will create its own [UndoHistoryController].
final
UndoHistoryController
?
undoController
;
/// {@template flutter.widgets.editableText.strutStyle}
/// {@template flutter.widgets.editableText.strutStyle}
/// The strut style used for the vertical layout.
/// The strut style used for the vertical layout.
///
///
...
@@ -2032,6 +2038,7 @@ class EditableText extends StatefulWidget {
...
@@ -2032,6 +2038,7 @@ class EditableText extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'scribbleEnabled'
,
scribbleEnabled
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'scribbleEnabled'
,
scribbleEnabled
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableInteractiveSelection'
,
enableInteractiveSelection
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableInteractiveSelection'
,
enableInteractiveSelection
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
UndoHistoryController
>(
'undoController'
,
undoController
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
SpellCheckConfiguration
>(
'spellCheckConfiguration'
,
spellCheckConfiguration
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
SpellCheckConfiguration
>(
'spellCheckConfiguration'
,
spellCheckConfiguration
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
List
<
String
>>(
'contentCommitMimeTypes'
,
contentInsertionConfiguration
?.
allowedMimeTypes
??
const
<
String
>[],
defaultValue:
contentInsertionConfiguration
==
null
?
const
<
String
>[]
:
kDefaultContentInsertionMimeTypes
));
properties
.
add
(
DiagnosticsProperty
<
List
<
String
>>(
'contentCommitMimeTypes'
,
contentInsertionConfiguration
?.
allowedMimeTypes
??
const
<
String
>[],
defaultValue:
contentInsertionConfiguration
==
null
?
const
<
String
>[]
:
kDefaultContentInsertionMimeTypes
));
}
}
...
@@ -4488,11 +4495,42 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -4488,11 +4495,42 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
cursor:
widget
.
mouseCursor
??
SystemMouseCursors
.
text
,
cursor:
widget
.
mouseCursor
??
SystemMouseCursors
.
text
,
child:
Actions
(
child:
Actions
(
actions:
_actions
,
actions:
_actions
,
child:
_TextEditingHistory
(
child:
UndoHistory
<
TextEditingValue
>
(
controller
:
widget
.
controller
,
value
:
widget
.
controller
,
onTriggered:
(
TextEditingValue
value
)
{
onTriggered:
(
TextEditingValue
value
)
{
userUpdateTextEditingValue
(
value
,
SelectionChangedCause
.
keyboard
);
userUpdateTextEditingValue
(
value
,
SelectionChangedCause
.
keyboard
);
},
},
shouldChangeUndoStack:
(
TextEditingValue
?
oldValue
,
TextEditingValue
newValue
)
{
if
(
newValue
==
TextEditingValue
.
empty
)
{
return
false
;
}
if
(
oldValue
==
null
)
{
return
true
;
}
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
iOS
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
fuchsia
:
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
windows
:
// Composing text is not counted in history coalescing.
if
(!
widget
.
controller
.
value
.
composing
.
isCollapsed
)
{
return
false
;
}
break
;
case
TargetPlatform
.
android
:
// Gboard on Android puts non-CJK words in composing regions. Coalesce
// composing text in order to allow the saving of partial words in that
// case.
break
;
}
return
oldValue
.
text
!=
newValue
.
text
||
oldValue
.
composing
!=
newValue
.
composing
;
},
focusNode:
widget
.
focusNode
,
controller:
widget
.
undoController
,
child:
Focus
(
child:
Focus
(
focusNode:
widget
.
focusNode
,
focusNode:
widget
.
focusNode
,
includeSemantics:
false
,
includeSemantics:
false
,
...
@@ -5266,286 +5304,6 @@ class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
...
@@ -5266,286 +5304,6 @@ class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
bool
get
isActionEnabled
=>
state
.
_value
.
selection
.
isValid
&&
!
state
.
_value
.
selection
.
isCollapsed
;
bool
get
isActionEnabled
=>
state
.
_value
.
selection
.
isValid
&&
!
state
.
_value
.
selection
.
isCollapsed
;
}
}
/// A void function that takes a [TextEditingValue].
@visibleForTesting
typedef
TextEditingValueCallback
=
void
Function
(
TextEditingValue
value
);
/// Provides undo/redo capabilities for text editing.
///
/// Listens to [controller] as a [ValueNotifier] and saves relevant values for
/// undoing/redoing. The cadence at which values are saved is a best
/// approximation of the native behaviors of a hardware keyboard on Flutter's
/// desktop platforms, as there are subtle differences between each of these
/// platforms.
///
/// Listens to keyboard undo/redo shortcuts and calls [onTriggered] when a
/// shortcut is triggered that would affect the state of the [controller].
class
_TextEditingHistory
extends
StatefulWidget
{
/// Creates an instance of [_TextEditingHistory].
const
_TextEditingHistory
({
required
this
.
child
,
required
this
.
controller
,
required
this
.
onTriggered
,
});
/// The child widget of [_TextEditingHistory].
final
Widget
child
;
/// The [TextEditingController] to save the state of over time.
final
TextEditingController
controller
;
/// Called when an undo or redo causes a state change.
///
/// If the state would still be the same before and after the undo/redo, this
/// will not be called. For example, receiving a redo when there is nothing
/// to redo will not call this method.
///
/// It is also not called when the controller is changed for reasons other
/// than undo/redo.
final
TextEditingValueCallback
onTriggered
;
@override
State
<
_TextEditingHistory
>
createState
()
=>
_TextEditingHistoryState
();
}
class
_TextEditingHistoryState
extends
State
<
_TextEditingHistory
>
{
final
_UndoStack
<
TextEditingValue
>
_stack
=
_UndoStack
<
TextEditingValue
>();
late
final
_Throttled
<
TextEditingValue
>
_throttledPush
;
Timer
?
_throttleTimer
;
// This is used to prevent a reentrant call to the history (a call to _undo or _redo
// should not call _push to add a new entry in the history).
bool
_locked
=
false
;
// This duration was chosen as a best fit for the behavior of Mac, Linux,
// and Windows undo/redo state save durations, but it is not perfect for any
// of them.
static
const
Duration
_kThrottleDuration
=
Duration
(
milliseconds:
500
);
void
_undo
(
UndoTextIntent
intent
)
{
_update
(
_stack
.
undo
());
}
void
_redo
(
RedoTextIntent
intent
)
{
_update
(
_stack
.
redo
());
}
void
_update
(
TextEditingValue
?
nextValue
)
{
if
(
nextValue
==
null
)
{
return
;
}
if
(
nextValue
.
text
==
widget
.
controller
.
text
)
{
return
;
}
_locked
=
true
;
widget
.
onTriggered
(
widget
.
controller
.
value
.
copyWith
(
text:
nextValue
.
text
,
selection:
nextValue
.
selection
,
));
_locked
=
false
;
}
void
_push
()
{
// Do not try to push a new state when the change is related to an undo or redo.
if
(
_locked
)
{
return
;
}
if
(
widget
.
controller
.
value
==
TextEditingValue
.
empty
)
{
return
;
}
switch
(
defaultTargetPlatform
)
{
case
TargetPlatform
.
iOS
:
case
TargetPlatform
.
macOS
:
case
TargetPlatform
.
fuchsia
:
case
TargetPlatform
.
linux
:
case
TargetPlatform
.
windows
:
// Composing text is not counted in history coalescing.
if
(!
widget
.
controller
.
value
.
composing
.
isCollapsed
)
{
return
;
}
break
;
case
TargetPlatform
.
android
:
// Gboard on Android puts non-CJK words in composing regions. Coalesce
// composing text in order to allow the saving of partial words in that
// case.
break
;
}
_throttleTimer
=
_throttledPush
(
widget
.
controller
.
value
);
}
@override
void
initState
()
{
super
.
initState
();
_throttledPush
=
_throttle
<
TextEditingValue
>(
duration:
_kThrottleDuration
,
function:
_stack
.
push
,
);
_push
();
widget
.
controller
.
addListener
(
_push
);
}
@override
void
didUpdateWidget
(
_TextEditingHistory
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
controller
!=
oldWidget
.
controller
)
{
_stack
.
clear
();
oldWidget
.
controller
.
removeListener
(
_push
);
widget
.
controller
.
addListener
(
_push
);
}
}
@override
void
dispose
()
{
widget
.
controller
.
removeListener
(
_push
);
_throttleTimer
?.
cancel
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
UndoTextIntent:
Action
<
UndoTextIntent
>.
overridable
(
context:
context
,
defaultAction:
CallbackAction
<
UndoTextIntent
>(
onInvoke:
_undo
)),
RedoTextIntent:
Action
<
RedoTextIntent
>.
overridable
(
context:
context
,
defaultAction:
CallbackAction
<
RedoTextIntent
>(
onInvoke:
_redo
)),
},
child:
widget
.
child
,
);
}
}
/// A data structure representing a chronological list of states that can be
/// undone and redone.
class
_UndoStack
<
T
>
{
/// Creates an instance of [_UndoStack].
_UndoStack
();
final
List
<
T
>
_list
=
<
T
>[];
// The index of the current value, or -1 if the list is empty.
int
_index
=
-
1
;
/// Returns the current value of the stack.
T
?
get
currentValue
=>
_list
.
isEmpty
?
null
:
_list
[
_index
];
/// Add a new state change to the stack.
///
/// Pushing identical objects will not create multiple entries.
void
push
(
T
value
)
{
if
(
_list
.
isEmpty
)
{
_index
=
0
;
_list
.
add
(
value
);
return
;
}
assert
(
_index
<
_list
.
length
&&
_index
>=
0
);
if
(
value
==
currentValue
)
{
return
;
}
// If anything has been undone in this stack, remove those irrelevant states
// before adding the new one.
if
(
_index
!=
_list
.
length
-
1
)
{
_list
.
removeRange
(
_index
+
1
,
_list
.
length
);
}
_list
.
add
(
value
);
_index
=
_list
.
length
-
1
;
}
/// Returns the current value after an undo operation.
///
/// An undo operation moves the current value to the previously pushed value,
/// if any.
///
/// Iff the stack is completely empty, then returns null.
T
?
undo
()
{
if
(
_list
.
isEmpty
)
{
return
null
;
}
assert
(
_index
<
_list
.
length
&&
_index
>=
0
);
if
(
_index
!=
0
)
{
_index
=
_index
-
1
;
}
return
currentValue
;
}
/// Returns the current value after a redo operation.
///
/// A redo operation moves the current value to the value that was last
/// undone, if any.
///
/// Iff the stack is completely empty, then returns null.
T
?
redo
()
{
if
(
_list
.
isEmpty
)
{
return
null
;
}
assert
(
_index
<
_list
.
length
&&
_index
>=
0
);
if
(
_index
<
_list
.
length
-
1
)
{
_index
=
_index
+
1
;
}
return
currentValue
;
}
/// Remove everything from the stack.
void
clear
()
{
_list
.
clear
();
_index
=
-
1
;
}
@override
String
toString
()
{
return
'_UndoStack
$_list
'
;
}
}
/// A function that can be throttled with the throttle function.
typedef
_Throttleable
<
T
>
=
void
Function
(
T
currentArg
);
/// A function that has been throttled by [_throttle].
typedef
_Throttled
<
T
>
=
Timer
Function
(
T
currentArg
);
/// Returns a _Throttled that will call through to the given function only a
/// maximum of once per duration.
///
/// Only works for functions that take exactly one argument and return void.
_Throttled
<
T
>
_throttle
<
T
>({
required
Duration
duration
,
required
_Throttleable
<
T
>
function
,
// If true, calls at the start of the timer.
bool
leadingEdge
=
false
,
})
{
Timer
?
timer
;
bool
calledDuringTimer
=
false
;
late
T
arg
;
return
(
T
currentArg
)
{
arg
=
currentArg
;
if
(
timer
!=
null
)
{
calledDuringTimer
=
true
;
return
timer
!;
}
if
(
leadingEdge
)
{
function
(
arg
);
}
calledDuringTimer
=
false
;
timer
=
Timer
(
duration
,
()
{
if
(!
leadingEdge
||
calledDuringTimer
)
{
function
(
arg
);
}
timer
=
null
;
});
return
timer
!;
};
}
/// The start and end glyph heights of some range of text.
/// The start and end glyph heights of some range of text.
@immutable
@immutable
class
_GlyphHeights
{
class
_GlyphHeights
{
...
...
packages/flutter/lib/src/widgets/undo_history.dart
0 → 100644
View file @
2a67bf78
// 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:async'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/services.dart'
;
import
'actions.dart'
;
import
'focus_manager.dart'
;
import
'framework.dart'
;
import
'text_editing_intents.dart'
;
/// Provides undo/redo capabilities for a [ValueNotifier].
///
/// Listens to [value] and saves relevant values for undoing/redoing. The
/// cadence at which values are saved is a best approximation of the native
/// behaviors of a number of hardware keyboard on Flutter's desktop
/// platforms, as there are subtle differences between each of the platforms.
///
/// Listens to keyboard undo/redo shortcuts and calls [onTriggered] when a
/// shortcut is triggered that would affect the state of the [value].
///
/// The [child] must manage focus on the [focusNode]. For example, using a
/// [TextField] or [Focus] widget.
class
UndoHistory
<
T
>
extends
StatefulWidget
{
/// Creates an instance of [UndoHistory].
const
UndoHistory
({
super
.
key
,
this
.
shouldChangeUndoStack
,
required
this
.
value
,
required
this
.
onTriggered
,
required
this
.
focusNode
,
this
.
controller
,
required
this
.
child
,
});
/// The value to track over time.
final
ValueNotifier
<
T
>
value
;
/// Called when checking whether a value change should be pushed onto
/// the undo stack.
final
bool
Function
(
T
?
oldValue
,
T
newValue
)?
shouldChangeUndoStack
;
/// Called when an undo or redo causes a state change.
///
/// If the state would still be the same before and after the undo/redo, this
/// will not be called. For example, receiving a redo when there is nothing
/// to redo will not call this method.
///
/// Changes to the [value] while this method is running will not be recorded
/// on the undo stack. For example, a [TextInputFormatter] may change the value
/// from what was on the undo stack, but this new value will not be recorded,
/// as that would wipe out the redo history.
final
void
Function
(
T
value
)
onTriggered
;
/// The [FocusNode] that will be used to listen for focus to set the initial
/// undo state for the element.
final
FocusNode
focusNode
;
/// {@template flutter.widgets.undoHistory.controller}
/// Controls the undo state.
///
/// If null, this widget will create its own [UndoHistoryController].
/// {@endtemplate}
final
UndoHistoryController
?
controller
;
/// The child widget of [UndoHistory].
final
Widget
child
;
@override
State
<
UndoHistory
<
T
>>
createState
()
=>
UndoHistoryState
<
T
>();
}
/// State for a [UndoHistory].
///
/// Provides [undo], [redo], [canUndo], and [canRedo] for programmatic access
/// to the undo state for custom undo and redo UI implementations.
@visibleForTesting
class
UndoHistoryState
<
T
>
extends
State
<
UndoHistory
<
T
>>
with
UndoManagerClient
{
final
_UndoStack
<
T
>
_stack
=
_UndoStack
<
T
>();
late
final
_Throttled
<
T
>
_throttledPush
;
Timer
?
_throttleTimer
;
bool
_duringTrigger
=
false
;
// This duration was chosen as a best fit for the behavior of Mac, Linux,
// and Windows undo/redo state save durations, but it is not perfect for any
// of them.
static
const
Duration
_kThrottleDuration
=
Duration
(
milliseconds:
500
);
// Record the last value to prevent pushing multiple
// of the same value in a row onto the undo stack. For example, _push gets
// called both in initState and when the EditableText receives focus.
T
?
_lastValue
;
UndoHistoryController
?
_controller
;
UndoHistoryController
get
_effectiveController
=>
widget
.
controller
??
(
_controller
??=
UndoHistoryController
());
@override
void
undo
()
{
_update
(
_stack
.
undo
());
_updateState
();
}
@override
void
redo
()
{
_update
(
_stack
.
redo
());
_updateState
();
}
@override
bool
get
canUndo
=>
_stack
.
canUndo
;
@override
bool
get
canRedo
=>
_stack
.
canRedo
;
void
_updateState
()
{
_effectiveController
.
value
=
UndoHistoryValue
(
canUndo:
canUndo
,
canRedo:
canRedo
);
if
(
defaultTargetPlatform
!=
TargetPlatform
.
iOS
)
{
return
;
}
if
(
UndoManager
.
client
==
this
)
{
UndoManager
.
setUndoState
(
canUndo:
canUndo
,
canRedo:
canRedo
);
}
}
void
_undoFromIntent
(
UndoTextIntent
intent
)
{
undo
();
}
void
_redoFromIntent
(
RedoTextIntent
intent
)
{
redo
();
}
void
_update
(
T
?
nextValue
)
{
if
(
nextValue
==
null
)
{
return
;
}
if
(
nextValue
==
_lastValue
)
{
return
;
}
_lastValue
=
nextValue
;
_duringTrigger
=
true
;
try
{
widget
.
onTriggered
(
nextValue
);
assert
(
widget
.
value
.
value
==
nextValue
);
}
finally
{
_duringTrigger
=
false
;
}
}
void
_push
()
{
if
(
widget
.
value
.
value
==
_lastValue
)
{
return
;
}
if
(
_duringTrigger
)
{
return
;
}
if
(!(
widget
.
shouldChangeUndoStack
?.
call
(
_lastValue
,
widget
.
value
.
value
)
??
true
))
{
return
;
}
_lastValue
=
widget
.
value
.
value
;
_throttleTimer
=
_throttledPush
(
widget
.
value
.
value
);
}
void
_handleFocus
()
{
if
(!
widget
.
focusNode
.
hasFocus
)
{
return
;
}
UndoManager
.
client
=
this
;
_updateState
();
}
@override
void
handlePlatformUndo
(
UndoDirection
direction
)
{
switch
(
direction
)
{
case
UndoDirection
.
undo
:
undo
();
break
;
case
UndoDirection
.
redo
:
redo
();
break
;
}
}
@override
void
initState
()
{
super
.
initState
();
_throttledPush
=
_throttle
<
T
>(
duration:
_kThrottleDuration
,
function:
(
T
currentValue
)
{
_stack
.
push
(
currentValue
);
_updateState
();
},
);
_push
();
widget
.
value
.
addListener
(
_push
);
_handleFocus
();
widget
.
focusNode
.
addListener
(
_handleFocus
);
_effectiveController
.
onUndo
.
addListener
(
undo
);
_effectiveController
.
onRedo
.
addListener
(
redo
);
}
@override
void
didUpdateWidget
(
UndoHistory
<
T
>
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
value
!=
oldWidget
.
value
)
{
_stack
.
clear
();
oldWidget
.
value
.
removeListener
(
_push
);
widget
.
value
.
addListener
(
_push
);
}
if
(
widget
.
focusNode
!=
oldWidget
.
focusNode
)
{
oldWidget
.
focusNode
.
removeListener
(
_handleFocus
);
widget
.
focusNode
.
addListener
(
_handleFocus
);
}
if
(
widget
.
controller
!=
oldWidget
.
controller
)
{
_effectiveController
.
onUndo
.
removeListener
(
undo
);
_effectiveController
.
onRedo
.
removeListener
(
redo
);
_controller
?.
dispose
();
_controller
=
null
;
_effectiveController
.
onUndo
.
addListener
(
undo
);
_effectiveController
.
onRedo
.
addListener
(
redo
);
}
}
@override
void
dispose
()
{
widget
.
value
.
removeListener
(
_push
);
widget
.
focusNode
.
removeListener
(
_handleFocus
);
_effectiveController
.
onUndo
.
removeListener
(
undo
);
_effectiveController
.
onRedo
.
removeListener
(
redo
);
_controller
?.
dispose
();
_throttleTimer
?.
cancel
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>{
UndoTextIntent:
Action
<
UndoTextIntent
>.
overridable
(
context:
context
,
defaultAction:
CallbackAction
<
UndoTextIntent
>(
onInvoke:
_undoFromIntent
)),
RedoTextIntent:
Action
<
RedoTextIntent
>.
overridable
(
context:
context
,
defaultAction:
CallbackAction
<
RedoTextIntent
>(
onInvoke:
_redoFromIntent
)),
},
child:
widget
.
child
,
);
}
}
/// Represents whether the current undo stack can undo or redo.
@immutable
class
UndoHistoryValue
{
/// Creates a value for whether the current undo stack can undo or redo.
///
/// The [canUndo] and [canRedo] arguments must have a value, but default to
/// false.
const
UndoHistoryValue
({
this
.
canUndo
=
false
,
this
.
canRedo
=
false
});
/// A value corresponding to an undo stack that can neither undo nor redo.
static
const
UndoHistoryValue
empty
=
UndoHistoryValue
();
/// Whether the current undo stack can perform an undo operation.
final
bool
canUndo
;
/// Whether the current undo stack can perform a redo operation.
final
bool
canRedo
;
@override
String
toString
()
=>
'
${objectRuntimeType(this, 'UndoHistoryValue')}
(canUndo:
$canUndo
, canRedo:
$canRedo
)'
;
@override
bool
operator
==(
Object
other
)
{
if
(
identical
(
this
,
other
))
{
return
true
;
}
return
other
is
UndoHistoryValue
&&
other
.
canUndo
==
canUndo
&&
other
.
canRedo
==
canRedo
;
}
@override
int
get
hashCode
=>
Object
.
hash
(
canUndo
.
hashCode
,
canRedo
.
hashCode
,
);
}
/// A controller for the undo history, for example for an editable text field.
///
/// Whenever a change happens to the underlying value that the [UndoHistory]
/// widget tracks, that widget updates the [value] and the controller notifies
/// it's listeners. Listeners can then read the canUndo and canRedo
/// properties of the value to discover whether [undo] or [redo] are possible.
///
/// The controller also has [undo] and [redo] methods to modify the undo
/// history.
///
/// {@tool dartpad}
/// This example creates a [TextField] with an [UndoHistoryController]
/// which provides undo and redo buttons.
///
/// ** See code in examples/api/lib/widgets/undo_history/undo_history_controller.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [EditableText], which uses the [UndoHistory] widget and allows
/// control of the underlying history using an [UndoHistoryController].
class
UndoHistoryController
extends
ValueNotifier
<
UndoHistoryValue
>
{
/// Creates a controller for an [UndoHistory] widget.
UndoHistoryController
({
UndoHistoryValue
?
value
})
:
super
(
value
??
UndoHistoryValue
.
empty
);
/// Notifies listeners that [undo] has been called.
final
ChangeNotifier
onUndo
=
ChangeNotifier
();
/// Notifies listeners that [redo] has been called.
final
ChangeNotifier
onRedo
=
ChangeNotifier
();
/// Reverts the value on the stack to the previous value.
void
undo
()
{
if
(!
value
.
canUndo
)
{
return
;
}
onUndo
.
notifyListeners
();
}
/// Updates the value on the stack to the next value.
void
redo
()
{
if
(!
value
.
canRedo
)
{
return
;
}
onRedo
.
notifyListeners
();
}
@override
void
dispose
()
{
onUndo
.
dispose
();
onRedo
.
dispose
();
super
.
dispose
();
}
}
/// A data structure representing a chronological list of states that can be
/// undone and redone.
class
_UndoStack
<
T
>
{
/// Creates an instance of [_UndoStack].
_UndoStack
();
final
List
<
T
>
_list
=
<
T
>[];
// The index of the current value, or -1 if the list is empty.
int
_index
=
-
1
;
/// Returns the current value of the stack.
T
?
get
currentValue
=>
_list
.
isEmpty
?
null
:
_list
[
_index
];
bool
get
canUndo
=>
_list
.
isNotEmpty
&&
_index
>
0
;
bool
get
canRedo
=>
_list
.
isNotEmpty
&&
_index
<
_list
.
length
-
1
;
/// Add a new state change to the stack.
///
/// Pushing identical objects will not create multiple entries.
void
push
(
T
value
)
{
if
(
_list
.
isEmpty
)
{
_index
=
0
;
_list
.
add
(
value
);
return
;
}
assert
(
_index
<
_list
.
length
&&
_index
>=
0
);
if
(
value
==
currentValue
)
{
return
;
}
// If anything has been undone in this stack, remove those irrelevant states
// before adding the new one.
if
(
_index
!=
_list
.
length
-
1
)
{
_list
.
removeRange
(
_index
+
1
,
_list
.
length
);
}
_list
.
add
(
value
);
_index
=
_list
.
length
-
1
;
}
/// Returns the current value after an undo operation.
///
/// An undo operation moves the current value to the previously pushed value,
/// if any.
///
/// Iff the stack is completely empty, then returns null.
T
?
undo
()
{
if
(
_list
.
isEmpty
)
{
return
null
;
}
assert
(
_index
<
_list
.
length
&&
_index
>=
0
);
if
(
_index
!=
0
)
{
_index
=
_index
-
1
;
}
return
currentValue
;
}
/// Returns the current value after a redo operation.
///
/// A redo operation moves the current value to the value that was last
/// undone, if any.
///
/// Iff the stack is completely empty, then returns null.
T
?
redo
()
{
if
(
_list
.
isEmpty
)
{
return
null
;
}
assert
(
_index
<
_list
.
length
&&
_index
>=
0
);
if
(
_index
<
_list
.
length
-
1
)
{
_index
=
_index
+
1
;
}
return
currentValue
;
}
/// Remove everything from the stack.
void
clear
()
{
_list
.
clear
();
_index
=
-
1
;
}
@override
String
toString
()
{
return
'_UndoStack
$_list
'
;
}
}
/// A function that can be throttled with the throttle function.
typedef
_Throttleable
<
T
>
=
void
Function
(
T
currentArg
);
/// A function that has been throttled by [_throttle].
typedef
_Throttled
<
T
>
=
Timer
Function
(
T
currentArg
);
/// Returns a _Throttled that will call through to the given function only a
/// maximum of once per duration.
///
/// Only works for functions that take exactly one argument and return void.
_Throttled
<
T
>
_throttle
<
T
>({
required
Duration
duration
,
required
_Throttleable
<
T
>
function
,
// If true, calls at the start of the timer.
bool
leadingEdge
=
false
,
})
{
Timer
?
timer
;
bool
calledDuringTimer
=
false
;
late
T
arg
;
return
(
T
currentArg
)
{
arg
=
currentArg
;
if
(
timer
!=
null
)
{
calledDuringTimer
=
true
;
return
timer
!;
}
if
(
leadingEdge
)
{
function
(
arg
);
}
calledDuringTimer
=
false
;
timer
=
Timer
(
duration
,
()
{
if
(!
leadingEdge
||
calledDuringTimer
)
{
function
(
arg
);
}
timer
=
null
;
});
return
timer
!;
};
}
packages/flutter/lib/widgets.dart
View file @
2a67bf78
...
@@ -147,6 +147,7 @@ export 'src/widgets/ticker_provider.dart';
...
@@ -147,6 +147,7 @@ export 'src/widgets/ticker_provider.dart';
export
'src/widgets/title.dart'
;
export
'src/widgets/title.dart'
;
export
'src/widgets/transitions.dart'
;
export
'src/widgets/transitions.dart'
;
export
'src/widgets/tween_animation_builder.dart'
;
export
'src/widgets/tween_animation_builder.dart'
;
export
'src/widgets/undo_history.dart'
;
export
'src/widgets/unique_widget.dart'
;
export
'src/widgets/unique_widget.dart'
;
export
'src/widgets/value_listenable_builder.dart'
;
export
'src/widgets/value_listenable_builder.dart'
;
export
'src/widgets/view.dart'
;
export
'src/widgets/view.dart'
;
...
...
packages/flutter/test/services/undo_manager_test.dart
0 → 100644
View file @
2a67bf78
// 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/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
TestWidgetsFlutterBinding
.
ensureInitialized
();
group
(
'Undo Interactions'
,
()
{
test
(
'UndoManagerClient handleUndo'
,
()
async
{
// Assemble an UndoManagerClient so we can verify its change in state.
final
_FakeUndoManagerClient
client
=
_FakeUndoManagerClient
();
UndoManager
.
client
=
client
;
expect
(
client
.
latestMethodCall
,
isEmpty
);
// Send handleUndo message with "undo" as the direction.
ByteData
?
messageBytes
=
const
JSONMessageCodec
().
encodeMessage
(<
String
,
dynamic
>{
'args'
:
<
dynamic
>[
'undo'
],
'method'
:
'UndoManagerClient.handleUndo'
,
});
await
ServicesBinding
.
instance
.
defaultBinaryMessenger
.
handlePlatformMessage
(
'flutter/undomanager'
,
messageBytes
,
null
,
);
expect
(
client
.
latestMethodCall
,
'handlePlatformUndo(
${UndoDirection.undo}
)'
);
// Send handleUndo message with "undo" as the direction.
messageBytes
=
const
JSONMessageCodec
().
encodeMessage
(<
String
,
dynamic
>{
'args'
:
<
dynamic
>[
'redo'
],
'method'
:
'UndoManagerClient.handleUndo'
,
});
await
ServicesBinding
.
instance
.
defaultBinaryMessenger
.
handlePlatformMessage
(
'flutter/undomanager'
,
messageBytes
,
(
ByteData
?
_
)
{},
);
expect
(
client
.
latestMethodCall
,
'handlePlatformUndo(
${UndoDirection.redo}
)'
);
});
});
}
class
_FakeUndoManagerClient
with
UndoManagerClient
{
String
latestMethodCall
=
''
;
@override
void
undo
()
{}
@override
void
redo
()
{}
@override
bool
get
canUndo
=>
false
;
@override
bool
get
canRedo
=>
false
;
@override
void
handlePlatformUndo
(
UndoDirection
direction
)
{
latestMethodCall
=
'handlePlatformUndo(
$direction
)'
;
}
}
packages/flutter/test/widgets/editable_text_test.dart
View file @
2a67bf78
...
@@ -6057,68 +6057,6 @@ void main() {
...
@@ -6057,68 +6057,6 @@ void main() {
'to come to the aid
\n
'
// 36 + 19 => 55
'to come to the aid
\n
'
// 36 + 19 => 55
'of their country.'
;
// 55 + 17 => 72
'of their country.'
;
// 55 + 17 => 72
Future
<
void
>
sendKeys
(
WidgetTester
tester
,
List
<
LogicalKeyboardKey
>
keys
,
{
bool
shift
=
false
,
bool
wordModifier
=
false
,
bool
lineModifier
=
false
,
bool
shortcutModifier
=
false
,
required
TargetPlatform
targetPlatform
,
})
async
{
final
String
targetPlatformString
=
targetPlatform
.
toString
();
final
String
platform
=
targetPlatformString
.
substring
(
targetPlatformString
.
indexOf
(
'.'
)
+
1
).
toLowerCase
();
if
(
shift
)
{
await
tester
.
sendKeyDownEvent
(
LogicalKeyboardKey
.
shiftLeft
,
platform:
platform
);
}
if
(
shortcutModifier
)
{
await
tester
.
sendKeyDownEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
wordModifier
)
{
await
tester
.
sendKeyDownEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
altLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
lineModifier
)
{
await
tester
.
sendKeyDownEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
altLeft
,
platform:
platform
,
);
}
for
(
final
LogicalKeyboardKey
key
in
keys
)
{
await
tester
.
sendKeyEvent
(
key
,
platform:
platform
);
await
tester
.
pump
();
}
if
(
lineModifier
)
{
await
tester
.
sendKeyUpEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
altLeft
,
platform:
platform
,
);
}
if
(
wordModifier
)
{
await
tester
.
sendKeyUpEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
altLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
shortcutModifier
)
{
await
tester
.
sendKeyUpEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
shift
)
{
await
tester
.
sendKeyUpEvent
(
LogicalKeyboardKey
.
shiftLeft
,
platform:
platform
);
}
if
(
shift
||
wordModifier
||
lineModifier
)
{
await
tester
.
pump
();
}
}
Future
<
void
>
testTextEditing
(
WidgetTester
tester
,
{
required
TargetPlatform
targetPlatform
})
async
{
Future
<
void
>
testTextEditing
(
WidgetTester
tester
,
{
required
TargetPlatform
targetPlatform
})
async
{
final
String
targetPlatformString
=
targetPlatform
.
toString
();
final
String
targetPlatformString
=
targetPlatform
.
toString
();
final
String
platform
=
targetPlatformString
.
substring
(
targetPlatformString
.
indexOf
(
'.'
)
+
1
).
toLowerCase
();
final
String
platform
=
targetPlatformString
.
substring
(
targetPlatformString
.
indexOf
(
'.'
)
+
1
).
toLowerCase
();
...
@@ -13066,14 +13004,14 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13066,14 +13004,14 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
// Undo first insertion.
// Undo first insertion.
await
sendUndo
(
tester
);
await
sendUndo
(
tester
);
expect
(
controller
.
value
,
composingStep2
.
copyWith
(
composing:
TextRange
.
empty
)
);
expect
(
controller
.
value
,
composingStep2
);
// Waiting for the throttling beetween undos should have no effect.
// Waiting for the throttling beetween undos should have no effect.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Undo second insertion.
// Undo second insertion.
await
sendUndo
(
tester
);
await
sendUndo
(
tester
);
expect
(
controller
.
value
,
composingStep1
.
copyWith
(
composing:
TextRange
.
empty
)
);
expect
(
controller
.
value
,
composingStep1
);
// On web, these keyboard shortcuts are handled by the browser.
// On web, these keyboard shortcuts are handled by the browser.
},
variant:
TargetPlatformVariant
.
only
(
TargetPlatform
.
android
),
skip:
kIsWeb
);
// [intended]
},
variant:
TargetPlatformVariant
.
only
(
TargetPlatform
.
android
),
skip:
kIsWeb
);
// [intended]
...
@@ -13594,6 +13532,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13594,6 +13532,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 nihao'
,
text:
'1 nihao'
,
composing:
TextRange
(
start:
2
,
end:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
),
),
);
);
...
@@ -13603,6 +13542,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13603,6 +13542,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 ni'
,
text:
'1 ni'
,
composing:
TextRange
(
start:
2
,
end:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
),
),
);
);
...
@@ -13620,6 +13560,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13620,6 +13560,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 ni'
,
text:
'1 ni'
,
composing:
TextRange
(
start:
2
,
end:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
),
),
);
);
...
@@ -13628,6 +13569,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13628,6 +13569,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 nihao'
,
text:
'1 nihao'
,
composing:
TextRange
(
start:
2
,
end:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
),
),
);
);
...
@@ -13653,6 +13595,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13653,6 +13595,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 nihao'
,
text:
'1 nihao'
,
composing:
TextRange
(
start:
2
,
end:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
),
),
);
);
...
@@ -13661,6 +13604,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13661,6 +13604,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 ni'
,
text:
'1 ni'
,
composing:
TextRange
(
start:
2
,
end:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
),
),
);
);
...
@@ -13700,6 +13644,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13700,6 +13644,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 ni'
,
text:
'1 ni'
,
composing:
TextRange
(
start:
2
,
end:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
selection:
TextSelection
.
collapsed
(
offset:
4
),
),
),
);
);
...
@@ -13708,6 +13653,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13708,6 +13653,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 nihao'
,
text:
'1 nihao'
,
composing:
TextRange
(
start:
2
,
end:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
selection:
TextSelection
.
collapsed
(
offset:
7
),
),
),
);
);
...
@@ -13829,6 +13775,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13829,6 +13775,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 2 ni'
,
text:
'1 2 ni'
,
composing:
TextRange
(
start:
4
,
end:
6
),
selection:
TextSelection
.
collapsed
(
offset:
6
),
selection:
TextSelection
.
collapsed
(
offset:
6
),
),
),
);
);
...
@@ -13887,6 +13834,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
...
@@ -13887,6 +13834,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
controller
.
value
,
controller
.
value
,
const
TextEditingValue
(
const
TextEditingValue
(
text:
'1 2 ni'
,
text:
'1 2 ni'
,
composing:
TextRange
(
start:
4
,
end:
6
),
selection:
TextSelection
.
collapsed
(
offset:
6
),
selection:
TextSelection
.
collapsed
(
offset:
6
),
),
),
);
);
...
...
packages/flutter/test/widgets/editable_text_utils.dart
View file @
2a67bf78
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
/// On web, the context menu (aka toolbar) is provided by the browser.
/// On web, the context menu (aka toolbar) is provided by the browser.
...
@@ -49,6 +50,69 @@ Offset textOffsetToPosition(WidgetTester tester, int offset, {int index = 0}) {
...
@@ -49,6 +50,69 @@ Offset textOffsetToPosition(WidgetTester tester, int offset, {int index = 0}) {
return
endpoints
[
0
].
point
+
const
Offset
(
kIsWeb
?
1.0
:
0.0
,
-
2.0
);
return
endpoints
[
0
].
point
+
const
Offset
(
kIsWeb
?
1.0
:
0.0
,
-
2.0
);
}
}
/// Mimic key press events by sending key down and key up events via the [tester].
Future
<
void
>
sendKeys
(
WidgetTester
tester
,
List
<
LogicalKeyboardKey
>
keys
,
{
bool
shift
=
false
,
bool
wordModifier
=
false
,
bool
lineModifier
=
false
,
bool
shortcutModifier
=
false
,
required
TargetPlatform
targetPlatform
,
})
async
{
final
String
targetPlatformString
=
targetPlatform
.
toString
();
final
String
platform
=
targetPlatformString
.
substring
(
targetPlatformString
.
indexOf
(
'.'
)
+
1
).
toLowerCase
();
if
(
shift
)
{
await
tester
.
sendKeyDownEvent
(
LogicalKeyboardKey
.
shiftLeft
,
platform:
platform
);
}
if
(
shortcutModifier
)
{
await
tester
.
sendKeyDownEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
wordModifier
)
{
await
tester
.
sendKeyDownEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
altLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
lineModifier
)
{
await
tester
.
sendKeyDownEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
altLeft
,
platform:
platform
,
);
}
for
(
final
LogicalKeyboardKey
key
in
keys
)
{
await
tester
.
sendKeyEvent
(
key
,
platform:
platform
);
await
tester
.
pump
();
}
if
(
lineModifier
)
{
await
tester
.
sendKeyUpEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
altLeft
,
platform:
platform
,
);
}
if
(
wordModifier
)
{
await
tester
.
sendKeyUpEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
altLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
shortcutModifier
)
{
await
tester
.
sendKeyUpEvent
(
platform
==
'macos'
||
platform
==
'ios'
?
LogicalKeyboardKey
.
metaLeft
:
LogicalKeyboardKey
.
controlLeft
,
platform:
platform
,
);
}
if
(
shift
)
{
await
tester
.
sendKeyUpEvent
(
LogicalKeyboardKey
.
shiftLeft
,
platform:
platform
);
}
if
(
shift
||
wordModifier
||
lineModifier
)
{
await
tester
.
pump
();
}
}
// Simple controller that builds a WidgetSpan with 100 height.
// Simple controller that builds a WidgetSpan with 100 height.
class
OverflowWidgetTextEditingController
extends
TextEditingController
{
class
OverflowWidgetTextEditingController
extends
TextEditingController
{
@override
@override
...
...
packages/flutter/test/widgets/undo_history_test.dart
0 → 100644
View file @
2a67bf78
// 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/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'editable_text_utils.dart'
;
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'UndoHistory Node'
);
void
main
(
)
{
TestWidgetsFlutterBinding
.
ensureInitialized
();
group
(
'UndoHistory'
,
()
{
Future
<
void
>
sendUndoRedo
(
WidgetTester
tester
,
[
bool
redo
=
false
])
{
return
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
keyZ
,
],
shortcutModifier:
true
,
shift:
redo
,
targetPlatform:
defaultTargetPlatform
,
);
}
Future
<
void
>
sendUndo
(
WidgetTester
tester
)
=>
sendUndoRedo
(
tester
);
Future
<
void
>
sendRedo
(
WidgetTester
tester
)
=>
sendUndoRedo
(
tester
,
true
);
testWidgets
(
'allows undo and redo to be called programmatically from the UndoHistoryController'
,
(
WidgetTester
tester
)
async
{
final
ValueNotifier
<
int
>
value
=
ValueNotifier
<
int
>(
0
);
final
UndoHistoryController
controller
=
UndoHistoryController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
UndoHistory
<
int
>(
value:
value
,
controller:
controller
,
onTriggered:
(
int
newValue
)
{
value
.
value
=
newValue
;
},
focusNode:
focusNode
,
child:
Container
(),
),
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Undo/redo have no effect if the value has never changed.
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
controller
.
redo
();
expect
(
value
.
value
,
0
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
controller
.
redo
();
expect
(
value
.
value
,
0
);
value
.
value
=
1
;
// Wait for the throttling.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Can undo/redo a single change.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
controller
.
redo
();
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
value
.
value
=
2
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// And can undo/redo multiple changes.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
controller
.
redo
();
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
controller
.
redo
();
expect
(
value
.
value
,
2
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
// Changing the value again clears the redo stack.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
value
.
value
=
3
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
},
variant:
TargetPlatformVariant
.
all
());
testWidgets
(
'allows undo and redo to be called using the keyboard'
,
(
WidgetTester
tester
)
async
{
final
ValueNotifier
<
int
>
value
=
ValueNotifier
<
int
>(
0
);
final
UndoHistoryController
controller
=
UndoHistoryController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
UndoHistory
<
int
>(
controller:
controller
,
value:
value
,
onTriggered:
(
int
newValue
)
{
value
.
value
=
newValue
;
},
focusNode:
focusNode
,
child:
Focus
(
focusNode:
focusNode
,
child:
Container
(),
),
),
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Undo/redo have no effect if the value has never changed.
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
false
);
await
sendUndo
(
tester
);
expect
(
value
.
value
,
0
);
await
sendRedo
(
tester
);
expect
(
value
.
value
,
0
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
false
);
await
sendUndo
(
tester
);
expect
(
value
.
value
,
0
);
await
sendRedo
(
tester
);
expect
(
value
.
value
,
0
);
value
.
value
=
1
;
// Wait for the throttling.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Can undo/redo a single change.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
await
sendUndo
(
tester
);
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
await
sendRedo
(
tester
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
value
.
value
=
2
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// And can undo/redo multiple changes.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
await
sendUndo
(
tester
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
await
sendUndo
(
tester
);
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
await
sendRedo
(
tester
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
await
sendRedo
(
tester
);
expect
(
value
.
value
,
2
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
// Changing the value again clears the redo stack.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
await
sendUndo
(
tester
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
value
.
value
=
3
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
},
variant:
TargetPlatformVariant
.
all
(),
skip:
kIsWeb
);
// [intended]
testWidgets
(
'duplicate changes do not affect the undo history'
,
(
WidgetTester
tester
)
async
{
final
ValueNotifier
<
int
>
value
=
ValueNotifier
<
int
>(
0
);
final
UndoHistoryController
controller
=
UndoHistoryController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
UndoHistory
<
int
>(
controller:
controller
,
value:
value
,
onTriggered:
(
int
newValue
)
{
value
.
value
=
newValue
;
},
focusNode:
focusNode
,
child:
Container
(),
),
),
);
focusNode
.
requestFocus
();
// Wait for the throttling.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
value
.
value
=
1
;
// Wait for the throttling.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Can undo/redo a single change.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
controller
.
redo
();
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
// Changes that result in the same state won't be saved on the undo stack.
value
.
value
=
1
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
},
variant:
TargetPlatformVariant
.
all
());
testWidgets
(
'ignores value changes pushed during onTriggered'
,
(
WidgetTester
tester
)
async
{
final
ValueNotifier
<
int
>
value
=
ValueNotifier
<
int
>(
0
);
final
UndoHistoryController
controller
=
UndoHistoryController
();
int
Function
(
int
newValue
)
valueToUse
=
(
int
value
)
=>
value
;
final
GlobalKey
<
UndoHistoryState
<
int
>>
key
=
GlobalKey
<
UndoHistoryState
<
int
>>();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
UndoHistory
<
int
>(
key:
key
,
value:
value
,
controller:
controller
,
onTriggered:
(
int
newValue
)
{
value
.
value
=
valueToUse
(
newValue
);
},
focusNode:
focusNode
,
child:
Container
(),
),
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Undo/redo have no effect if the value has never changed.
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
controller
.
redo
();
expect
(
value
.
value
,
0
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
false
);
controller
.
undo
();
expect
(
value
.
value
,
0
);
controller
.
redo
();
expect
(
value
.
value
,
0
);
value
.
value
=
1
;
// Wait for the throttling.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
valueToUse
=
(
int
value
)
=>
3
;
expect
(()
=>
key
.
currentState
!.
undo
(),
throwsAssertionError
);
},
variant:
TargetPlatformVariant
.
all
());
testWidgets
(
'changes should send setUndoState to the UndoManagerConnection on iOS'
,
(
WidgetTester
tester
)
async
{
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
SystemChannels
.
undoManager
.
setMockMethodCallHandler
((
MethodCall
methodCall
)
async
{
log
.
add
(
methodCall
);
});
final
FocusNode
focusNode
=
FocusNode
();
final
ValueNotifier
<
int
>
value
=
ValueNotifier
<
int
>(
0
);
final
UndoHistoryController
controller
=
UndoHistoryController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
UndoHistory
<
int
>(
controller:
controller
,
value:
value
,
onTriggered:
(
int
newValue
)
{
value
.
value
=
newValue
;
},
focusNode:
focusNode
,
child:
Focus
(
focusNode:
focusNode
,
child:
Container
(),
),
),
),
);
await
tester
.
pump
();
focusNode
.
requestFocus
();
await
tester
.
pump
();
// Wait for the throttling.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Undo and redo should both be disabled.
MethodCall
methodCall
=
log
.
lastWhere
((
MethodCall
m
)
=>
m
.
method
==
'UndoManager.setUndoState'
);
expect
(
methodCall
.
method
,
'UndoManager.setUndoState'
);
expect
(
methodCall
.
arguments
as
Map
<
String
,
dynamic
>,
<
String
,
bool
>{
'canUndo'
:
false
,
'canRedo'
:
false
});
// Making a change should enable undo.
value
.
value
=
1
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
methodCall
=
log
.
lastWhere
((
MethodCall
m
)
=>
m
.
method
==
'UndoManager.setUndoState'
);
expect
(
methodCall
.
method
,
'UndoManager.setUndoState'
);
expect
(
methodCall
.
arguments
as
Map
<
String
,
dynamic
>,
<
String
,
bool
>{
'canUndo'
:
true
,
'canRedo'
:
false
});
// Undo should remain enabled after another change.
value
.
value
=
2
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
methodCall
=
log
.
lastWhere
((
MethodCall
m
)
=>
m
.
method
==
'UndoManager.setUndoState'
);
expect
(
methodCall
.
method
,
'UndoManager.setUndoState'
);
expect
(
methodCall
.
arguments
as
Map
<
String
,
dynamic
>,
<
String
,
bool
>{
'canUndo'
:
true
,
'canRedo'
:
false
});
// Undo and redo should be enabled after one undo.
controller
.
undo
();
methodCall
=
log
.
lastWhere
((
MethodCall
m
)
=>
m
.
method
==
'UndoManager.setUndoState'
);
expect
(
methodCall
.
method
,
'UndoManager.setUndoState'
);
expect
(
methodCall
.
arguments
as
Map
<
String
,
dynamic
>,
<
String
,
bool
>{
'canUndo'
:
true
,
'canRedo'
:
true
});
// Only redo should be enabled after a second undo.
controller
.
undo
();
methodCall
=
log
.
lastWhere
((
MethodCall
m
)
=>
m
.
method
==
'UndoManager.setUndoState'
);
expect
(
methodCall
.
method
,
'UndoManager.setUndoState'
);
expect
(
methodCall
.
arguments
as
Map
<
String
,
dynamic
>,
<
String
,
bool
>{
'canUndo'
:
false
,
'canRedo'
:
true
});
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}),
skip:
kIsWeb
);
// [intended]
testWidgets
(
'handlePlatformUndo should undo or redo appropriately on iOS'
,
(
WidgetTester
tester
)
async
{
final
ValueNotifier
<
int
>
value
=
ValueNotifier
<
int
>(
0
);
final
UndoHistoryController
controller
=
UndoHistoryController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
UndoHistory
<
int
>(
controller:
controller
,
value:
value
,
onTriggered:
(
int
newValue
)
{
value
.
value
=
newValue
;
},
focusNode:
focusNode
,
child:
Focus
(
focusNode:
focusNode
,
child:
Container
(),
),
),
),
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
focusNode
.
requestFocus
();
await
tester
.
pump
();
// Undo/redo have no effect if the value has never changed.
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
false
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
undo
);
expect
(
value
.
value
,
0
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
redo
);
expect
(
value
.
value
,
0
);
value
.
value
=
1
;
// Wait for the throttling.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// Can undo/redo a single change.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
undo
);
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
redo
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
value
.
value
=
2
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
// And can undo/redo multiple changes.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
undo
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
undo
);
expect
(
value
.
value
,
0
);
expect
(
controller
.
value
.
canUndo
,
false
);
expect
(
controller
.
value
.
canRedo
,
true
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
redo
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
redo
);
expect
(
value
.
value
,
2
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
// Changing the value again clears the redo stack.
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
UndoManager
.
client
!.
handlePlatformUndo
(
UndoDirection
.
undo
);
expect
(
value
.
value
,
1
);
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
true
);
value
.
value
=
3
;
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
controller
.
value
.
canUndo
,
true
);
expect
(
controller
.
value
.
canRedo
,
false
);
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}),
skip:
kIsWeb
);
// [intended]
});
group
(
'UndoHistoryController'
,
()
{
testWidgets
(
'UndoHistoryController notifies onUndo listeners onUndo'
,
(
WidgetTester
tester
)
async
{
int
calls
=
0
;
final
UndoHistoryController
controller
=
UndoHistoryController
();
controller
.
onUndo
.
addListener
(()
{
calls
++;
});
// Does not notify the listener if canUndo is false.
controller
.
undo
();
expect
(
calls
,
0
);
// Does notify the listener if canUndo is true.
controller
.
value
=
const
UndoHistoryValue
(
canUndo:
true
);
controller
.
undo
();
expect
(
calls
,
1
);
});
testWidgets
(
'UndoHistoryController notifies onRedo listeners onRedo'
,
(
WidgetTester
tester
)
async
{
int
calls
=
0
;
final
UndoHistoryController
controller
=
UndoHistoryController
();
controller
.
onRedo
.
addListener
(()
{
calls
++;
});
// Does not notify the listener if canUndo is false.
controller
.
redo
();
expect
(
calls
,
0
);
// Does notify the listener if canRedo is true.
controller
.
value
=
const
UndoHistoryValue
(
canRedo:
true
);
controller
.
redo
();
expect
(
calls
,
1
);
});
testWidgets
(
'UndoHistoryController notifies listeners on value change'
,
(
WidgetTester
tester
)
async
{
int
calls
=
0
;
final
UndoHistoryController
controller
=
UndoHistoryController
(
value:
const
UndoHistoryValue
(
canUndo:
true
));
controller
.
addListener
(()
{
calls
++;
});
// Does not notify if the value is the same.
controller
.
value
=
const
UndoHistoryValue
(
canUndo:
true
);
expect
(
calls
,
0
);
// Does notify if the value has changed.
controller
.
value
=
const
UndoHistoryValue
(
canRedo:
true
);
expect
(
calls
,
1
);
});
});
}
packages/flutter_test/lib/src/test_text_input.dart
View file @
2a67bf78
...
@@ -370,4 +370,16 @@ class TestTextInput {
...
@@ -370,4 +370,16 @@ class TestTextInput {
Future
<
void
>
handleKeyUpEvent
(
LogicalKeyboardKey
key
)
async
{
Future
<
void
>
handleKeyUpEvent
(
LogicalKeyboardKey
key
)
async
{
await
_keyHandler
?.
handleKeyUpEvent
(
key
);
await
_keyHandler
?.
handleKeyUpEvent
(
key
);
}
}
/// Simulates iOS responding to an undo or redo gesture or button.
Future
<
void
>
handleKeyboardUndo
(
String
direction
)
async
{
assert
(
isRegistered
);
await
TestDefaultBinaryMessengerBinding
.
instance
.
defaultBinaryMessenger
.
handlePlatformMessage
(
SystemChannels
.
textInput
.
name
,
SystemChannels
.
textInput
.
codec
.
encodeMethodCall
(
MethodCall
(
'TextInputClient.handleUndo'
,
<
dynamic
>[
direction
]),
),
(
ByteData
?
data
)
{
/* response from framework is discarded */
},
);
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment