アイディアがうまく描画されず苦戦 – 開発日記(6)

Flutterの基礎でつまづく 2023/03/21

一度レーン(Lane)が生成した後だと、追加されたアイディア(Idea)がうまく描画されない」という問題が直らない。。。

Flutterの基礎やRiverpodなどを学ぶも、結構難しくまだ理解しきれていない。

Keyの問題なのか。データは生成されているはずだが描画がされないように見える。
なぜEnterキーででさらに新しいIdeaを作ると、古いものも含めて描画されるのか。分からない。

再現専用のessential版を作った。

main_essential.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

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

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

class LanesManager extends StatefulWidget {
  @override
  State<LanesManager> createState() => _LanesManagerState();
}

class _LanesManagerState extends State<LanesManager> {
  List<Lane> lanes = [
    Lane(
        key: GlobalKey<_LaneState>(),
        ideas: [Idea(key: GlobalKey<_IdeaState>(), index: 0)])
  ];

  @override
  void initState() {
    super.initState();

    for (int i = 0; i < lanes.length; i++) {
      GlobalKey<_LaneState> laneKey = GlobalKey<_LaneState>();
      lanes[i] = Lane(
          key: laneKey, ideas: [Idea(key: GlobalKey<_IdeaState>(), index: 0)]);
    }
  }

  void addNewLane() {
    GlobalKey<_LaneState> newLaneKey = GlobalKey<_LaneState>();
    setState(() {
      lanes.add(Lane(
          key: newLaneKey,
          ideas: [Idea(key: GlobalKey<_IdeaState>(), index: 0)]));
    });
  }

  void focusNextLane(int targetLaneIndex, int targetIdeaIndex) {
    if (targetLaneIndex < 0 || targetIdeaIndex < 0) return;
    if (targetLaneIndex == lanes.length) {
      addNewLane();
    }
    if (targetLaneIndex < lanes.length) {
      for (int i = lanes[targetLaneIndex].ideas.length - 1;
          i < targetIdeaIndex;
          i++) {
        GlobalKey<_IdeaState> newIdeaKey = GlobalKey<_IdeaState>();
        lanes[targetLaneIndex].ideas.add(Idea(key: newIdeaKey, index: i + 1));
        for (int i = 0; i < lanes[targetLaneIndex].ideas.length; i++) {
          lanes[targetLaneIndex].ideas[i].index = i;
        }
      }
      ;
      lanes[targetLaneIndex]
          .ideas[targetIdeaIndex]
          .key
          .currentState
          ?._textFocusNode
          .requestFocus();
    }
  }

  @override
  Widget build(BuildContext context) {
    return LanesManagerInheritedWidget(
      laneManagerState: this,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: lanes.length,
        itemBuilder: (context, index) {
          return Container(
            width: MediaQuery.of(context).size.width * 0.4,
            child: lanes[index],
          );
        },
      ),
    );
  }
}

class LanesManagerInheritedWidget extends InheritedWidget {
  final _LanesManagerState laneManagerState;

  LanesManagerInheritedWidget(
      {required this.laneManagerState, required Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(LanesManagerInheritedWidget oldWidget) => true;

  static _LanesManagerState of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<LanesManagerInheritedWidget>()!
        .laneManagerState;
  }
}

class Lane extends StatefulWidget {
  final List<Idea> ideas;
  Lane({required GlobalKey<_LaneState> key, required this.ideas});

  @override
  _LaneState createState() => _LaneState();
}

class _LaneState extends State<Lane> {
  List<Idea> get ideas => widget.ideas;

  void addNewIdea(int currentIndex) {
    setState(() {
      int currentIndentLevel =
          ideas[currentIndex].key.currentState?.indentLevel ?? 0;
      ideas.insert(
        currentIndex + 1,
        Idea(
          key: GlobalKey<_IdeaState>(),
          index: currentIndex + 1,
        ),
      );
      for (int i = currentIndex + 2; i < ideas.length; i++) {
        ideas[i].index = i;
      }
    });
  }

