Flutter FocusNode onKeyEvent detect multiple keys - flutter

Using this for Flutter Desktop. I used to use onKey of FocusNode. The doc says it will be deprecated:
/// To receive key events that focuses on this node, pass a listener to `onKeyEvent`.
/// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated
/// in the future.
This is how I used onKey for detecting shift + enter:
FocusNode(
onKey: (node, event)
{
if (event.isKeyPressed(LogicalKeyboardKey.enter))
{
if (event.isShiftPressed)
return KeyEventResult.ignored;
sendAction();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
How do you detect multiple keys being pressed at the same time with onKeyEvent?
Edit: I'm looking for a way to detect multiple keys being pressed WITHOUT the legacy approach. The event parameter in onKeyEvent is not
RawKeyEvent type, therefore isKeyPressed() is not available there. The only thing available is event.logicalKey.keyLabel which can't be used for multiple key detection.

To handle the shortcuts Flutter has Shortcut widget. To start using it you have to specify:
Intent - describes that shortcut intent triggered
Action - describes action on triggered Intent
Let's create an intent class
class NewLineIntent extends Intent {
const NewLineIntent();
}
Add it to your widget and specify the conditions:
static const newLine = SingleActivator(LogicalKeyboardKey.enter, shift: true);
Finally, build a widget:
#override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: const {
newLine: NewLineIntent()
},
child: Actions(
actions: {
NewLineIntent: CallbackAction<NewLineIntent>(
onInvoke: (NewLineIntent intent) {
print('New line');
},
)
},
child: Focus(
autofocus: true,
child: ... ,
),
),
);
}
And when your widget will appear on the screen and you press Shift + Enter your shortcut will be triggered.

Related

Is there any way to generate the same key for the "same" widget in Flutter without having any values to be based on

