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
fa98a522
Unverified
Commit
fa98a522
authored
Feb 08, 2022
by
Justin McCandless
Committed by
GitHub
Feb 08, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Undo/redo (#96968)
parent
93a1b7a5
Changes
4
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
820 additions
and
84 deletions
+820
-84
default_text_editing_shortcuts.dart
...utter/lib/src/widgets/default_text_editing_shortcuts.dart
+8
-0
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+343
-83
text_editing_intents.dart
packages/flutter/lib/src/widgets/text_editing_intents.dart
+21
-1
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+448
-0
No files found.
packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart
View file @
fa98a522
...
@@ -205,6 +205,8 @@ class DefaultTextEditingShortcuts extends Shortcuts {
...
@@ -205,6 +205,8 @@ class DefaultTextEditingShortcuts extends Shortcuts {
const
SingleActivator
(
LogicalKeyboardKey
.
keyC
,
control:
true
):
CopySelectionTextIntent
.
copy
,
const
SingleActivator
(
LogicalKeyboardKey
.
keyC
,
control:
true
):
CopySelectionTextIntent
.
copy
,
const
SingleActivator
(
LogicalKeyboardKey
.
keyV
,
control:
true
):
const
PasteTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyV
,
control:
true
):
const
PasteTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyA
,
control:
true
):
const
SelectAllTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyA
,
control:
true
):
const
SelectAllTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyZ
,
control:
true
):
const
UndoTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyZ
,
shift:
true
,
control:
true
):
const
RedoTextIntent
(
SelectionChangedCause
.
keyboard
),
};
};
// The following key combinations have no effect on text editing on this
// The following key combinations have no effect on text editing on this
...
@@ -215,6 +217,7 @@ class DefaultTextEditingShortcuts extends Shortcuts {
...
@@ -215,6 +217,7 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + C
// * Meta + C
// * Meta + V
// * Meta + V
// * Meta + A
// * Meta + A
// * Meta + shift? + Z
// * Meta + shift? + arrow down
// * Meta + shift? + arrow down
// * Meta + shift? + arrow left
// * Meta + shift? + arrow left
// * Meta + shift? + arrow right
// * Meta + shift? + arrow right
...
@@ -235,6 +238,7 @@ class DefaultTextEditingShortcuts extends Shortcuts {
...
@@ -235,6 +238,7 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + C
// * Meta + C
// * Meta + V
// * Meta + V
// * Meta + A
// * Meta + A
// * Meta + shift? + Z
// * Meta + shift? + arrow down
// * Meta + shift? + arrow down
// * Meta + shift? + arrow left
// * Meta + shift? + arrow left
// * Meta + shift? + arrow right
// * Meta + shift? + arrow right
...
@@ -259,6 +263,7 @@ class DefaultTextEditingShortcuts extends Shortcuts {
...
@@ -259,6 +263,7 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + C
// * Meta + C
// * Meta + V
// * Meta + V
// * Meta + A
// * Meta + A
// * Meta + shift? + Z
// * Meta + shift? + arrow down
// * Meta + shift? + arrow down
// * Meta + shift? + arrow left
// * Meta + shift? + arrow left
// * Meta + shift? + arrow right
// * Meta + shift? + arrow right
...
@@ -319,12 +324,15 @@ class DefaultTextEditingShortcuts extends Shortcuts {
...
@@ -319,12 +324,15 @@ class DefaultTextEditingShortcuts extends Shortcuts {
const
SingleActivator
(
LogicalKeyboardKey
.
keyC
,
meta:
true
):
CopySelectionTextIntent
.
copy
,
const
SingleActivator
(
LogicalKeyboardKey
.
keyC
,
meta:
true
):
CopySelectionTextIntent
.
copy
,
const
SingleActivator
(
LogicalKeyboardKey
.
keyV
,
meta:
true
):
const
PasteTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyV
,
meta:
true
):
const
PasteTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyA
,
meta:
true
):
const
SelectAllTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyA
,
meta:
true
):
const
SelectAllTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyZ
,
meta:
true
):
const
UndoTextIntent
(
SelectionChangedCause
.
keyboard
),
const
SingleActivator
(
LogicalKeyboardKey
.
keyZ
,
shift:
true
,
meta:
true
):
const
RedoTextIntent
(
SelectionChangedCause
.
keyboard
),
// The following key combinations have no effect on text editing on this
// The following key combinations have no effect on text editing on this
// platform:
// platform:
// * End
// * End
// * Home
// * Home
// * Control + shift? + end
// * Control + shift? + end
// * Control + shift? + home
// * Control + shift? + home
// * Control + shift? + Z
};
};
// The following key combinations have no effect on text editing on this
// The following key combinations have no effect on text editing on this
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
fa98a522
...
@@ -30,6 +30,7 @@ import 'scroll_configuration.dart';
...
@@ -30,6 +30,7 @@ import 'scroll_configuration.dart';
import
'scroll_controller.dart'
;
import
'scroll_controller.dart'
;
import
'scroll_physics.dart'
;
import
'scroll_physics.dart'
;
import
'scrollable.dart'
;
import
'scrollable.dart'
;
import
'shortcuts.dart'
;
import
'text.dart'
;
import
'text.dart'
;
import
'text_editing_intents.dart'
;
import
'text_editing_intents.dart'
;
import
'text_selection.dart'
;
import
'text_selection.dart'
;
...
@@ -3138,6 +3139,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -3138,6 +3139,11 @@ 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
(
controller:
widget
.
controller
,
onTriggered:
(
TextEditingValue
value
)
{
userUpdateTextEditingValue
(
value
,
SelectionChangedCause
.
keyboard
);
},
child:
Focus
(
child:
Focus
(
focusNode:
widget
.
focusNode
,
focusNode:
widget
.
focusNode
,
includeSemantics:
false
,
includeSemantics:
false
,
...
@@ -3226,6 +3232,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -3226,6 +3232,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
),
),
),
),
),
),
),
);
);
}
}
...
@@ -4154,3 +4161,256 @@ class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
...
@@ -4154,3 +4161,256 @@ class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
@override
@override
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
({
Key
?
key
,
required
this
.
child
,
required
this
.
controller
,
required
this
.
onTriggered
,
})
:
super
(
key:
key
);
/// 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 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
;
}
widget
.
onTriggered
(
widget
.
controller
.
value
.
copyWith
(
text:
nextValue
.
text
,
selection:
nextValue
.
selection
,
));
}
void
_push
()
{
if
(
widget
.
controller
.
value
==
TextEditingValue
.
empty
)
{
return
;
}
_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 null if the list is emtpy.
late
int
_index
;
/// 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
!=
null
&&
_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/src/widgets/text_editing_intents.dart
View file @
fa98a522
...
@@ -231,6 +231,16 @@ class PasteTextIntent extends Intent {
...
@@ -231,6 +231,16 @@ class PasteTextIntent extends Intent {
final
SelectionChangedCause
cause
;
final
SelectionChangedCause
cause
;
}
}
/// An [Intent] that represents a user interaction that attempts to go back to
/// the previous editing state.
class
RedoTextIntent
extends
Intent
{
/// Creates a [RedoTextIntent].
const
RedoTextIntent
(
this
.
cause
);
/// {@macro flutter.widgets.TextEditingIntents.cause}
final
SelectionChangedCause
cause
;
}
/// An [Intent] that represents a user interaction that attempts to modify the
/// An [Intent] that represents a user interaction that attempts to modify the
/// current [TextEditingValue] in an input field.
/// current [TextEditingValue] in an input field.
class
ReplaceTextIntent
extends
Intent
{
class
ReplaceTextIntent
extends
Intent
{
...
@@ -250,10 +260,20 @@ class ReplaceTextIntent extends Intent {
...
@@ -250,10 +260,20 @@ class ReplaceTextIntent extends Intent {
final
SelectionChangedCause
cause
;
final
SelectionChangedCause
cause
;
}
}
/// An [Intent] that represents a user interaction that attempts to go back to
/// the previous editing state.
class
UndoTextIntent
extends
Intent
{
/// Creates an [UndoTextIntent].
const
UndoTextIntent
(
this
.
cause
);
/// {@macro flutter.widgets.TextEditingIntents.cause}
final
SelectionChangedCause
cause
;
}
/// An [Intent] that represents a user interaction that attempts to change the
/// An [Intent] that represents a user interaction that attempts to change the
/// selection in an input field.
/// selection in an input field.
class
UpdateSelectionIntent
extends
Intent
{
class
UpdateSelectionIntent
extends
Intent
{
/// Creates a [UpdateSelectionIntent].
/// Creates a
n
[UpdateSelectionIntent].
const
UpdateSelectionIntent
(
this
.
currentTextEditingValue
,
this
.
newSelection
,
this
.
cause
);
const
UpdateSelectionIntent
(
this
.
currentTextEditingValue
,
this
.
newSelection
,
this
.
cause
);
/// The [TextEditingValue] that this [Intent]'s action should perform on.
/// The [TextEditingValue] that this [Intent]'s action should perform on.
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
fa98a522
This diff is collapsed.
Click to expand it.
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