  void focusAdjacentIdea(int currentIndex, int direction) {
    if (currentIndex + direction >= 0 &&
        currentIndex + direction < ideas.length) {
      ideas[currentIndex + direction]
          .key
          .currentState
          ?._textFocusNode
          .requestFocus();
    }
  }

  void redrawAffectedIdeas(int startIndex, int endIndex) {
    if (startIndex < 0 || endIndex >= ideas.length) return;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return LaneInheritedWidget(
      laneState: this,
      child: ListView.builder(
        itemCount: ideas.length,
        itemBuilder: (context, index) {
          return ideas[index];
        },
      ),
    );
  }
}

class LaneInheritedWidget extends InheritedWidget {
  final _LaneState laneState;

  LaneInheritedWidget({required this.laneState, required Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(LaneInheritedWidget oldWidget) => true;

  static _LaneState of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<LaneInheritedWidget>()!
        .laneState;
  }
}

class Idea extends StatefulWidget {
  int index;
  final GlobalKey<_IdeaState> key;
  final double boxSpacing; //他のエディタとの間隔

  Idea({
    required this.key,
    required this.index,
    this.boxSpacing = 8.0,
  }) : super(key: ValueKey('Idea ${index}'));

  @override
  _IdeaState createState() => _IdeaState();
}

class _IdeaState extends State<Idea> {
  TextEditingController _textController = TextEditingController();
  int indentLevel = 0;
  FocusNode _textFocusNode = FocusNode();

  void updateIndex(int newIndex) {
    setState(() {
      widget.index = newIndex;
    });
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      FocusScope.of(context).requestFocus(_textFocusNode);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      onKey: (FocusNode node, RawKeyEvent event) {
        if (event.runtimeType == RawKeyDownEvent) {
          if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
            LaneInheritedWidget.of(context).focusAdjacentIdea(widget.index, -1);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
            if (widget.index ==
                LaneInheritedWidget.of(context).ideas.length - 1) {
              LaneInheritedWidget.of(context).addNewIdea(widget.index);
            } else {
              LaneInheritedWidget.of(context)
                  .focusAdjacentIdea(widget.index, 1);
            }
          } else if (event.logicalKey == LogicalKeyboardKey.arrowRight &&
              _textController.selection.baseOffset ==
                  _textController.text.length) {
            int laneIndex = LanesManagerInheritedWidget.of(context)
                .lanes
                .indexOf(LaneInheritedWidget.of(context).widget);
            LanesManagerInheritedWidget.of(context)
                .focusNextLane(laneIndex + 1, widget.index);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft &&
              _textController.selection.baseOffset == 0) {
            int laneIndex = LanesManagerInheritedWidget.of(context)
                .lanes
                .indexOf(LaneInheritedWidget.of(context).widget);
            LanesManagerInheritedWidget.of(context)
                .focusNextLane(laneIndex - 1, widget.index);
          }
        }
        return KeyEventResult.ignored;
      },
      child: RawKeyboardListener(
        focusNode: FocusNode(),
        child: Container(
          margin:
              EdgeInsets.symmetric(horizontal: 16, vertical: widget.boxSpacing),
          child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
            Expanded(
              child: Container(
                height: 48,
                child: TextField(
                  controller: _textController,
                  focusNode: _textFocusNode,
                  maxLines: 1,
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.multiline,
                  textInputAction: TextInputAction.done,
                  onSubmitted: (value) {
                    _textFocusNode.unfocus();
                    LaneInheritedWidget.of(context).addNewIdea(widget.index);
                  },
                ),
              ),
            ),
          ]),
        ),
      ),
    );
  }
}

ひとまず問題は解消 2023/03/25

色々苦しんだ結果、ようやく描画の問題の解消方法は分かった。

まだうまい書き方にはなっていないがまずは前に進めそうだ。長かった……。

再現専用の main_essential2.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

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

