Flutter2.2でショートカットキーによるON/OFFを学ぶ

グリッド表示ができたので次はショートカットキーによる表示非表示をしたいですね。

Flutterでショートカットキーってどうやってやるんでしょうか?

おそらく専用の関数があるとは思うので調べてみましょう。

Flutterのショートカットキーについて

RawKeyboardListenerというウィジェットを使って低レベルのキー入力を取得できるそうなのですが どうやら動く環境が限られているようでWeb版には対応してないようです。

MacOSは対応しているという意味不明な状況。

個人的にはWindowsアプリにして動いてくれたらベストなのでちょっと試してみます。

RawKeyboardListenerで囲むといいらしいので一旦SafeAreaになっている部分を差し替えてみます。

以下のサイトを参考にさせて実装してみました。

https://cbtdev.net/flutter-rawkeyboard-listener/

class _GridLineWidget extends StatelessWidget {
  var _controller = TextEditingController();
  final _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return RawKeyboardListener(
      focusNode: _focusNode,
      onKey: (event) {
        print(event.logicalKey.debugName);
      },
      child: TextField(
        textInputAction: TextInputAction.none,
        controller: _controller,
      ),
    );
    // return SafeArea(
    //   child: LayoutBuilder(
    //     builder: (BuildContext context, BoxConstraints constraints) {
    //       return Container(
    //         child: CustomPaint(
    //           painter: _LinePainter(),
    //         ),
    //       );
    //     },
    //   ),
    // );
  }
}

元々あったやつはコメントアウトしています。

一応Winodwsアプリ・Webともに動作していますね。

Flutter2.2になって対応済みって感じなのでしょうか。

ただTextFieldだと動くのですが元々作っていたグリッドだと動かなかったです。

どうやらフォーカス状態じゃないと動かないみたいなので ショートカットキーの実装は結構難しいかも知れません。

もうちょっといい方法を発見した

色々調べてると普通にWeb版でもショートカットキーを設定することが出来ました。

結論だけ先に記述しておきます。

  // キーボードショートカット設定
  final _gridFlagKeySet = LogicalKeySet(
    LogicalKeyboardKey.control,
    LogicalKeyboardKey.keyG,
  );

  @override
  Widget build(BuildContext context) {
    return FocusableActionDetector(
      autofocus: true,
      shortcuts: {_gridFlagKeySet: _GridSwitchIntent()},
      actions: {
        _GridSwitchIntent:
            CallbackAction(onInvoke: (e) => _gridFlagSwitch(context)),
      },
      child: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          return Container(
            child: CustomPaint(
              painter: _LinePainter(viewModel.gridFlag),
            ),
          );
        },
      )
    );
  }

キーボードショートカットはLogicalKeySetで定義します。 見ての通りコントロールキーとGキーの組み合わせを作りました。

重要なのはFocusableActionDetectorで囲むことです。

フォーカスをしていないとキー入力ができないようなので 最初はフォーム関連じゃないと無理なのかなとおもったのですが意外と行けました。

中の設定でautofocusをtrueにするのを忘れないようにしましょう。

shortcutsで作ったキーボードショートカットをあてがって、 中身にインテントを設定します。

インテントは以下のように自作します。

// グリッド描写ON/OFFインテント
class _GridSwitchIntent extends Intent {}

中身は特になにもなしでいいみたいです。

インテントは正直良くわかってません。 そのうちまた勉強しておきます。

次にactionsでショートカットの動作を指定します。

先程作ったインテントに対してCallbackActionにonInvokeに 動かしたい関数を設定します。

_gりdFlagSwitchは以下のようにしました。

  // グリッドのスイッチ
  void _gridFlagSwitch(BuildContext context) {
    Provider.of<LayouteditViewModel>(context, listen: false).toggleGridFlag();
  }

ViewModelの関数を実行しているだけです。

toggleGridFlagは単純にViewModel側で呼び出されたらTrueとFalseを切り替えているだけです。

次に_gridFlagSwitchにcontextを渡したいのでFocusableActionDetectorを Consumerで囲みます。

_GridLineWidgetクラスはこんな感じになりました。

// グリッド描写ON/OFFインテント
class _GridSwitchIntent extends Intent {}

class _GridLineWidget extends StatelessWidget {
  // キーボードショートカット設定
  final _gridFlagKeySet = LogicalKeySet(
    LogicalKeyboardKey.control,
    LogicalKeyboardKey.keyG,
  );

  // グリッドのスイッチ
  void _gridFlagSwitch(BuildContext context) {
    Provider.of<LayouteditViewModel>(context, listen: false).toggleGridFlag();
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<LayouteditViewModel>(builder: (context, viewModel, _) {
      return FocusableActionDetector(
          autofocus: true,
          shortcuts: {_gridFlagKeySet: _GridSwitchIntent()},
          actions: {
            _GridSwitchIntent:
                CallbackAction(onInvoke: (e) => _gridFlagSwitch(context)),
          },
          child: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              return Container(
                child: CustomPaint(
                  painter: _LinePainter(viewModel.gridFlag),
                ),
              );
            },
          ));
    });
  }
}

// ライン描写ウィジェット
class _LinePainter extends CustomPainter {
  bool _gridFlag = false;

  _LinePainter(bool showFlag) {
    this._gridFlag = showFlag;
  }

  // 再描写をするかどうか
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }

  // 実際の描写関数
  @override
  void paint(Canvas canvas, Size size) {
    if (this._gridFlag) {
      final paint = Paint();
      paint.color = Colors.grey;
      paint.strokeWidth = 1;
      for (var i = 0; i < 50; i++) {
        canvas.drawLine(Offset(0, i * 32), Offset(1000, i * 32), paint);
        canvas.drawLine(Offset(i * 32, 0), Offset(i * 32, 1000), paint);
      }
    }
  }
}

正直このやり方が正しいのかはわからないのですが、 理想の動きをしてくれたのでヨシとします。

他にいい方法が見つかったらそっちに乗り換えればいいですね。

方法は色々しってるにこしたことはありません。

ついでにViewModelに追記した分も書き残しておきます。

import 'package:flutter/material.dart';

class LayouteditViewModel extends ChangeNotifier {
  // 矩形のポジション変数
  Offset _pos = Offset(0, 0);
  // 値を取得するためのget関数
  Offset get pos => _pos;
  // グローバルキー
  GlobalKey _key = GlobalKey();
  // グローバルキー
  GlobalKey get globalkey => _key;
  // AppBarの情報を保持
  AppBar _appBar = AppBar(title: Text("Layout Edit"));
  AppBar get appBar => _appBar;
  // グリッド表示フラグ
  bool _gridFlag = true;
  bool get gridFlag => _gridFlag;

  void changeWidgetLayout() {
    notifyListeners();
  }

  // ウィンドウ自由配置用のセットメソッド
  void setWindowPosition(Offset vp) {
    _pos = Offset(vp.dx, vp.dy - appBar.preferredSize.height);
    notifyListeners();
  }

  // グリッドのフラグのON/OFF
  void toggleGridFlag() {
    _gridFlag = !_gridFlag;
    notifyListeners();
  }
}