ついにアイディア同士のリレーションがつながった – 開発日記(10)

2023/04/06

関連付けたIdea同士の線を描画をしようと思ったがうまくいかない。

今の描画状況を確認するためにデバッガで表示を変えてみた。
こういった機能があるのはありがたい。

2023/04/07

引き続きアイディア同士を線で結ぶ機能(リレーション)の作成。

Paint()を使って描画することも探りながらなので、座標など、描画位置を確認しつつ実装を試行錯誤。

そしてついに線がつながった。ソースも載せておきます。

flutterのソースファイル:main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'indent_line_painter.dart';
import 'dart:math';
import 'dart:convert'; //JSON形式のデータを扱う
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path_dart; //asが無いとcontextを上書きされてしまう
import 'dart:io';
import 'package:file_selector/file_selector.dart';
//import 'package:flutter/rendering.dart';//debugPaintSizeEnabled用

void main() {
//  debugPaintSizeEnabled = true;
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(child: CanvasZone(key: GlobalKey<_CanvasZoneState>(),)),
      ),
    );
  }
}

StateProvider<bool> _relatedIdeaSelectMode = StateProvider((ref) => false);
StateProvider<Idea?> _originIdeaForSelectMode = StateProvider((ref) => null);

class CanvasZone extends StatefulWidget {
  @override
  final GlobalKey<_CanvasZoneState> key;
  Map<Idea,Idea> relatedIdeasMap = {};

  CanvasZone({
    required this.key,
  }) : super(key: key);