class CanvasZone extends StatefulWidget {
  @override
  State<CanvasZone> createState() => _CanvasZoneState();
}

class _CanvasZoneState extends State<CanvasZone> {
  List<Lane> lanes = [];

  @override
  void initState() {
    super.initState();
    addNewLane(0);
  }

  void addNewLane(int newIndex) {
    print("addNewLane");
    setState(() {
      lanes.add(Lane(
            key: GlobalKey<_LaneState>(),
            index: newIndex,
            ideas: [Idea(key: GlobalKey<_IdeaState>(), index: 0)]));
    });
  }

  void focusNextLane(int targetLaneIndex, int targetIdeaIndex) {
    print("--focusNextLane targetLaneIndex:"+ targetLaneIndex.toString() + ' targetIdeaIndex:'+ targetIdeaIndex.toString());
    if (targetLaneIndex < 0 || targetIdeaIndex < 0) return;
    if (targetLaneIndex == lanes.length) {
      addNewLane(targetLaneIndex);
    }
    if (targetLaneIndex < lanes.length) {
      print("lanes[targetLaneIndex].ideas.length:"+ lanes[targetLaneIndex].ideas.length.toString());
      for (int i = lanes[targetLaneIndex].ideas.length - 1;
          i < targetIdeaIndex;
          i++) {
        lanes[targetLaneIndex].key.currentState?.addNewIdea(i);
/*
        GlobalKey<_IdeaState> newIdeaKey = GlobalKey<_IdeaState>();
        setState(() {
          print("i:"+ i.toString());
          lanes[targetLaneIndex].ideas.add(Idea(key: newIdeaKey, index: i + 1));
          for (int j = 0; j < lanes[targetLaneIndex].ideas.length; j++) {
            lanes[targetLaneIndex].ideas[j].index = j;
          }
        });
*/
      };
      print("lanes[targetLaneIndex].ideas[targetIdeaindex].key:"+ lanes[targetLaneIndex].ideas[targetIdeaIndex].key.toString());
      lanes[targetLaneIndex]
          .key
          .currentState?.build(context);
      lanes[targetLaneIndex].ideas[targetIdeaIndex].key.currentState?.build(context);

      lanes[targetLaneIndex]
          .ideas[targetIdeaIndex]
          .key
          .currentState
          ?._textFocusNode
          .requestFocus();
    }
  }

  @override
  Widget build(BuildContext context) {
    return CanvasZoneInheritedWidget(
      canvasZoneState: this,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: lanes.length,
        itemBuilder: (context, index) {
          return Container(
            width: MediaQuery.of(context).size.width * 0.4,
            child: lanes[index],
          );
        },
      ),
    );
  }
}

class CanvasZoneInheritedWidget extends InheritedWidget {
  final _CanvasZoneState canvasZoneState;

  CanvasZoneInheritedWidget(
      {required this.canvasZoneState, required Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(CanvasZoneInheritedWidget oldWidget) => true;

  static _CanvasZoneState of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<CanvasZoneInheritedWidget>()!
        .canvasZoneState;
  }
}

class Lane extends StatefulWidget {
  int index;
  final GlobalKey<_LaneState> key;
  List<Idea> ideas;

  Lane({
    required this.key,
    required this.index,
    required this.ideas
  }) : super(key: ValueKey('Lane'+key.toString()));

  @override
  _LaneState createState() => _LaneState();
}

class _LaneState extends State<Lane> {
  List<Idea> get ideas => widget.ideas;

  void addNewIdea(int currentIndex) {
    setState(() {
      ideas.insert(
        currentIndex + 1,
        Idea(
          key: GlobalKey<_IdeaState>(),
          index: currentIndex + 1,
        ),
      );
      for (int i = currentIndex + 2; i < ideas.length; i++) {
        ideas[i].index = i;
      }
    });
  }

  void focusAdjacentIdea(int currentIndex, int direction) {
    if (currentIndex + direction >= 0 &&
        currentIndex + direction < ideas.length) {
      ideas[currentIndex + direction]
          .key
          .currentState
          ?._textFocusNode
          .requestFocus();
    }
  }