I have a widget which can have multiple input sections from the same type. When I delete the first child then it behaves weirdly like showing still the old value from that first widget. I figured out I need to use keys for the children but then my UX gets broken. Let me show you some code snippet, please:
class _ParentState extends State<ParentWidget> {
...
#override
Widget build(BuildContext context) {
return Column(
children: stateList
.mapIndexed((index, element) => ChildWidget(key: UniqueKey(), input: element, onChanged: (text) {
setState(() {
stateList[index].text = text;
});
}))
.toList(),
);
}
}
class _ChildState extends State<ChildWidget> {
final ctrl = TextEditingController();
#override
void initState() {
super.initState();
ctrl.text = widget.input.text;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: ctrl,
onChanged: (value) {
widget.onChanged(value);
}
),
...
]
);
}
}
When I wrote the user experience is broken, I was meaning that whenever the user starts to type into the text field then the focus gets loosen character-by-character since the callback is invoked and then generates a new child widget from the state because it has now "different" key... How can I refactor this? I just need a key mechanism which results the same for a child while it is not disposed yet!?
TL;DR
Solution:
remove key
by default, flutter is smart enough to determine which widget is need to rebuild or not. Thats why, key is very rarely used.
ChildWidget(input: element,
onChanged: (text) {
setState(() {
....
When you set UniquieKey() you widget will have different key every time build method called.
which is: everytime you call SetState() your apps will build NEW widget.
if you want to set key, you can use index value
ChildWidget(
key: ValueKey('$index'),
input: element,
onChanged: (text) {
what caused your "user experience broken".
it because, in your onChange function, you call setState() and also you set UniqueKey to your widget. So.. when setState() has called, it will re-execute build method, and since your ChildWidget has Unique key, it will rebuild the widget.
every time user typing, ChildWidget is rebuild. that caused lost focus

Flutter - Handle key event only if there is no text entry event

I'm trying to implement some shortcuts on a desktop app. I've been looking into these links:
Understanding Flutter's focus system
Focus and text fields
Using Actions and Shortcuts
I would like to do an action when the user presses on the key a (for example).
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int count = 0;
KeyEventResult onKey(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.keyA) {
setState(() {
count++;
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Focus(
onKey: onKey,
child: Column(
children: [
Text('count: $count'),
const TextField(),
TextButton(
child: Text('button'),
onPressed: () {},
),
],
),
),
),
);
}
}
When the user clicks on a, the count increments.
The issue with this code is the user being unable to type a in the text field anymore because it is being handled by the focus node.
The first link states:
Key events start at the focus node with primary focus. If that node doesn’t return KeyEventResult.handled from its onKey handler, then its parent focus node is given the event. If the parent doesn’t handle it, it goes to its parent, and so on, until it reaches the root of the focus tree.
and
Focus key events are processed before text entry events, so handling a key event when the focus widget surrounds a text field prevents that key from being entered into the text field.
I would like my Focus widget to handle the key event only if the subtree didn't handle it itself including TextFields (and therefore text entry events).
I tried to always return KeyEventResult.ignored in the onKey method, but the OS triggers a sound meaning there is no action available every time the user clicks on a.
Is there a way to implement what I am trying to do? If yes, how?
You could wrap your text field in a focus that returns KeyEventResult.skipRemainingHandlers in onKeyEvent. This will prevent it affecting the other handlers while the text field is focused. Although this is inelegant and I'd love a correction with a better solution.
const unhandledKeys = [
LogicalKeyboardKey.delete,
LogicalKeyboardKey.backspace,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight
];
final focusNode = useFocusNode(onKeyEvent: (_a, _b) {
if (unhandledKeys.contains(event.logicalKey)) {
return KeyEventResult.ignored;
}
return KeyEventResult.skipRemainingHandlers;
});
return Focus(
focusNode: focusNode,
child: TextField()
);

Prevent keyboard event from bubbling (propagating to parents) in Flutter

I am using RawKeyboardListener to detect the escape key to close (pop) windows, but I can't use the event and prevent it from bubbling (propagating) to parent windows, so all parent windows will receive escape and will close!
I tried using Focus element and it's onKey too, but not difference.
return Scaffold(
body: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(context);
}},
autofocus: true,
child: Container(
child: Text("blah blah")
),
),
),
);
you can attach the key listener to focus Node this listener will return a KeyEventResult enum that determines that the key event is handled or not
var focus = FocusNode(onKey: (FocusNode node, RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.escape)
{
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
});
Also here is the KeyEventResult description :
/// An enum that describes how to handle a key event handled by a
/// [FocusOnKeyCallback].
enum KeyEventResult {
/// The key event has been handled, and the event should not be propagated to
/// other key event handlers.
handled,
/// The key event has not been handled, and the event should continue to be
/// propagated to other key event handlers, even non-Flutter ones.
ignored,
/// The key event has not been handled, but the key event should not be
/// propagated to other key event handlers.
///
/// It will be returned to the platform embedding to be propagated to text
/// fields and non-Flutter key event handlers on the platform.
skipRemainingHandlers,
}

How to detect key presses in flutter without a RawKeyboardListener

I'm trying to detect key presses like "Enter", "Delete" and "Backspace" within flutter. My issue with using a RawKeyboardListener is that it takes focus away from any child widgets.
For example
RawKeyboardListener(
focusNode: _focusNode,
onKey: handleKey,
child: TextField()
)
This makes it impossible to detect both key presses and use the Textfield at the same time.
Does anyone have a alternative way for detecting key presses.
Thanks
You can use the following from dart_html:
window.onKeyPress.listen((KeyboardEvent e) {
print(e.charCode.toString() + " " + new String.fromCharCode(e.charCode));
});
You can use dart:ui and set method window.onKeyData in initState in a stateful widget
Be careful with that method as if you have any focusable nodes on the screen you will need to pass events to them too, otherwise, for example, TextField will not work.
#override
void initState() {
window.onKeyData = (final keyData) {
if (keyData.logical == LogicalKeyboardKey.escape.keyId) {
widget.onPressed();
return true;
}
/// Let event pass to other focuses if it is not the key we looking for
return false;
};
super.initState();
}
#override
void dispose() {
window.onKeyData = null;
super.dispose();
}

Trouble implementing contextual aware icon button in flutter

I have a TextField and an IconButton in a row like so.
I would like the IconButton to be enabled only when there is text in the TextField. I am using the provider package for state management.
Here is the ChangeNotifier implementation.
class ChatMessagesProvider with ChangeNotifier{
List<ChatMessage> chatMessages = <ChatMessage>[];
bool messageTyped = false;
ChatMessagesProvider(this.chatMessages);
void newMessage(String textMessage){
ChatMessage message = ChatMessage(textMessage);
this.chatMessages.add(message);
notifyListeners();
}
int messageCount() => chatMessages.length;
void updateMessageTyped(bool typed){
this.messageTyped = typed;
// notifyListeners(); Un-comennting this makes the Text disappear everytime I type something on the text field
}
}
Here is the actual widget:
class TextCompose extends StatelessWidget {
final TextEditingController _composeTextEditingController = new TextEditingController();
TextCompose(this.chatMessagesProvider);
final ChatMessagesProvider chatMessagesProvider;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Flexible(
child: new TextField(
controller: _composeTextEditingController,
onSubmitted: (String text) {
_onMessageSubmitted(text, context);
},
onChanged: (String text){
if(text.length > 0){
chatMessagesProvider.updateMessageTyped(true);
print(text);
}
else{
chatMessagesProvider.updateMessageTyped(false);
print("No text typed");
}
},
decoration: new InputDecoration.collapsed(
hintText: "Enter message"
),
),
),
new Container(
margin: new EdgeInsets.all(8.0),
child: new IconButton(
color: Theme.of(context).accentColor,
icon: new Icon(Icons.send),
disabledColor: Colors.grey,
onPressed:chatMessagesProvider.messageTyped // This dosen't work
? () => _onMessageSubmitted(_composeTextEditingController.text, context)
: null,
),
)
],
),
);
}
void _onMessageSubmitted(String text, BuildContext context){
if(chatMessagesProvider.messageTyped) { // This works fine.
// clear the message compose text box
_composeTextEditingController.clear();
// add the message to provider.
chatMessagesProvider.newMessage(text);
// set the message typed to false
chatMessagesProvider.messageTyped = false;
}
I am using messageTyped from ChatMessageProvider to check to see if there is any text in the TextField. It seems to work fine when I check it in the _onMessageSubmitted method but not when I check its value in the onPressed property for the IconButton.
I know this because I can see the IconButton remains disabled(colour doesn't change from grey) when I type text, whereas I can hit the submit button on the virtual keyboard and the text is cleared from the TextField(as per call to _composeTextEditingController.clear())
Question:
why does chatMessagesProvider.messageTyped return the right value when called from the _onMessageSubmitted but not when it is called from the onPrssed attribute in the IconButton?
How would I debug something like this in Flutter, I would really like to drop a breakpoint in onPressedAttribute and see the value for chatMessagesProvider.messageTyped
Let me know if you need to see any more of my code.
onPressed:chatMessagesProvider.messageTyped this line is being executed during widget build time so it is always default value and it will never get refreshed until unless you rebuild the widget using notify listener or stateful widget.
Store the currently being typed message in your provider and make your send button enable/disable depends on whether currently being typed message is empty or not.
You say you are using 'provider_package' but you actually have no Provider in your layout. Instead you have a custom built ChangeNotifier with no listeners - you are indeed calling notifyListeners() but there are actually no listeners, so no rebuild is being triggered. A rebuild is needed in order for the button to change its onPressed function reference and implicitly its color.
As for debugging, you can set a breakpoint on the line with onPressed, but it will only be hit during a rebuild.
The most important thing to understand is that the function reference you give to onPressed will be invoked correctly, but a rebuild is needed for the widget to change visually.
Although your current ChangeNotifier implementation does not make much sense, simply wrapping your calls to updateMessageTyped within setState should solve the visual problem - and your breakpoint will also be hit after each typed/deleted character.
The simplest solution you can, first of all, make your widget StatefulWidget.
You need a boolean inside State class:
bool hasText = false;
Then create initState:
#override
void initState() {
_composeTextEditingController.addListener(() {
if (_composeTextEditingController.text.isNotEmpty) {
setState(() {
hasText = true;
});
} else {
setState(() {
hasText = false;
});
}
});
super.initState();
}
Also don't forget to dispose:
#override
void dispose() {
_composeTextEditingController.dispose();
super.dispose();
}
And finally your build method:
Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _composeTextEditingController,
)),
if (hasText) IconButton(icon: Icon(Icons.send), onPressed: () {})
],
),