  @override
  State<CanvasZone> createState() => _CanvasZoneState();
}

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

  @override
  void initState() {
    super.initState();
    addNewLane();
    loadData();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      lanes[0].ideas[0].key.currentState?._textFocusNode.requestFocus();
    });
  }

  Future<void> addNewLaneAndFocusTitle(int targetLaneIndex) async {
    if(targetLaneIndex == lanes.length) {
      await addNewLane();
    }
    // ウィジェットが構築されるのを待つ
    WidgetsBinding.instance.addPostFrameCallback((_) {
      lanes[targetLaneIndex].key.currentState?._titleFocusNode.requestFocus();
    });
  }

  Future<void> addNewLane() async {
    setState(() {
      var newLane = Lane(
          key: GlobalKey<_LaneState>(),
          index: lanes.length,
          ideas: [Idea(key: GlobalKey<_IdeaState>(), index: 0)]);
      lanes.add(newLane);
      newLane.ideas[0].parentLane = newLane;
    });
    WidgetsBinding.instance.addPostFrameCallback((_) {
      lanes[lanes.length-1].ideas[0].key.currentState?._textFocusNode.requestFocus();
    });
  }

  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();
    }
    if (targetLaneIndex < lanes.length) {
      for (int i = lanes[targetLaneIndex].ideas.length - 1;
      i < targetIdeaIndex;
      i++) {
        if(lanes[targetLaneIndex].key.currentState == null) {
          setState(() { //addNewIdeaが使えない状態なので、やむなく自力実装
            lanes[targetLaneIndex].ideas.add(Idea(key: GlobalKey<_IdeaState>(), index: i + 1));
            lanes[targetLaneIndex].ideas[lanes[targetLaneIndex].ideas.length-1].parentLane = lanes[targetLaneIndex];
            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 _relationLinePaint(Canvas canvas, Size size) {

    relationLinePaint(canvas, widget);
    //relationLinePaintForDebug(canvas, widget);
  }

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

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

  Future<String> get _defaultFileName async {
    final directory = await getApplicationDocumentsDirectory();
    return '${directory.path}/default.json';
  }

  Future<void> loadData({String? fileName}) async {
    fileName ??= await _defaultFileName;
    final file = File(fileName);
    if (await file.exists()==false) {
      print('No such files:{$fileName}');
      return;
    }
    String jsonString = await file.readAsString();
    Map<String, dynamic> jsonMap = jsonDecode(jsonString);

    List<Lane> loadedLanes = [];
    for (var laneJson in jsonMap['lanes']) {
      List<Idea> loadedIdeas = [];
      var newLane = Lane(
        key: GlobalKey<_LaneState>(),
        index: laneJson['index'] ?? 0,
        title: laneJson['title'] ?? '',
        ideas: loadedIdeas,
      );
      for (var ideaJson in laneJson['ideas']) {
        loadedIdeas.add(Idea(
          key: GlobalKey<_IdeaState>(),
          index: ideaJson['index'],
          indentLevel: ideaJson['indentLevel'],
          parentLane: newLane,
        ));
      }
      loadedLanes.add(newLane);
    }
    setState(() {
      lanes = loadedLanes;
      for (int i = 0; i < lanes.length; i++) {
        for (int j = 0; j < lanes[i].ideas.length; j++) {
          lanes[i].ideas[j].content =
          jsonMap['lanes'][i]['ideas'][j]['content'];
        }
      }
    });

    Map<Idea,Idea> loadedRelatedIdeasMap = {};
    if (jsonMap['relatedIdeas'] != null) {
      for (var relatedJson in jsonMap['relatedIdeas']) {
        var originLaneIndex = relatedJson['origin_lane_index'];
        var originIdeaIndex = relatedJson['origin_idea_index'];
        var relatedLaneIndex = relatedJson['related_lane_index'];
        var relatedIdeaIndex = relatedJson['related_idea_index'];
        loadedRelatedIdeasMap[loadedLanes[originLaneIndex].ideas[originIdeaIndex]] = loadedLanes[relatedLaneIndex].ideas[relatedIdeaIndex];
//        loadedLanes[originLaneIndex].ideas[originIdeaIndex].relatedIdeas.add(loadedLanes[relatedLaneIndex].ideas[relatedIdeaIndex]);
      }
      setState(() {
        widget.relatedIdeasMap = loadedRelatedIdeasMap;
        if(widget.relatedIdeasMap.isNotEmpty) {
          widget.relatedIdeasMap.forEach((key, value) {
            print("[loadData] origin lane:${key.parentLane?.index},idea:${key
                .index} , related lane:${value.parentLane?.index},idea:${value
                .index}");
          });
        }
      });
    }
  }

  Future<void> loadDataWithDialog() async {
    const XTypeGroup typeGroup = XTypeGroup(
      label: 'json',
      extensions: ['json'],
    );
    final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]);
    if (file != null) {
      loadData(fileName: file.path);
    }
  }

  Future<void> saveDataWithDialog() async {
    String? path = await getSavePath(
      acceptedTypeGroups: [
        const XTypeGroup(label: 'json', extensions: ['json'])
      ],
      suggestedName: "",
    );
    if (path != null) {
      if ( path_dart.extension(path) == '' ) {
        path = '$path.json';
      }
      saveData(fileName: path);
    }
  }

  Future<void> saveData({String? fileName}) async {
    List<Map<String, dynamic>> lanesJson = [];

    fileName ??= await _defaultFileName;

    for (var lane in lanes) {
      List<Map<String, dynamic>> ideasJson = [];
      for (var idea in lane.ideas) {
        ideasJson.add({
          'index': idea.index,
          'indentLevel': idea.indentLevel,
          'content': idea.key.currentState?._textController.text ?? '',
        });
      }
      lanesJson.add({
        'key': lane.key.toString(),
        'index': lane.index,
        'title': lane.key.currentState?._titleController.text ?? '',
        'ideas': ideasJson,
      });
    }

    List<Map<String, dynamic>> relatedIdeasJson = [];
    widget.relatedIdeasMap.forEach((origin, related) {
      relatedIdeasJson.add({
        'origin_lane_index': origin.parentLane?.index,
        'origin_idea_key': origin.key.toString(),
        'origin_idea_index': origin.index,
        'related_lane_index': related.parentLane?.index,
        'related_idea_key': related.key.toString(),
        'related_idea_index': related.index,
      });
    });

    String jsonString = jsonEncode({'lanes': lanesJson, 'relatedIdeas': relatedIdeasJson});
    await File(fileName).writeAsString(jsonString);
  }

  @override
  Widget build(BuildContext context) {
    return CanvasZoneInheritedWidget(
      canvasZoneState: this,
      child: Column(
        children: [
          Align(
            alignment: Alignment.topCenter,
            child: Container(
              margin: const EdgeInsets.only(top: 10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: () {

                    },
                    child: const Text("テスト"),
                  ),
                  const SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: addNewLane,
                    child: const Text("レーン追加"),
                  ),
                  const SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: removeLane,
                    child: const Text("レーン削除"),
                  ),
                  const SizedBox(width: 30),
                  ElevatedButton(
                    onPressed: scaleUp,
                    child: const Text("拡大"),
                  ),
                  const SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: scaleDown,
                    child: const Text("縮小"),
                  ),
                  const SizedBox(width: 30),
                  ElevatedButton(
                    onPressed: saveData,
                    child: const Text("保存"),
                  ),
                  const SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: loadData,
                    child: const Text("読込"),
                  ),
                  const SizedBox(width: 30),
                  ElevatedButton(
                    onPressed: saveDataWithDialog,
                    child: const Text("名前を付けて保存"),
                  ),
                  const SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: loadDataWithDialog,
                    child: const Text("ファイルを読込"),
                  ),
                ],
              ),
            ),
          ),
          const Divider(),
          Expanded(
            child: Transform.scale(
              scale: _scale,
              alignment: Alignment.topLeft,
              child: Container(
                width: double.infinity,
                height: double.infinity,
                color: Colors.transparent, // カラーを指定しないと上のGestureDetectorの範囲がchildの範囲に収まってしまう
                child: Stack(
                  children: <Widget>[
                    ListView.builder(
                      scrollDirection: Axis.horizontal,
                      itemCount: lanes.length,
                      itemBuilder: (context, index) {
                        return SizedBox(
                          width: MediaQuery.of(context).size.width * 0.4,
                          child: lanes[index],
                        );
                      },
                    ),
                    CustomPaint(
                      painter: _CustomPainterB(_relationLinePaint),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

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 {
  @override
  final GlobalKey<_LaneState> key;
  int index;
  String title;
  List<Idea> ideas;
  final double indentWidth=40; //インデントの下げ幅

  Lane({
    required this.key,
    required this.index,
    required this.ideas,
    this.title = '',
  }) : super(key: ValueKey('Lane$key'));

  @override
  State<Lane> createState() => _LaneState();
}

class _LaneState extends State<Lane> {
  List<Idea> get ideas => widget.ideas;
  final TextEditingController _titleController = TextEditingController();
  final FocusNode _titleFocusNode = FocusNode();

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

    WidgetsBinding.instance.addPostFrameCallback((_) {
      ideas[currentIndex+1].key.currentState?._textFocusNode.requestFocus();
    });

  }

  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 == 0 && direction <= 0) {
      FocusScope.of(context).requestFocus(_titleFocusNode);
    } else 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?.indentLevel = widget.key.currentState?.indentLevel ?? indentLevel;
      }
    });
  }

  @override
  void initState() {
    super.initState();
    _titleController.text = widget.title;
  }

  @override
  Widget build(BuildContext context) {
    return LaneInheritedWidget(
      laneState: this,
      child: Column(
          children: [
            Focus(
                onKey: (FocusNode node, RawKeyEvent event) {
                  if (event.runtimeType == RawKeyDownEvent) {
                    if (event.logicalKey == LogicalKeyboardKey.arrowDown) { //カーソルキー ↓
                      ideas[0].key.currentState?._textFocusNode.requestFocus();
                    } else if (event.logicalKey == LogicalKeyboardKey.arrowRight && //カーソルキー →
                        _titleController.selection.baseOffset ==
                            _titleController.text.length) {
                      CanvasZoneInheritedWidget
                          .of(context).addNewLaneAndFocusTitle(widget.index + 1);
                    } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft && //カーソルキー ←
                        _titleController.selection.baseOffset == 0) {
                      if(widget.index > 0) {
                        CanvasZoneInheritedWidget
                            .of(context).lanes[widget.index-1].key.currentState?._titleFocusNode.requestFocus();
                      }
                    }            }
                  return KeyEventResult.ignored;
                },
                child: Container(
                  color: Colors.blue[50],
                  padding: EdgeInsets.symmetric(horizontal: widget.indentWidth),
                  child: TextField(
                    controller: _titleController,
                    focusNode: _titleFocusNode,
                    decoration: InputDecoration(
                      border: InputBorder.none,
                      disabledBorder: const OutlineInputBorder(
                        borderSide: BorderSide(color: Colors.blue, width: 2.0),
                      ),
//                labelText: '',
                      fillColor: Colors.blue[50],
                      filled: true,
                    ),
                    style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.blue[900]),
                    keyboardType: TextInputType.text,
                    textInputAction: TextInputAction.done,
                    onSubmitted: (value) {
                      _titleFocusNode.unfocus();
                    },
                  ),
                )),
            Expanded(
              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 ConsumerStatefulWidget {
  @override
  final GlobalKey<_IdeaState> key;
  int index;
  int indentLevel;
  final double boxSpacing; //他のエディタとの間隔
  final double textFieldHeight; //テキスト欄の高さ
  String content;
  Lane? parentLane;
  List<Idea> relatedIdeas = [];
  GlobalKey textFieldKey = GlobalKey();

  Idea({
    required this.key,
    required this.index,
    this.parentLane,
    this.boxSpacing = 8.0,
    this.textFieldHeight = 48.0, //デフォルトは48
    this.indentLevel = 0,
    this.content = '',
  }) : super(key: ValueKey('Idea$key'));

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

class _IdeaState extends ConsumerState<Idea> with AutomaticKeepAliveClientMixin<Idea> {
  final TextEditingController _textController = TextEditingController();
  final FocusNode _textFocusNode = FocusNode();
  Key? repaintBoundaryKey;

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

  void updateIndentLevel() {
    setState(() {
    });
  }

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

  String toJson() {
    Map<String, dynamic> data = {
      'index': widget.index,
      'indentLevel': widget.indentLevel,
      'text': _textController.text,
    };
    return json.encode(data);
  }

  @override
  void initState() {
    super.initState();
    _textController.text = widget.content;
  }

  @override
  bool get wantKeepAlive => true; //これが無いと画面外から出たときに中身も消えてしまう

  @override
  Widget build(BuildContext context) {
    super.build(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, widget.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(() {
                  widget.indentLevel = max(0, widget.indentLevel - 1);
                });
              } else {
                setState(() {
                  widget.indentLevel += 1;
                });
              }
              findAndRepaintAffectedIdeas(context, widget.index, widget.indentLevel,
                  widget.index, widget.index);
              return KeyEventResult.handled; //デフォルトのタブの挙動を無効化して、フォーカスの移動を防ぐ
            } else if ((event.logicalKey == LogicalKeyboardKey.delete || //DEL & BSキー
                event.logicalKey == LogicalKeyboardKey.backspace) &&
                _textController.text.isEmpty) {
              LaneInheritedWidget.of(context).deleteIdea(widget.index);
            } else if (event.logicalKey == LogicalKeyboardKey.f1) {
              StateController<bool> mode =  ref.watch(_relatedIdeaSelectMode.notifier);
              mode.state = true;
              ref.watch(_originIdeaForSelectMode.notifier).state = widget;
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('IdeaSelectMode:${mode.state} Lane:${CanvasZoneInheritedWidget.of(context).lanes.indexOf(LaneInheritedWidget.of(context).widget)} Idea:${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: LaneInheritedWidget.of(context).widget.indentWidth * widget.indentLevel),
                    ),
                    Expanded(
                      child: SizedBox(
                        height: widget.textFieldHeight,
                        child: RepaintBoundary(
                          key: repaintBoundaryKey,
                          child: TextField(
                              key: widget.textFieldKey,
                              controller: _textController,
                              focusNode: _textFocusNode,
                              maxLines: 1,
                              decoration: const InputDecoration(
                                border: OutlineInputBorder(),
                              ),
                              keyboardType: TextInputType.multiline,
                              textInputAction: TextInputAction.done,
                              onSubmitted: (value) {
                                Offset? globalOffset;
                                if ( ref.watch(_relatedIdeaSelectMode.notifier).state == true) {
                                  StateController<bool> mode = ref.watch(_relatedIdeaSelectMode.notifier);
                                  Idea? origin = ref.watch(_originIdeaForSelectMode.notifier).state;
                                  if( origin != null) {
                                    widget.relatedIdeas.add( origin );
                                    CanvasZoneInheritedWidget.of(context).widget.relatedIdeasMap[origin] = widget;
                                    origin = null;
                                    RenderBox? box = widget.key.currentContext?.findRenderObject() as RenderBox;
                                    globalOffset = box.localToGlobal(Offset.zero);
                                  }
                                  ScaffoldMessenger.of(context).hideCurrentSnackBar();
                                  ScaffoldMessenger.of(context).showSnackBar(
                                    SnackBar(
                                      content: Text('globaolOffset:$globalOffset | originIndex:${widget.relatedIdeas[0].index}  | Lane:${CanvasZoneInheritedWidget.of(context).lanes.indexOf(LaneInheritedWidget.of(context).widget)} Idea:${widget.index}'),
                                    ),
                                  );
                                  mode.state = false;
                                } else {
                                  _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;
}

class _CustomPainterB extends CustomPainter {
  final void Function(Canvas, Size) _paint;
  _CustomPainterB(this._paint);

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

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

indent_line_painter.dart (main.dartと同じフォルダに配置)

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

//関連付けの線を描画する
void relationLinePaint(Canvas canvas,CanvasZone widget) {
  final paint = Paint()
    ..color = Colors.blue
    ..strokeWidth = 2;

  if (widget.relatedIdeasMap.isNotEmpty) {
    widget.relatedIdeasMap.forEach((origin, related) {
      print("origin lane:${origin.parentLane?.index},idea:${origin.index} , related lane:${related.parentLane?.index},idea:${related.index}");

      if (widget.key.currentState == null) return;
      if (widget.key.currentState!.lanes.isEmpty) return;
      if (widget.key.currentState!.lanes[0].ideas.isEmpty) return;

      RenderBox lane0Box = widget.key.currentState!.lanes[0].key.currentContext?.findRenderObject() as RenderBox;
      RenderBox originIdeaBox = origin.key.currentContext?.findRenderObject() as RenderBox;
      RenderBox originIdeaTextFieldBox = origin.textFieldKey.currentContext?.findRenderObject() as RenderBox;
      RenderBox relatedIdeaBox = related.key.currentContext?.findRenderObject() as RenderBox;
      RenderBox relatedIdeaTextFieldBox = related.textFieldKey.currentContext?.findRenderObject() as RenderBox;

      double originIdeaY = originIdeaBox.localToGlobal(Offset.zero).dy - lane0Box.localToGlobal(Offset.zero).dy + (originIdeaBox.size.height / 2 );
      double originTextFieldX = originIdeaTextFieldBox.localToGlobal(Offset.zero).dx;
      if (origin.parentLane!.index < related.parentLane!.index) {
        originTextFieldX += originIdeaTextFieldBox.size.width;
      }

      canvas.drawCircle(Offset(originTextFieldX, originIdeaY) , 3, paint);

      double relatedIdeaY = relatedIdeaBox.localToGlobal(Offset.zero).dy - lane0Box.localToGlobal(Offset.zero).dy + (relatedIdeaBox.size.height / 2 );
      double relatedTextFieldX = relatedIdeaTextFieldBox.localToGlobal(Offset.zero).dx;
      if (origin.parentLane!.index > related.parentLane!.index) {
        relatedTextFieldX += relatedIdeaTextFieldBox.size.width;
      }

      canvas.drawCircle(Offset(relatedTextFieldX, relatedIdeaY) , 3, paint);
      canvas.drawLine(Offset(originTextFieldX, originIdeaY), Offset(relatedTextFieldX, relatedIdeaY), paint);

//      print("origin:${originOffset} | originTextBox:${originTextBox1.localToGlobal(Offset.zero)} originTextBox+Width:${originX} | $globalOffset2,");
    });
  }
}

void relationLinePaintForDebug(Canvas canvas, CanvasZone widget) {
  //ツールバーの下:Offset(0,0) 又は canvasZone..RenderObject()のTop
  //lanes[0]..RenderObject().localToGlobalのTopは、なぜかIdeasのTopの位置あたりになってしまう(タイトルより下)
  //lanes[0].ideas[0]..RenderObject().localToGlobalのTopは、中途半端に2つめのIdeasのTopのあたりになってしまう
  //とりあえずtitle分下にずらし、idea[0]のTopの位置を求めるには下記で行けそう、
  //allIdeaTopY = idea0box.localToGlobal(Offset.zero).dy - lane0box.localToGlobal(Offset.zero).dy;

  final paint = Paint()
    ..color = Colors.black;
  canvas.drawCircle(const Offset(10,0), 5, paint);

  paint.color = Colors.purple;
  canvas.drawCircle( (widget.key.currentContext?.findRenderObject() as RenderBox).localToGlobal(Offset.zero), 5, paint);

  if (widget.key.currentState == null) return;
  if (widget.key.currentState!.lanes.isEmpty) return;
  if (widget.key.currentState!.lanes[0].ideas.isEmpty) return;

  RenderBox lane0Box = widget.key.currentState!.lanes[0].key.currentContext?.findRenderObject() as RenderBox;
  RenderBox idea0Box = widget.key.currentState!.lanes[0].ideas[0].key.currentContext?.findRenderObject() as RenderBox;
  RenderBox idea0TextFieldBox = widget.key.currentState!.lanes[0].ideas[0].textFieldKey.currentContext?.findRenderObject() as RenderBox;

  paint.color = Colors.red;
  canvas.drawCircle(lane0Box.localToGlobal(Offset.zero), 5, paint);

  paint.color = Colors.green;
  canvas.drawCircle(idea0Box.localToGlobal(Offset.zero), 10, paint);

  double allIdeaTopY = idea0Box.localToGlobal(Offset.zero).dy - lane0Box.localToGlobal(Offset.zero).dy;
  double textFieldLeftX = idea0TextFieldBox.localToGlobal(Offset.zero).dx;
  paint.color = Colors.yellow;
  canvas.drawCircle(Offset(textFieldLeftX + idea0TextFieldBox.size.width, allIdeaTopY + (idea0Box.size.height / 2 )) , 5, paint);
}

void indentLinePaint(Canvas canvas,Size size,BuildContext context, int index, int indentLevel, double textFieldHeight, double boxSpacing, double indentWidth) {
  // インデントの線を描画する
  final paint = Paint()
    ..color = Colors.grey
    ..strokeWidth = 1;
  var leftX = indentWidth / 2;

  // 一行上のIdeaのindentLevelを取得する
  int upperIndentLevel = 0;
  int upperIndex = index - 1;

  //描画:Canvasの起点は左側センター
  if (indentLevel > 0 && index > 0 /*一番上の時はツリーを描画しない*/) {
    //一番上の親が居ない場合の特別対応:自分の上のどこかに、indentLevelが小さい親がいるかチェック
    bool cancelFlg = false; //描画しない特別な場合用
    var i = index - 1;
    while (i >= 0) {
      if ((LaneInheritedWidget.of(context)
          .ideas[i].indentLevel) <
          indentLevel) {
        break;
      }
      i--;
    }
    if (i == -1) cancelFlg = true;

    if (!cancelFlg) {
      //縦の線:左側の基準
      leftX = leftX + (indentWidth * (indentLevel - 1));

      //縦の線:自分の領域内で上に引く
      double startY =
          0 - (textFieldHeight / 2) - (boxSpacing * 2);

      //縦の線:自分の領域外でどのくらい上まで引くかを計算
      upperIndentLevel = LaneInheritedWidget.of(context)
          .ideas[upperIndex].indentLevel;
      if (indentLevel <= upperIndentLevel) {
        while (indentLevel < upperIndentLevel && upperIndex >= 0) {
          startY -= (textFieldHeight + boxSpacing * 2);
          upperIndex--;
          upperIndentLevel = LaneInheritedWidget.of(context)
              .ideas[upperIndex].indentLevel;
        }
        if (indentLevel <= upperIndentLevel) {
          startY -= textFieldHeight / 2;
        }
      }

      //縦の線
      canvas.drawLine(Offset(leftX, 0), Offset(leftX, startY), paint);

      //横の線
      canvas.drawLine(Offset(leftX, size.height),
          Offset(indentWidth * indentLevel, size.height), paint);
    }
  }
}

void findAndRepaintAffectedIdeas(BuildContext context, int myIndex, int myIndentLevel, int startIndex, int endIndex){
  // Find the start index
  for (int i = myIndex - 1; i >= 0; i--) {
    if (LaneInheritedWidget.of(context)
        .ideas[i].indentLevel == myIndentLevel - 1) {
      startIndex = i;
      break;
    }
  }
  // Find the end index
  for (int i = myIndex + 1;
  i < LaneInheritedWidget.of(context).ideas.length;
  i++) {
    if ((LaneInheritedWidget.of(context)
        .ideas[i].indentLevel) >= myIndentLevel) {
      endIndex = i;
      break;
    }
  }
  LaneInheritedWidget.of(context).redrawAffectedIdeas(startIndex, endIndex);
}

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

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

コメント

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