  void redrawAffectedIdeas(int startIndex, int endIndex) {
    if (startIndex < 0 || endIndex >= ideas.length) return;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    print("build_Lane"+widget.index.toString());
    return LaneInheritedWidget(
      laneState: this,
      child: ListView.builder(
        itemCount: ideas.length,
        itemBuilder: (context, index) {
          return ideas[index];
        },
      ),
    );
  }
}

class LaneInheritedWidget extends InheritedWidget {
  final _LaneState laneState;

  LaneInheritedWidget({required this.laneState, required Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(LaneInheritedWidget oldWidget) => true;

  static _LaneState of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<LaneInheritedWidget>()!
        .laneState;
  }
}

class Idea extends StatefulWidget {
  int index;
  final GlobalKey<_IdeaState> key;

  Idea({
    required this.key,
    required this.index,
  }) : super(key: ValueKey('Idea'+key.toString()));

  @override
  _IdeaState createState() => _IdeaState();
}

class _IdeaState extends State<Idea> {
  TextEditingController _textController = TextEditingController();
  int indentLevel = 0;
  FocusNode _textFocusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      FocusScope.of(context).requestFocus(_textFocusNode);
    });
  }

  @override
  Widget build(BuildContext context) {
    print("IdeaState_build key:"+ widget.key.toString());
    return Focus(
      onKey: (FocusNode node, RawKeyEvent event) {
        if (event.runtimeType == RawKeyDownEvent) {
          if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
            LaneInheritedWidget.of(context).focusAdjacentIdea(widget.index, -1);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
            if (widget.index ==
                LaneInheritedWidget.of(context).ideas.length - 1) {
              LaneInheritedWidget.of(context).addNewIdea(widget.index);
            } else {
              LaneInheritedWidget.of(context)
                  .focusAdjacentIdea(widget.index, 1);
            }
          } else if (event.logicalKey == LogicalKeyboardKey.arrowRight &&
              _textController.selection.baseOffset ==
                  _textController.text.length) {
            int laneIndex = CanvasZoneInheritedWidget.of(context)
                .lanes
                .indexOf(LaneInheritedWidget.of(context).widget);
            CanvasZoneInheritedWidget.of(context)
                .focusNextLane(laneIndex + 1, widget.index);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft &&
              _textController.selection.baseOffset == 0) {
            int laneIndex = CanvasZoneInheritedWidget.of(context)
                .lanes
                .indexOf(LaneInheritedWidget.of(context).widget);
            CanvasZoneInheritedWidget.of(context)
                .focusNextLane(laneIndex - 1, widget.index);
          }
        }
        return KeyEventResult.ignored;
      },
      child: RawKeyboardListener(
        focusNode: FocusNode(),
          child: Container(
            height: 48,
            child: TextField(
                controller: _textController,
                focusNode: _textFocusNode,
                maxLines: 1,
                decoration: const InputDecoration(
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.multiline,
              textInputAction: TextInputAction.done,
              onSubmitted: (value) {
                _textFocusNode.unfocus();
                LaneInheritedWidget.of(context).addNewIdea(widget.index);
              },
            ),
          ),
        ),
    );
  }
}

現時点でのメインコードは次のようになっている。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'indent_line_painter.dart';
import 'dart:math';

void main() => runApp(MyApp());

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

class CanvasZone extends StatefulWidget {
  @override
  State<CanvasZone> createState() => _CanvasZoneState();
}

class _CanvasZoneState extends State<CanvasZone> {
  double _scale = 1.0;

  List<Lane> lanes = [];

  @override
  void initState() {
    super.initState();
    addNewLane(0);
  }

  void addNewLane(int newIndex) {
    setState(() {
      lanes.add(Lane(
          key: GlobalKey<_LaneState>(),
          index: newIndex,
          ideas: [Idea(key: GlobalKey<_IdeaState>(), index: 0)]));
    });
  }

