4

In a Flutter Desktop app, I want to know if, when a user clicks on a button with the mouse, they were also holding down a key (like Shift, Control, Alt etc).

How can this be done?

EDIT

My initial question wasn't clear enough.

I have a dynamic list of checkboxes and I want to use SHIFT+click to select everything between the last selected one and the one that was selected with SHIFT down.

I have looked at FocusNode but that seems to only work for 1 element.

D. Joe
  • 573
  • 1
  • 9
  • 17

1 Answers1

7

This can be done with a FocusNode.

You'll need a stateful widget where you can use initialize the node. You need to attach the node and define the callback that is called on keyboard presses. Then you can request focus from the node with requestFocus so that the node receives the keyboard events.

You'll also need to call _nodeAttachment.reparent(); in your build method. You should also dispose the node in dispose.

The example below prints true or false for whether the shift key is pressed when the button is pressed. This can be easily expanded to other keys like control and alt with the isControlPressed and isAltPressed properties.


Full example:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late final FocusNode focus;
  late final FocusAttachment _nodeAttachment;
  
  bool isShiftPressed = false;
  
  @override
  void initState() {
    super.initState();
    focus = FocusNode(debugLabel: 'Button');
    _nodeAttachment = focus.attach(context, onKey: (node, event) {
      isShiftPressed = event.isShiftPressed;
    });
    focus.requestFocus();
  }
  
  @override
  void dispose() {
    focus.dispose();
    super.dispose();
  }
  
  Widget build(BuildContext context) {
    _nodeAttachment.reparent();
    return TextButton(
      onPressed: () {
        print(isShiftPressed);
      },
      child: Text('Test'),
    );
  }
}

You can still use this solution for your more specific problem. Wrap the above example around your list of checkboxes. You can do a bit of simple logic to get your intended behavior. If what I have here is not exact, you should be able to easily modify it to your needs. This proves that you can use this method for your need, however, even if some details in the logic are not exact:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late final FocusNode focus;
  late final FocusAttachment _nodeAttachment;
  
  bool isShiftPressed = false;
  
  List<bool> checkboxStates = List.filled(5, false);
  
  int lastClicked = -1;
  
  @override
  void initState() {
    super.initState();
    focus = FocusNode(debugLabel: 'Button');
    _nodeAttachment = focus.attach(context, onKey: (node, event) {
      isShiftPressed = event.isShiftPressed;
    });
    focus.requestFocus();
  }
  
  @override
  void dispose() {
    focus.dispose();
    super.dispose();
  }
  
  Widget build(BuildContext context) {
    _nodeAttachment.reparent();
    return Column(
      children: List.generate(checkboxStates.length, (index) => Checkbox(
        value: checkboxStates[index],
        onChanged: (val) {
          if(val == null) {
            return;
          }
          
          setState(() {            
            if(isShiftPressed && val) {
              if(lastClicked >= 0) {
                bool loopForward = lastClicked < index;
                if(loopForward) {
                  for(int x = lastClicked; x < index; x++) {
                    checkboxStates[x] = true;
                  }
                }
                else {
                  for(int x = lastClicked; x > index; x--) {
                    checkboxStates[x] = true;
                  }
                }
              }
            }
            checkboxStates[index] = val;
          });
          
          if(val) {
            lastClicked = index;
          }
          else {
            lastClicked = -1;
          }
          
          print('Checkbox $index: $isShiftPressed');
        }
      )),
    );
  }
}
Christopher Moore
  • 15,626
  • 10
  • 42
  • 52
  • Thank you very much for your reply. I realize now I must have not stated my problem clearly enough. Please see the edit in my original question. – D. Joe May 04 '21 at 08:41
  • yes, saw your edit but didn't yet have the time to test it. I will let you know as soon as I implement it. Thanks. – D. Joe May 06 '21 at 07:50
  • Finally got the time to try this out, and it worked. Thanks. Marked your answer as the correct one. – D. Joe May 30 '21 at 03:12
  • I used this solution to watch keypresses. however it took me many hours to find out that this causes the state to be set as dirty and rebuild and layout when for example showMenu() is called, or a DropdownButton is clicked, triggers an entire widget rebuild. – gene tsai Mar 29 '23 at 09:43