  void removeLane() {
    setState(() {
      if (lanes.length > 1) {
        lanes.removeLast();
      }
    });
  }

  void focusNextLane(int targetLaneIndex, int targetIdeaIndex) {
    if (targetLaneIndex < 0 || targetIdeaIndex < 0) return;
    if (targetLaneIndex == lanes.length) {
      addNewLane(targetLaneIndex);
    }
    if (targetLaneIndex < lanes.length) {
      for (int i = lanes[targetLaneIndex].ideas.length - 1;
        i < targetIdeaIndex;
        i++) {
        if(lanes[targetLaneIndex].key.currentState == null) {
          setState(() {
            lanes[targetLaneIndex].ideas.add(Idea(key: GlobalKey<_IdeaState>(), index: i + 1));
            for (int j = 0; j < lanes[targetLaneIndex].ideas.length; j++) {
              lanes[targetLaneIndex].ideas[j].index = j;
            }
          });
        } else {
          lanes[targetLaneIndex].key.currentState?.addNewIdea(i);
        }
      };
      lanes[targetLaneIndex]
       .ideas[targetIdeaIndex]
       .key
       .currentState
       ?._textFocusNode
       .requestFocus();
    }
  }

  void scaleUp() {
    setState(() {
      _scale += 0.1;
    });
  }

  void scaleDown() {
    setState(() {
      _scale = max(_scale - 0.1, 0.1);
    });
  }

  @override
  Widget build(BuildContext context) {
    return CanvasZoneInheritedWidget(
      canvasZoneState: this,
      child: Column(
        children: [
          Align(
            alignment: Alignment.topCenter,
            child: Container(
              margin: EdgeInsets.only(top: 10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: null,//addNewLane(lanes.length),
                    child: Text("レーン追加"),
                  ),
                  SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: removeLane,
                    child: Text("レーン削除"),
                  ),
                  SizedBox(width: 30),
                  ElevatedButton(
                    onPressed: scaleUp,
                    child: Text("拡大"),
                  ),
                  SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: scaleDown,
                    child: Text("縮小"),
                  ),
                ],
              ),
            ),
          ),
          const Divider(),
          Expanded(
            child: Transform.scale(
              scale: _scale,
              alignment: Alignment.topLeft,
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                itemCount: lanes.length,
                itemBuilder: (context, index) {
                  return Container(
                    width: MediaQuery.of(context).size.width * 0.4,
                    child: lanes[index],
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class CanvasZoneInheritedWidget extends InheritedWidget {
  final _CanvasZoneState canvasZoneState;

  CanvasZoneInheritedWidget(
      {required this.canvasZoneState, required Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(CanvasZoneInheritedWidget oldWidget) => true;

  static _CanvasZoneState of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<CanvasZoneInheritedWidget>()!
        .canvasZoneState;
  }
}

class Lane extends StatefulWidget {
  int index;
  final GlobalKey<_LaneState> key;
  List<Idea> ideas;

  Lane({
    required this.key,
    required this.index,
    required this.ideas
  }) : super(key: ValueKey('Lane'+key.toString()));

  @override
  _LaneState createState() => _LaneState();
}

class _LaneState extends State<Lane> {
  List<Idea> get ideas => widget.ideas;

  void addNewIdea(int currentIndex) {
    setState(() {
      int currentIndentLevel =
          ideas[currentIndex].key.currentState?.indentLevel ?? 0;
      ideas.insert(
        currentIndex + 1,
        Idea(
          key: GlobalKey<_IdeaState>(),
          index: currentIndex + 1,
          textFieldHeight: 48.0,
          initialIndentLevel: currentIndentLevel,
        ),
      );
      for (int i = currentIndex + 2; i < ideas.length; i++) {
        ideas[i].index = i;
      }
    });
  }


  void moveIdea(int currentIndex, int direction) {
    if (currentIndex + direction >= 0 &&
        currentIndex + direction < ideas.length) {
      setState(() {
        Idea temp = ideas[currentIndex];
        ideas[currentIndex] = ideas[currentIndex + direction];
        ideas[currentIndex + direction] = temp;
        ideas[currentIndex].key.currentState?.updateIndex(currentIndex);
        ideas[currentIndex + direction]
            .key
            .currentState
            ?.updateIndex(currentIndex + direction);
      });
    }
  }

  void deleteIdea(int currentIndex) {
    if (ideas.length > 1) {
      setState(() {
        ideas.removeAt(currentIndex);
        for (int i = currentIndex; i < ideas.length; i++) {
          ideas[i].index = i;
        }
        if (currentIndex > 0) {
          ideas[currentIndex - 1]
              .key
              .currentState
              ?._textFocusNode
              .requestFocus();
        } else {
          ideas[currentIndex].key.currentState?._textFocusNode.requestFocus();
        }
      });
    }
  }

  void focusAdjacentIdea(int currentIndex, int direction) {
    if (currentIndex + direction >= 0 &&
        currentIndex + direction < ideas.length) {
      ideas[currentIndex + direction]
          .key
          .currentState
          ?._textFocusNode
          .requestFocus();
    }
  }

  void redrawAffectedIdeas(int startIndex, int endIndex) {
    if (startIndex < 0 || endIndex >= ideas.length) return;
    setState(() {
      for (int i = startIndex; i <= endIndex; i++) {
        ideas[i].key.currentState?.updateIndentLevel();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return LaneInheritedWidget(
      laneState: this,
      child: ListView.builder(
        itemCount: ideas.length,
        itemBuilder: (context, index) {
          return ideas[index];
        },
      ),
    );
  }
}

class LaneInheritedWidget extends InheritedWidget {
  final _LaneState laneState;

  LaneInheritedWidget({required this.laneState, required Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(LaneInheritedWidget oldWidget) => true;

  static _LaneState of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<LaneInheritedWidget>()!
        .laneState;
  }
}

class Idea extends StatefulWidget {
  int index;
  final GlobalKey<_IdeaState> key;
  final double indentWidth; //インデントの下げ幅
  final double boxSpacing; //他のエディタとの間隔
  final double textFieldHeight; //テキスト欄の高さ
  final int initialIndentLevel;

  Idea({
    required this.key,
    required this.index,
    this.indentWidth = 40.0,
    this.boxSpacing = 8.0,
    this.textFieldHeight = 48.0, //デフォルトは48
    this.initialIndentLevel = 0,
  }) : super(key: ValueKey('Idea'+key.toString()));

  @override
  _IdeaState createState() => _IdeaState();
}

class _IdeaState extends State<Idea> {
  TextEditingController _textController = TextEditingController();
  int indentLevel = 0;
  FocusNode _textFocusNode = FocusNode();

  void updateIndex(int newIndex) {
    setState(() {
      widget.index = newIndex;
    });
  }

  void updateIndentLevel() {
    setState(() {
      indentLevel = widget.key.currentState?.indentLevel ?? indentLevel;
    });
  }

  // インデントの線を描画する
  void _indentLinePaint(Canvas canvas, Size size) {
    indentLinePaint(canvas, size, context, widget.index, indentLevel,
        widget.textFieldHeight, widget.boxSpacing, widget.indentWidth);
  }

  @override
  void initState() {
    super.initState();
    indentLevel = widget.initialIndentLevel;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      FocusScope.of(context).requestFocus(_textFocusNode);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      onKey: (FocusNode node, RawKeyEvent event) {
        if (event.runtimeType == RawKeyDownEvent) {
          if (event.logicalKey == LogicalKeyboardKey.capsLock) {
            // CapsLockキーを無視する条件を追加
            return KeyEventResult.ignored;
          } else if (event.isControlPressed) { //コントロールキー
            bool needRepaint = false;
            if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
              LaneInheritedWidget.of(context).moveIdea(widget.index, -1);
              needRepaint = true;
            } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
              LaneInheritedWidget.of(context).moveIdea(widget.index, 1);
              needRepaint = true;
            }
            if (needRepaint) {
              findAndRepaintAffectedIdeas(context, widget.index, indentLevel,
                  widget.index, widget.index + 1 /*Ctrlキーの場合、1つ下からチェックが必要*/);
            }
          } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { //カーソルキー ↑
            LaneInheritedWidget.of(context)
                .focusAdjacentIdea(widget.index, -1);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
            if (widget.index ==
                LaneInheritedWidget.of(context).ideas.length - 1) {
              LaneInheritedWidget.of(context).addNewIdea(widget.index);
            } else {
              LaneInheritedWidget.of(context)
                  .focusAdjacentIdea(widget.index, 1);
            }
          } else if (event.logicalKey == LogicalKeyboardKey.arrowRight && //カーソルキー →
              _textController.selection.baseOffset ==
                  _textController.text.length) {
            int laneIndex = CanvasZoneInheritedWidget.of(context)
                .lanes
                .indexOf(LaneInheritedWidget.of(context).widget);
            CanvasZoneInheritedWidget.of(context)
                .focusNextLane(laneIndex + 1, widget.index);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft &&
              _textController.selection.baseOffset == 0) {
            int laneIndex = CanvasZoneInheritedWidget.of(context)
                .lanes
                .indexOf(LaneInheritedWidget.of(context).widget);
            CanvasZoneInheritedWidget.of(context)
                .focusNextLane(laneIndex - 1, widget.index);
          } else if (event.logicalKey == LogicalKeyboardKey.tab) { //TABキー
            if (event.isShiftPressed) {
              setState(() {
                indentLevel = max(0, indentLevel - 1);
              });
            } else {
              setState(() {
                indentLevel += 1;
              });
            }

            findAndRepaintAffectedIdeas(context, widget.index, indentLevel,
                widget.index, widget.index);

            return KeyEventResult.handled; //デフォルトのタブの挙動を無効化して、フォーカスの移動を防ぐ
          } else if ((event.logicalKey == LogicalKeyboardKey.delete ||
                  event.logicalKey == LogicalKeyboardKey.backspace) &&
              _textController.text.isEmpty) {
            LaneInheritedWidget.of(context).deleteIdea(widget.index);
          }
        }
        return KeyEventResult.ignored;
      },
      child: RawKeyboardListener(
        focusNode: FocusNode(),
        child: Container(
          margin: EdgeInsets.symmetric(
              horizontal: 16, vertical: widget.boxSpacing),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              const SizedBox(
                  width: 26,
                  child: DecoratedBox(
                      decoration: BoxDecoration(color: Colors.red))),
              Expanded(
                flex: 4,
                child: Row(children: [
                  CustomPaint(
                    painter: _CustomPainter(_indentLinePaint),
                    child: Container(width: widget.indentWidth * indentLevel),
                  ),
                  Expanded(
                    child: Container(
                      height: widget.textFieldHeight,
                      child: TextField(
                        controller: _textController,
                        focusNode: _textFocusNode,
                        maxLines: 1,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                        ),
                        keyboardType: TextInputType.multiline,
                        textInputAction: TextInputAction.done,
                        onSubmitted: (value) {
                          _textFocusNode.unfocus();
                          LaneInheritedWidget.of(context)
                              .addNewIdea(widget.index);
                        },
                      ),
                    ),
                  ),
                ]),
              ),
            ],
          ),
        ),
      ));
  }
}

class _CustomPainter extends CustomPainter {
  final void Function(Canvas, Size) _paint;

  _CustomPainter(this._paint);

  @override
  void paint(Canvas canvas, Size size) {
    _paint(canvas, size);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

※本記事は、過去の記録(メモ)をもとに、他の方が読んで分かるように加筆・補足して掲載しています

テキストベースの思考整理ツール「アイディア・レーン」最新版はこちら

コメント

タイトルとURLをコピーしました