Firebase認証とFirestoreへの保存・読込 – 開発日記(29)

2023/06/01

  • Firebase認証とFirestoreの利用に着手。
    • 色々な日本語のFlutter関連サイトに助けられた。本当に感謝。
    • このブログも制作過程を書いた上でソースを公開しているのは、多少は参考になる人がいるかも知れないと願ってのことです。(勉強しながら作っているので、未熟な部分が多いと思いますが)
  • FirebaseはWindowsアプリに対応していない模様。firedartを使えばなんとかなるのだろうか?(link)

 

2023/06/02

  • Firestoreへの保存・読込を実装中。まだいろいろバグっている。

 

2024/06/03

  • Firestoreへの保存・読込を実装。キャンバス1枚は出来るようになった。複数キャンバスの扱い・切り替えはゼロから実装しないと。

この時点でのflutterソースファイル:main.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'logic_collection.dart';
import 'settings.dart' as setting_dart;
import 'dart:io'; //Platformを判別するときは必要
import 'dart:math';
import 'dart:convert'; //JSON形式のデータを扱う
import 'package:path/path.dart' as path_dart; //asが無いとcontextを上書きされてしまう
import 'package:file_selector/file_selector.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:uuid/uuid.dart';


/*
class MyCustomScrollBehavior extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {
    PointerDeviceKind.touch, // 通常のタッチ入力デバイス
    PointerDeviceKind.mouse, // これを追加! https://qiita.com/Kurunp/items/5112b72e618002a93939
  };
}
*/

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (kIsWeb == false) {
    if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
      //setWindowTitle("コントロールパネル");
    }
  }
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  final GlobalKey<_WholeAppState> wholeAppKey = GlobalKey();

  MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //scrollBehavior: MyCustomScrollBehavior(),
      home: NotificationListener<SizeChangedLayoutNotification>(
        onNotification: (notification) {
          printWhenDebug("window size changed", level: 2);
          wholeAppKey.currentState!.canvasZoneKey.currentState!.reflectLastLaneHeight();
          return true;
        },
        child: SizeChangedLayoutNotifier(
          child: Scaffold(
            body: SafeArea(child:
              WholeApp(key: wholeAppKey),
            ),
          ),
        ),
      ),
    );
  }
}

class WholeApp extends StatefulWidget {
  const WholeApp({Key? key}) : super(key: key);

  @override
  State<WholeApp> createState() => _WholeAppState();
}

class _WholeAppState extends State<WholeApp> {
  GlobalKey<CanvasZoneState> canvasZoneKey = GlobalKey();
  GlobalKey<ToolbarZoneState> toolbarZoneKey = GlobalKey();
  late ToolbarZone toolbarZone;
  late CanvasZone canvasZone;
  double x = 0.0;
  double y = 0.0;

  @override
  void initState() {
    canvasZone = CanvasZone(key: canvasZoneKey);
    toolbarZone = ToolbarZone(key: toolbarZoneKey);
    super.initState();
  }

  void _updateMouseLocation(PointerEvent details) {
    setState(() {
      toolbarZoneKey.currentState!.mouseX = details.position.dx;
      toolbarZoneKey.currentState!.mouseY = details.position.dy;
    });
  }

  @override
  Widget build(BuildContext context) {
    return WholeAppInheritedWidget(
      wholeAppState: this,
      child: MouseRegion(
        onHover: _updateMouseLocation, //座標を表示する
        child: Column(
          children: [
            toolbarZone,
            const Divider(),
            canvasZone,
          ]
        ),
      ),
    );
  }
}

class WholeAppInheritedWidget extends InheritedWidget {
  final _WholeAppState wholeAppState;
  const WholeAppInheritedWidget({required this.wholeAppState, required Widget child, super.key}) : super(child: child);

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

  static _WholeAppState of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<WholeAppInheritedWidget>()!.wholeAppState;
  }
}

class ToolbarZone extends ConsumerStatefulWidget {
  @override
  final GlobalKey<ToolbarZoneState> key;

  const ToolbarZone({required this.key}) : super(key: key);

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

class ToolbarZoneState extends ConsumerState<ToolbarZone> {
  double mouseX = -1;
  double mouseY = -1;

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

  void addNewVerticalLane() {
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.addNewVerticalLane();
  }

  void removeVerticalLane() {
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.setState(() {
      WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.removeLastVerticalLane();
    });
  }
  void removeHorizontalLane() => WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.removeLastHorizontalLane();
  void addNewHorizontalLane() => WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.addNewHorizontalLane();

  void scaleUp() {
    ref.watch(currentScale.notifier).state = ref.read(currentScale)*1.2;
  }

  void scaleDown() {
    ref.watch(currentScale.notifier).state = ref.read(currentScale)*0.85;
  }

  void scaleReset() {
    if (WholeAppInheritedWidget.of(context).canvasZoneKey.currentState == null) return;
    TransformationController controller = WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!._transformationController;
    controller.value = Matrix4.identity();
    ref.watch(currentScale.notifier).state = 1.0;
  }

  void callAccountScreen() async {
    await accountDialog(context);
  }

  void newCanvas() {
    if (WholeAppInheritedWidget.of(context).canvasZoneKey.currentState == null) return;
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.makeNewCanvas();
  }

  void callSettingsScreen() async {
    await setting_dart.SettingDialog(context);
    setState(() {});
  }

  void callTest() => WholeAppInheritedWidget.of(context).canvasZoneKey.currentState?.test();

  Future<void> saveToCloud() async {
    await saveDataToCloud(state: WholeAppInheritedWidget.of(context).canvasZoneKey.currentState);
  }

  Future<void> loadFromCloud() async {
    printWhenDebug("loadFromCloud");
    loadDataFromCloud(state: WholeAppInheritedWidget.of(context).canvasZoneKey.currentState);

    //現在のレーンの高さを計算&反映
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.updateAllHorizontalViewTopAndLaneHeights();
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.updateAllVerticalLaneHeaderViewLeftAndWidths();
    //スクロールポジションを戻す
    //WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!._scrollController.jumpTo(0);
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.setState(() {}); //画面の再描画

    return;
    List<DocumentSnapshot> documentList = [];
    // コレクション内のドキュメント一覧を取得
    final snapshot1 = await FirebaseFirestore.instance
        .collection('users')
        .doc('id_abc')
        .collection('orders')
        .doc('id_567')
        .get();
    int age1 = snapshot1.get('age');
    printWhenDebug("age:$age1");

    final snapshot2 = await FirebaseFirestore.instance.collection('users').get();
    documentList = snapshot2.docs;
    for(int i=0; i<documentList.length; i++) {
      String s = documentList[i].get('name');
      int age = documentList[i].get('age');
      printWhenDebug("document['name']:$s");
      printWhenDebug("document['age']:$age");
    }
  }

  Future<void> loadFromFile( XFile xFile ) async {

    await loadDataFromFile( xFile:xFile, state: WholeAppInheritedWidget.of(context).canvasZoneKey.currentState);

    // contextを渡す前に、contextが現在のWidgetツリー内に存在しているかどうかチェック
    if (!mounted){
      printWhenDebug('【!mounted at】loadFromFile');
      return;
    }

    //現在のレーンの高さを計算&反映
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.updateAllHorizontalViewTopAndLaneHeights();
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.updateAllVerticalLaneHeaderViewLeftAndWidths();
    //スクロールポジションを戻す
    //WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!._scrollController.jumpTo(0);
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.setState(() {}); //画面の再描画
  }

  Future<void> loadDataWithDialog() async {

    /*
      fileName ??= await defaultFileName;
      final file = File(fileName);
    */

    const XTypeGroup typeGroup = XTypeGroup(
      label: 'json',
      extensions: <String>['json'],
    );
    final XFile? xFile = await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
    if (xFile == null) {
      return;
    }

    await loadFromFile(xFile);

    // contextを渡す前に、contextが現在のWidgetツリー内に存在しているかどうかチェック
    if (!mounted){
      printWhenDebug('【!mounted at】loadDataWithDialog');
      return;
    }

    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.updateCanvasParametersAndRequestFocusForInit();
  }

  Future<void> saveAs() async {
    String? path = await getSavePath(
      acceptedTypeGroups: [
        const XTypeGroup(label: 'json', extensions: ['json'])
      ],
      suggestedName: "",
    );

    if (path != null) {
      if (path != "" && //Webの場合は""になる
          path_dart.extension(path) == '') {
        path = '$path.json';
      }
      saveDataToFile(fileName: path, state: WholeAppInheritedWidget.of(context).canvasZoneKey.currentState);
    }
  }

  String getCurrentMousePositionStr() {
    String result = "";
    if (mouseX != -1) {
      result = "x:${mouseX.toInt().toString().padLeft(5,'0')}, y:${mouseY.toInt().toString().padLeft(5,'0')} ";
    }
    return result;
  }

  Widget buildEvaluatedToolButton(Widget text, void Function() f) {
    return
      ElevatedButton(
        onPressed: f,
        child: text,
      );
  }

  Widget buildEvaluatedIconButton2(Widget icon, void Function() f) {
    return
      ElevatedButton(
        onPressed: f,
        style: ElevatedButton.styleFrom(
          fixedSize: const Size.fromWidth(20),//Sizeを使って横幅を設定
            padding: EdgeInsets.zero,
        ),
        child: icon,
      );
  }

  Widget buildEvaluatedIconButton(Widget icon, String toolTipStr, void Function() f) {
    return
      Tooltip(
        message: toolTipStr,
        waitDuration: const Duration(milliseconds: 600),
        child: InkWell(
          onTap: f,
//        overlayColor: MaterialStateProperty.resolveWith(Colors.blue.withOpacity(0.08)),
          child: Container(
            height: 28.0,
            width:  40.0,
            decoration: BoxDecoration(
              color: Colors.blue,
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.circular(4),
            ),
            child: icon,
          ),
        ),
      );
  }

  OutlinedButton facebookIconButtonWithBoolTest(BuildContext context, bool isSwitched, enableDisableElevatedButton()) {
    return OutlinedButton.icon(
      label: Text(''),
      icon: Icon(Icons.web),
      style: OutlinedButton.styleFrom(
          foregroundColor: isSwitched ? Colors.teal[600] : Colors.grey[500],
          side: isSwitched ? BorderSide(color: Colors.teal[600]!) : BorderSide(color: Colors.grey[500]!)),
      onPressed: () {
        if (isSwitched == false) {
          enableDisableElevatedButton();
        } else
          Navigator.pushNamed(context, '/register');
      },
    );
  }

  Widget buildButton(double top, double left, Widget icon, String toolTipStr, void Function(Idea? focusedIdea, {bool fromToolbar}) f, {bool? selected=false, Color? selectedColor}) {
    return
      Tooltip(
        message: toolTipStr,
        waitDuration: const Duration(milliseconds: 600),
        child: IconButton.filledTonal(
          padding: EdgeInsets.zero,
          color: (selected ?? false) ? Colors.blue : ( selectedColor != null ) ? selectedColor : Colors.black,
          splashRadius: 18,
          onPressed: () { f( WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.focusedIdea, fromToolbar: true);},
          icon: icon,
        ),
      );
  }

  List<Widget> getToolButtonsForIdea() {
    List<Widget> result = [];
    double xSize = 25, dx = 0;
    double groupY, groupX;
    groupY= -5; groupX=  5;
    Idea? focusedIdea = WholeAppInheritedWidget.of(context).canvasZoneKey.currentState?.focusedIdea;

    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add), "新規項目", funcAddNewIdeaWithFocus), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add_circle_outline), "新規項目(子)", funcAddNewChildIdeaWithFocus), ); dx +=xSize;
    result.add( const SizedBox(height: 40, child: VerticalDivider(thickness: 1,)) );
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_indent_decrease), "インデント上げ", funcIndentDecrease ), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_indent_increase), "インデント下げ", funcIndentIncrease ), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_down), "下に移動", funcIdeaMoveDown), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_up), "上に移動", funcIdeaMoveUp), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_left), "左のレーンに移動", moveAndInsertIdeaToAnotherLaneLeft), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_right), "右のレーンに移動", moveAndInsertIdeaToAnotherLaneRight), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_down), "右のレーンに移動", moveAndInsertIdeaToAnotherLaneDown), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_up), "上のレーンに移動", moveAndInsertIdeaToAnotherLaneUp), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.polyline_outlined), "関係性を指定", funcStartRelationMode), ); dx +=xSize;
    result.add( const SizedBox(height: 40, child: VerticalDivider(thickness: 1,)) );
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_bold), "太字", funcIdeaStyleToggleBold, selected: focusedIdea?.textStyleBold)); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_italic), "斜線", funcIdeaStyleToggleItalic, selected: focusedIdea?.textStyleItalic), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_underline), "下線", funcIdeaStyleToggleUnderLine, selected: focusedIdea?.textStyleUnderLine), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_strikethrough), "取消線", funcIdeaStyleToggleLineThrough, selected: focusedIdea?.textStyleStrikeThrough), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_color_text), "文字色", funcIdeaStyleTextColor, selectedColor: focusedIdea?.textColor), ); dx +=xSize;

    return result;
  }

  @override
  Widget build(BuildContext context) {
    return ToolbarZoneInheritedWidget(
      toolbarZoneState: this,
      child: Column(
        children: [
          Align(
            alignment: Alignment.centerLeft,
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Container( //ツールバー
                margin: const EdgeInsets.only(top: 10),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const SizedBox(width: GlobalConst.horizontalLaneHeaderDefaultWidth),
                    Column(
                      children: [
                        const Text("縦レーン"),
                        Row(
                          children: [
                            buildEvaluatedToolButton(const Text("右に追加"), addNewVerticalLane),
                            const SizedBox(width: 10),
                            buildEvaluatedToolButton(const Text("削除"), removeVerticalLane),
                          ],
                        )
                      ]
                    ),
                    const SizedBox(width: 30),
                    Column(
                        children: [
                          const Text("横レーン"),
                          Row(
                            children: [
                              buildEvaluatedToolButton(const Text("下に追加"), addNewHorizontalLane),
                              const SizedBox(width: 10),
                              buildEvaluatedToolButton(const Text("削除"), removeHorizontalLane),
                            ],
                          )
                        ]
                    ),
                    const SizedBox(width: 30),
                    Column(
                      children: [
                        const Text("拡大縮小"),
                        Row(
                          children: [
                            buildEvaluatedIconButton(const Icon(Icons.zoom_in, color: Colors.white), "拡大", scaleUp),
                            const SizedBox(width: 5),
                            buildEvaluatedIconButton(const Icon(Icons.zoom_out, color: Colors.white), "縮小", scaleDown),
                            const SizedBox(width: 5),
                            buildEvaluatedIconButton(const Icon(Icons.horizontal_rule, color: Colors.white), "リセット", scaleReset),
                          ],
                        )
                      ]
                    ),
                    const SizedBox(width: 30),
                    Column(
                        children: [
                          const Text("クラウド"),
                          Row(
                            children: [
                              const SizedBox(width: 10),
                              buildEvaluatedIconButton(const Icon(Icons.person, color: Colors.white), "アカウント", callAccountScreen),
                              const SizedBox(width: 10),
                              buildEvaluatedToolButton(const Text("保存"), saveToCloud),
                              const SizedBox(width: 10),
                              buildEvaluatedToolButton(const Text("読込"), loadFromCloud),
                            ],
                          )
                        ]
                    ),
                    const SizedBox(width: 30),
                    Column(
                      children: [
                        const Text("ファイル"),
                        Row(
                          children: [
                            buildEvaluatedToolButton(const Text("別名保存"), saveAs),
                            const SizedBox(width: 10),
                            buildEvaluatedToolButton(const Text("読込"), loadDataWithDialog),
                            const SizedBox(width: 10),
                            buildEvaluatedToolButton(const Text("新規"), newCanvas),
                          ],
                        )
                      ]
                    ),
                    const SizedBox(width: 30),
                    Column(
                      children: [
                        const Text("その他"),
                        Row(
                          children: [
                            ElevatedButton.icon(
                              onPressed: callSettingsScreen,
                              icon: const Icon(Icons.settings),
                              label: const Text("設定"),
                            ),
                          ],
                        )
                      ]
                    ),
                    const SizedBox(width: 30),
                    Column(
                      children: [
                        const Text(" "),
                        Row(
                          children: [
                            buildEvaluatedToolButton(const Text("Test"), callTest),
                            const SizedBox(width: 10),
                            Text(getCurrentMousePositionStr()),
                          ],
                        )
                      ]
                    ),
                  ],
                ),
              ),
            ),
          ),
          const Divider(),
          Align(
            alignment: Alignment.centerLeft,
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Container( //ツールバー
                margin: const EdgeInsets.fromLTRB(GlobalConst.horizontalLaneHeaderDefaultWidth,0,20,0),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: getToolButtonsForIdea(),
                ),
              ),
            ),
          ),
        ]
      ),
    );
  }
}

class ToolbarZoneInheritedWidget extends InheritedWidget {
  final ToolbarZoneState toolbarZoneState;
  const ToolbarZoneInheritedWidget({required this.toolbarZoneState, required Widget child, super.key}) : super(child: child);

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

  static ToolbarZoneState of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ToolbarZoneInheritedWidget>()!.toolbarZoneState;
  }
}

class HorizontalLane {
  int index = 0;
  double viewHeight = GlobalConst.horizontalLaneDefaultViewHeight;
  double realHeight = GlobalConst.horizontalLaneDefaultViewHeight;
  double viewTop = 0;
  late HorizontalLaneHeader header;
  final scrollControllerForHLane = ScrollController();
  List<LaneBody> childVLaneBodies = [];

  double getRealHeight() {
    double result = 1;
    for (var element in childVLaneBodies) {
      if( element.requiredHeight > result ) {
        result = element.requiredHeight;
      }
    }
    return result;
  }

  HorizontalLane(this.index, String title) {
    header = HorizontalLaneHeader(index: index, relatedLane: this, title: title,);
    scrollControllerForHLane.addListener(_scrollVListener);
  }

  void _scrollVListener() {
    if(header.key.currentState != null) {
      CanvasZoneInheritedWidget.of(header.key.currentState!.context).setState(() { });
    }
  }
}

class CanvasInfo {
  // IDを生成する
  var canvasId = const Uuid().v4();
  String title='';
  DateTime createDatetime = DateTime.now();
  DateTime? exportedDatetime;
  DateTime? cloudUpdatedDatetime;
}

class CanvasZone extends ConsumerStatefulWidget {
  const CanvasZone({Key? key}) : super(key: key);

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

class CanvasZoneState extends ConsumerState<CanvasZone> {
  List<HorizontalLane> horizontalLanes = [];
  double horizontalLaneHeaderWidth = GlobalConst.horizontalLaneHeaderDefaultWidth;
  double allSumHorizontalViewHeights=500;
  double allSumVerticalHeaderViewWidths=500;

  List<LaneBody> firstVerticalLanes = [];
  List<VerticalLaneHeader> verticalLaneHeaders = [];
  final TransformationController _transformationController = TransformationController();
  double globalTop = 0.0;
  double contextDxAdjust=0.0, contextDyAdjust=0.0;
  Idea? focusedIdea;

  //スクロールバー
  final GlobalKey scrollbarVKey = GlobalKey();
  final GlobalKey scrollbarHKey = GlobalKey();
  final scrollControllerVertical = ScrollController();
  final scrollControllerHorizontal = ScrollController();

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

    scrollControllerVertical.addListener(_scrollVListener);
    scrollControllerHorizontal.addListener(_scrollHListener);

    WidgetsBinding.instance.addPostFrameCallback((_) {
      () async{
        makeNewCanvas();
        await loadDataAtStartUp();
      }();
    });
  }

  Future<void> loadDataAtStartUp() async {
    // await loadDataFromFile(state: this);
    // updateCanvasParametersAndRequestFocusForInit();
  }

  void makeNewCanvas(){
    clearCurrentData();
    addNewHorizontalLane();
    updateCanvasParametersAndRequestFocusForInit();
  }

  void test() async {
    optimizeHorizontalViewAndHeights(skipFlg: false);
    setState(() {});
    printWhenDebug("-----");
    printWhenDebug("MediaQuery.of(context).size.height:${MediaQuery.of(context).size.height}");
    printWhenDebug("globalTop:$globalTop");
    printWhenDebug("ref.watch(currentScale):${ref.watch(currentScale)}");
    printWhenDebug("allSumHorizontalViewHeights:$allSumHorizontalViewHeights");
    printWhenDebug("allSumVerticalHeaderViewWidths:$allSumVerticalHeaderViewWidths");
    printWhenDebug("ref.watch(canvasScrollDelta) X:${ref.watch(canvasScrollDeltaX)} Y:${ref.watch(canvasScrollDeltaY)}");
    printWhenDebug("horizontalLanes.length:${horizontalLanes.length}");
    for(int i=0; i< horizontalLanes.length; i++) {
      printWhenDebug("---");
      printWhenDebug("horizontalLanes[$i].index:${horizontalLanes[i].index} title:${horizontalLanes[i].header.title}");
      printWhenDebug("..viewTop:${horizontalLanes[i].viewTop}");
      printWhenDebug("..viewHeight:${horizontalLanes[i].viewHeight}");
      printWhenDebug("..childVLaneBodies.length:${horizontalLanes[i].childVLaneBodies.length}");
      for(int v=0; v< horizontalLanes[i].childVLaneBodies.length; v++) {
        printWhenDebug("..childVLaneBodies[$v].indexY:${horizontalLanes[i].childVLaneBodies[v].indexY} indexX:${horizontalLanes[i].childVLaneBodies[v].indexX}"
              " length:${horizontalLanes[i].childVLaneBodies[v].ideas.length}");
      }
    }
    printWhenDebug("ref.watch(ideasRelationList).length:${ref.watch(ideasRelationList).length}");
    for (IdeasRelation relationSet in ref.watch(ideasRelationList)) {
      printWhenDebug("relationSet.fromIdea LaneY:${relationSet.fromIdea.parentLane.indexY} LaneX:${relationSet.fromIdea.parentLane.indexX} index:${relationSet.fromIdea.index}");
      printWhenDebug("relationSet.toIdea   LaneY:${relationSet.toIdea.parentLane.indexY} LaneX:${relationSet.toIdea.parentLane.indexX} index:${relationSet.toIdea.index}");
    }
  }

  void clearCurrentData() {
    printWhenDebug("clearCurrentData();", level: 2);
    focusedIdea = null; //コンテキストメニュー対策
    for(int h=horizontalLanes.length-1; h>=0; h--) {
      removeLastHorizontalLane();
    }
    verticalLaneHeaders.clear();
    horizontalLanes.clear();
    horizontalLaneHeaderWidth = GlobalConst.horizontalLaneHeaderDefaultWidth;
    ref.watch(ideasRelationList.notifier).state.clear();
  }

  void updateCanvasParametersAndRequestFocusForInit() {
    //CanvasのTop座標を取得(スクロール制御用)
    globalTop = (WholeAppInheritedWidget.of(context).canvasZoneKey.currentContext!.findRenderObject() as RenderBox).localToGlobal(Offset.zero).dy;
    printWhenDebug("【update globalTop】$globalTop");

    //レーンの幅を決定
    ref.watch(defaultEachLaneWidth.notifier).state = MediaQuery.of(context).size.width * 0.4;

    //レーンの数に合わせて全体の高さや見た目を調整
    optimizeHorizontalViewAndHeights();

    //現在のレーンの高さと幅を計算&反映
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.updateAllVerticalLaneHeaderViewLeftAndWidths();
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.updateAllHorizontalViewTopAndLaneHeights();

    //画面の描画とフォーカスの設定
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.setState(() {}); //画面の再描画

    if(horizontalLanes.isNotEmpty && horizontalLanes[0].childVLaneBodies.isNotEmpty && horizontalLanes[0].childVLaneBodies[0].ideas.isNotEmpty) {
      printWhenDebug("updateCanvasParametersAndRequestFocusForInit:Focus");
      horizontalLanes[0].childVLaneBodies[0].ideas[0].key.currentState?.textFocusNode.requestFocus();
    }
  }

  String getDefaultVertLaneTitle(int index) {
    return '縦レーン$index';
  }

  double getCanvasLaneZoneViewHeightMinusBottomSpace() {
    return (MediaQuery.of(context).size.height - globalTop - (GlobalConst.verticalLaneHeaderHeight*ref.watch(currentScale)) - 35/*余白*/) * (1 / ref.watch(currentScale));
  }

  void reflectLastLaneHeight() {
    optimizeHorizontalViewAndHeights();
    updateAllHorizontalViewTopAndLaneHeights();
  }

  //レーンの数に合わせて全体の高さや見た目を調整
  void optimizeHorizontalViewAndHeights({bool skipFlg = true}) {
    if(skipFlg) return;
    const minimumRealHeight = GlobalConst.eachIdeaHeight *1.6;
    double canvasViewHeightMinusBottomSpace = getCanvasLaneZoneViewHeightMinusBottomSpace();

    //横レーンの高さの調整
    if(horizontalLanes.length == 1) {
      //横レーンが1つの場合
      horizontalLanes[0].viewHeight = max(
        GlobalConst.horizontalLaneDefaultViewHeight,
        horizontalLanes[0].getRealHeight() + GlobalConst.eachIdeaHeight/3 /*バッファ*/  // canvasViewHeightMinusBottomSpace;
      );

      printWhenDebug("canvasViewHeightMinusBottomSpace:$canvasViewHeightMinusBottomSpace");
      //縦のサイズが画面に収まらない場合
      if(horizontalLanes[0].viewHeight>horizontalLanes[0].viewHeight) {
        horizontalLanes[0].viewHeight = canvasViewHeightMinusBottomSpace;
      }
    } else {
      //複数レーンがある状態
      var restHeight = canvasViewHeightMinusBottomSpace;
      if (horizontalLanes[0].getRealHeight() <= canvasViewHeightMinusBottomSpace * 0.6) {
        horizontalLanes[0].viewHeight = max(minimumRealHeight, horizontalLanes[0].getRealHeight());
      } else {
        horizontalLanes[0].viewHeight = canvasViewHeightMinusBottomSpace * 0.6;
      }
      restHeight = restHeight - horizontalLanes[0].viewHeight;

      for (int i=1; i<horizontalLanes.length; i++){
        if(restHeight <= 0) break;

        if(i==horizontalLanes.length-1) {
          //最後のレーンの場合
          horizontalLanes[i].viewHeight = restHeight;
        } else {
          horizontalLanes[i].viewHeight = min( max(minimumRealHeight, horizontalLanes[i].getRealHeight()), restHeight);
        }
        restHeight = restHeight - horizontalLanes[i].viewHeight;
      }
    }

    //横レーンのヘッダー領域の幅
    if( (horizontalLanes.length == 1) &&
          (
            (horizontalLanes[0].header.title == '') ||
            (horizontalLanes[0].header.title == getDefaultVertLaneTitle(0))
          ) ){
      horizontalLaneHeaderWidth = GlobalConst.horizontalLaneHeaderDefaultWidth / 2;
    } else {
      horizontalLaneHeaderWidth = GlobalConst.horizontalLaneHeaderDefaultWidth;
    }

    updateAllHorizontalViewTopAndLaneHeights();
  }

  List<Widget> getVerticalLanesHeaders() {
    List<Widget> result = [];
    double currentLeft;

    //ヘッダー本体の描画
    currentLeft = 0;
    for(int i=0; i<verticalLaneHeaders.length; i++) {
      result.add(
        Positioned( //ヘッダー
          left: currentLeft,
          top: 0,
          child: Container(
            height: GlobalConst.verticalLaneHeaderHeight,
            width: verticalLaneHeaders[i].viewWidth,
            color: Colors.yellow,
            child: verticalLaneHeaders[i],
          ),
        ),
      );
      currentLeft += verticalLaneHeaders[i].viewWidth + GlobalConst.laneSplitterBodyWidthOrHeight; //区切り線を考慮
    }

    //区切り線の描画(本体描画後に描くことで上に載せている)
    currentLeft = 0;
    for(int i=0; i<verticalLaneHeaders.length; i++) {
      result.add(
        Positioned( //区切り線
          left: currentLeft + verticalLaneHeaders[i].viewWidth + GlobalConst.laneSplitterBodyWidthOrHeight/2 - (GlobalConst.laneSplitterHeaderWidthOrHeight)/2,
          top: 0,
          child: MouseRegion(
            cursor: SystemMouseCursors.resizeColumn,
            child: GestureDetector(
              onPanUpdate: (details) {
                setState(() {
                  verticalLaneHeaders[i].viewWidth = verticalLaneHeaders[i].viewWidth + details.delta.dx;
                  if (verticalLaneHeaders[i].viewWidth < GlobalConst.verticalLaneHeaderMinimumWidth) {
                    verticalLaneHeaders[i].viewWidth = GlobalConst.verticalLaneHeaderMinimumWidth;
                  }
                  updateAllVerticalLaneHeaderViewLeftAndWidths();
                });
              },
              child: Container(
                width: GlobalConst.laneSplitterHeaderWidthOrHeight,
                height: GlobalConst.verticalLaneHeaderHeight,
                color: Colors.blue,
              ),
            ),
          ),
        )
      );
      currentLeft += verticalLaneHeaders[i].viewWidth + GlobalConst.laneSplitterBodyWidthOrHeight;
    }

    return result;
  }

  List<Widget> getHorizontalLanesHeaders() {
    List<Widget> result = [];

    //ヘッダー本体の描画
    for(int i=0; i<horizontalLanes.length; i++) {
      result.add(
        Positioned( //ヘッダー
          left: 0,
          top: horizontalLanes[i].viewTop,
          child: Container(
            height: horizontalLanes[i].viewHeight,
            width: horizontalLaneHeaderWidth,
            color: Colors.yellow,
            child: horizontalLanes[i].header,
          ),
        ),
      );
    }

    //区切り線の描画(本体描画後に描くことで上に載せている)
    for(int i=0; i<horizontalLanes.length; i++) {
      result.add(
        Positioned( //区切り線
          left: 0,
          top: horizontalLanes[i].viewTop + horizontalLanes[i].viewHeight - GlobalConst.laneSplitterHeaderWidthOrHeight / 2,
          child: MouseRegion(
            cursor: SystemMouseCursors.resizeRow,
            child: GestureDetector(
              onPanUpdate: (details) {
                setState(() {
                  horizontalLanes[i].viewHeight = horizontalLanes[i].viewHeight + details.delta.dy;
                  if (horizontalLanes[i].viewHeight < GlobalConst.horizontalLaneHeaderMinimumHeight) {
                    horizontalLanes[i].viewHeight = GlobalConst.horizontalLaneHeaderMinimumHeight;
                  }
                  updateAllHorizontalViewTopAndLaneHeights();
                });
              },
              child: Container(
                width: horizontalLaneHeaderWidth,
                height: GlobalConst.laneSplitterHeaderWidthOrHeight,
                color: Colors.blue,
              ),
            ),
          ),
        )
      );
    }

    return result;
  }

  LaneBody addNewVLaneBodyTo(HorizontalLane targetHLane, {bool withIdea = true}){
    var indexX = targetHLane.childVLaneBodies.length;
    var newLane = LaneBody(indexY:targetHLane.index, indexX: indexX, ideas: []);

    if(withIdea) {
      newLane.ideas.add(Idea(index: 0, parentLane: newLane));
      newLane.updateRequiredHeight();
    }
    targetHLane.childVLaneBodies.add(newLane);

    if (newLane.indexX >= verticalLaneHeaders.length) { //ヘッダーが不足している場合
      verticalLaneHeaders.add( VerticalLaneHeader(index: newLane.indexX, viewWidth: ref.watch(defaultEachLaneWidth)) );
    }
    for(int i=0; i<horizontalLanes.length; i++) { //他の横レーンに、縦レーンがなければ埋める(縦の区切り線の描画を行うためにレーン作成)
      if(i != targetHLane.index) {
        while (horizontalLanes[i].childVLaneBodies.length <= indexX) {
          addNewVLaneBodyTo(horizontalLanes[i], withIdea: false);
        }
      }
    }
    return newLane;
  }

  void addNewVerticalLane({int horizontalLaneIndex = 0, bool withIdea = true}) async {
    setState(() {
      var newLane =  addNewVLaneBodyTo(horizontalLanes[horizontalLaneIndex], withIdea:withIdea);
      updateAllVerticalLaneHeaderViewLeftAndWidths(); //縦レーンのヘッダーサイズを再計算
    });
  }

  updateAllHorizontalViewTop() {
    //Topの計算
    double aViewTop=0;
    for (int i=0; i< horizontalLanes.length; i++) {
      if (i==0) {
        aViewTop = 0;
      } else {
        aViewTop = aViewTop + horizontalLanes[i-1].viewHeight + (GlobalConst.laneSplitterBodyWidthOrHeight);
      }
      horizontalLanes[i].viewTop = aViewTop;
    }
  }

  updateAllVerticalLaneHeaderViewLeftAndWidths() {
    //レーン全体の幅の合計
    double aViewLeft = 0;
    double allWidths = 0;
    for (int i=0; i< verticalLaneHeaders.length; i++) {
      verticalLaneHeaders[i].viewLeft = aViewLeft;
      allWidths = allWidths + verticalLaneHeaders[i].viewWidth + GlobalConst.laneSplitterBodyWidthOrHeight;
      aViewLeft = allWidths;
    }
    allSumVerticalHeaderViewWidths = allWidths;
  }

  updateAllHorizontalViewTopAndLaneHeights() {
    updateAllHorizontalViewTop();

    //レーン全体の高さの計算
    double allHeights = 0;
    for (int i=0; i< horizontalLanes.length; i++) {
      allHeights = allHeights + horizontalLanes[i].viewHeight + GlobalConst.laneSplitterBodyWidthOrHeight;
    }
    allSumHorizontalViewHeights = allHeights;
  }

  void addNewHorizontalLane({bool withIdea = true}) {
    setState(() {
      horizontalLanes.add( HorizontalLane( horizontalLanes.length, 'Title${(horizontalLanes.length+1).toString()}') ); //横レーン作成(ボディ無し)
      addNewVLaneBodyTo(horizontalLanes[horizontalLanes.length-1],withIdea:withIdea); //ボディ作成。縦のヘッダーが足りなければ追加する

      //他の横レーンに、縦レーンがなければ埋める(縦の区切り線の描画を正しく行うためにレーン作成)
      for(int i=0; i<horizontalLanes.length; i++) {
        while (horizontalLanes[i].childVLaneBodies.length < verticalLaneHeaders.length) {
          addNewVLaneBodyTo(horizontalLanes[i], withIdea: false);
        }
      }

      optimizeHorizontalViewAndHeights();
      updateAllHorizontalViewTopAndLaneHeights();
    });
  }

  Future<void> addNewLaneAndFocusTitle(int targetLaneIndex) async {
    if (targetLaneIndex < verticalLaneHeaders.length) {
      verticalLaneHeaders[targetLaneIndex].key.currentState?._titleFocusNode.requestFocus();
    } else {
      addNewVerticalLane();
      // ウィジェットが構築されるのを待ってフォーカス
      WidgetsBinding.instance.addPostFrameCallback((_) {
        verticalLaneHeaders[targetLaneIndex].key.currentState?._titleFocusNode.requestFocus();
      });
    }
  }

  void removeLastVerticalLane() {
    if (verticalLaneHeaders.length > 1) {
      for(int h=0; h<horizontalLanes.length; h++){
        //各横レーンで、現在一番右のレーンに該当するものがあれば削除する
        if(horizontalLanes[h].childVLaneBodies.length == verticalLaneHeaders.length) {
          var targetVLaneBody = horizontalLanes[h].childVLaneBodies[ horizontalLanes[h].childVLaneBodies.length-1 ];
          //関係するリレーションを削除
          for (var idea in targetVLaneBody.ideas) {
            targetVLaneBody.key.currentState?.removeFromRelation(idea);
          }
          horizontalLanes[h].childVLaneBodies.removeLast();
        }
      }
      verticalLaneHeaders.removeLast();
      updateAllVerticalLaneHeaderViewLeftAndWidths();
    }
  }

  void removeLastHorizontalLane() {
    setState(() {
      if (horizontalLanes.length > 1) {
        var targetHorizontalLane = horizontalLanes[horizontalLanes.length-1];
        for(int i=targetHorizontalLane.childVLaneBodies.length-1 ; i>=0 ; i--) {
          //関係するリレーションを削除
          for (var idea in targetHorizontalLane.childVLaneBodies[i].ideas) {
            targetHorizontalLane.childVLaneBodies[i].key.currentState?.removeFromRelation(idea);
          }
        }
        targetHorizontalLane.childVLaneBodies.clear();
        horizontalLanes.remove(targetHorizontalLane);

        optimizeHorizontalViewAndHeights();
        updateAllHorizontalViewTopAndLaneHeights();
      }
    });
  }

  void jumpFocusTo(int targetHLaneIndex, int targetVLaneIndex, int targetIdeaIndex, {bool fillToIndex = true}) {
    if( targetHLaneIndex < 0 || targetVLaneIndex < 0 || targetIdeaIndex < 0 ) return;
    if(fillToIndex == false) {
      if(horizontalLanes.length <= targetHLaneIndex) {
        targetIdeaIndex=0;
      } else if ( horizontalLanes[targetHLaneIndex].childVLaneBodies.length <= targetVLaneIndex ) {
        targetIdeaIndex=0;
      } else if ( horizontalLanes[targetHLaneIndex].childVLaneBodies[targetVLaneIndex].ideas.length <= targetIdeaIndex) {
        targetIdeaIndex= max(0, horizontalLanes[targetHLaneIndex].childVLaneBodies[targetVLaneIndex].ideas.length -1);
      }
    }
    printWhenDebug("jumpFocusTo", level: 2);
    if( checkAndFillIdeas(targetHLaneIndex, targetVLaneIndex, targetIdeaIndex, true) ) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        horizontalLanes[targetHLaneIndex].childVLaneBodies[targetVLaneIndex].ideas[targetIdeaIndex].key.currentState?.textFocusNode.requestFocus(); //FillIdeasされた場合は遅延でのフォーカスが必要
      });
    } else {
      horizontalLanes[targetHLaneIndex].childVLaneBodies[targetVLaneIndex].ideas[targetIdeaIndex].key.currentState?.textFocusNode.requestFocus(); //FillIdeasされなかった場合は即座にフォーカス
    }
  }

  //対象レーンに十分なアイディアがあるかチェックして、必要なら埋める
  bool checkAndFillIdeas(int targetHLaneIndex, int targetVLaneIndex, int targetIdeaIndex, bool fillToIndex) {
    bool added = false; //アイディアが追加されたかどうかで、呼び出し元が処理を分けられるようにする
    bool needManualStatePaint = false;
    if (targetVLaneIndex < 0 || targetIdeaIndex < 0) return added;

    while (targetHLaneIndex >= horizontalLanes.length) {
      //新規に横レーン追加が必要な場合
      WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.addNewHorizontalLane(withIdea: false);
      added = true;
    }

    while (horizontalLanes[targetHLaneIndex].childVLaneBodies.length <= targetVLaneIndex ) {
      //対象横レーンに、縦レーンが足りない場合は足す
      addNewVerticalLane( horizontalLaneIndex:targetHLaneIndex ,withIdea: false);
      added = true;
    }

    var targetVLane = horizontalLanes[targetHLaneIndex].childVLaneBodies[targetVLaneIndex];
    //アイディアが足りなければ足す
    for (int i =targetVLane.ideas.length; i <= targetIdeaIndex - (fillToIndex ? 0:1) ; i++) {

      if( targetVLane.key.currentState != null ) {
        targetVLane.key.currentState?.addNewIdeaBelowTo(currentIndex:i-1);//現在のインデックスを指定するので注意
      } else {
        //addNewIdeaBelowToが使えない場合があるので、やむなく自力実装 ※安易にこのロジックを変えると不具合の温床になる
        targetVLane.ideas.add(Idea(index: i, parentLane: targetVLane));
        needManualStatePaint = true;
      }
      added = true;
    }

    if(needManualStatePaint) {
      targetVLane.rebuildItemIndexesAndUpdateRequiredHeight(0);
      setState(() {
        if(targetIdeaIndex>= targetVLane.ideas.length) {
          printWhenDebug("【error】targetHLaneIndex:$targetHLaneIndex targetVLaneIndex:$targetVLaneIndex targetIdeaIndex:$targetIdeaIndex targetVLane.ideas.length:${targetVLane.ideas.length}");
        }
        targetVLane.ideas[targetIdeaIndex].key.currentState?.build(context);  //強制的にアイディアを描画する ※安易にこのロジックを変えると不具合の温床になる
      });
    }
    return added;
  }

  void _scrollVListener() {
    ref.watch(canvasScrollDeltaY.notifier).state = -scrollControllerVertical.position.pixels;
  }

  void _scrollHListener() {
    ref.watch(canvasScrollDeltaX.notifier).state = -scrollControllerHorizontal.position.pixels;
  }

  @override
  Widget build(BuildContext context) {
    return CanvasZoneInheritedWidget(
      canvasZoneState: this,
      child: Expanded( //これが無いと画面描画のOverFlowを起こす
        child: Stack( //Transformを狙い通り動かすためにStack上に載せる
          children:[
            Positioned(
              top: 0,
              left:0,
              child: Transform.scale(
                alignment: Alignment.topLeft,
                scale: ref.watch(currentScale),
                child: Container( //Canvasの全体サイズ。但し拡大縮小した場合にはサイズを補正する
                  width:  max( allSumVerticalHeaderViewWidths, MediaQuery.of(context).size.width * (1 / ref.watch(currentScale)) ),
                  height: max((MediaQuery.of(context).size.height - globalTop) * (1 / ref.watch(currentScale)),  ( GlobalConst.verticalLaneHeaderHeight + allSumHorizontalViewHeights ) * (1 / ref.watch(currentScale)) ),
                  color: Colors.transparent, // カラーを指定しないと上のGestureDetectorの範囲がchildの範囲に収まってしまう
                  child: Stack(
                    children: [
                      Positioned( // 縦レーンのタイトル行(横並び) |   |   |   |
                        top:0,
                        left: horizontalLaneHeaderWidth,
                        child: Container( //タイトル行のビューポート
                          width: max( allSumVerticalHeaderViewWidths, MediaQuery.of(context).size.width * (1 / ref.watch(currentScale)) ),
                          height: GlobalConst.verticalLaneHeaderHeight,
                          color: Colors.transparent,
                          child: Stack(
                            children: [
                              Positioned(
                                top:   0,
                                left:  ref.watch(canvasScrollDeltaX),
                                width: allSumVerticalHeaderViewWidths + GlobalConst.laneSplitterHeaderWidthOrHeight,
                                height:GlobalConst.verticalLaneHeaderHeight,
                                child: Stack( children: getVerticalLanesHeaders(), ), //縦レーン各タイトルの描画
                              ),
                            ],
                          ),
                        ),
                      ),
                      Positioned( // 横レーンのタイトル行(縦並び) ------
                        top: GlobalConst.verticalLaneHeaderHeight,
                        left:0,
                        child: SizedBox( //タイトル列のビューポート
                          width: GlobalConst.horizontalLaneHeaderDefaultWidth,
                          height: allSumHorizontalViewHeights + GlobalConst.laneSplitterHeaderWidthOrHeight,
//                          color: Colors.lime,
                          child: Stack(
                            children: [
                              Positioned(
                                top:   ref.watch(canvasScrollDeltaY),
                                left:  0,
                                width: GlobalConst.horizontalLaneHeaderDefaultWidth,
                                height: allSumHorizontalViewHeights + GlobalConst.laneSplitterHeaderWidthOrHeight,
                                child: Stack(
                                  children: getHorizontalLanesHeaders(),//横レーン各タイトルの描画
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                      Positioned( // レーンボディ
                        top: GlobalConst.verticalLaneHeaderHeight,
                        left:horizontalLaneHeaderWidth,
                        child: GestureDetector(
                          onPanUpdate: (details) {
                            scrollControllerVertical.jumpTo(   max( -GlobalConst.canvasPanMargin, scrollControllerVertical.position.pixels - details.delta.dy) );
                            scrollControllerHorizontal.jumpTo( max( -GlobalConst.canvasPanMargin, scrollControllerHorizontal.position.pixels - details.delta.dx) );
                            //メモ:タイトル行の移動は、スクロールバーのリスナーで ref.watch(canvasScrollDeltaX & Y) を更新する事で実現している
                          },
                          onTap: () async {
                            if (ref.watch(relationHoverFlg)) {
                              await relationSelectDialog(context,this);
                              setState(() {});
                            }
                          },
                          child: SizedBox( //ボディ部スクロール範囲のビューポート
                            width:    MediaQuery.of(context).size.width  * (1 / ref.watch(currentScale)) - horizontalLaneHeaderWidth, //横レーンのヘッダー分ビューポートを狭くしないと、縦スクロールバーが画面外に出てしまう
                            height: ( MediaQuery.of(context).size.height - globalTop - ( GlobalConst.verticalLaneHeaderHeight*ref.watch(currentScale) ) ) * (1/ref.watch(currentScale)),
                            child: Stack(
                              children:[
                                Scrollbar(
                                key: scrollbarVKey,
                                controller: scrollControllerVertical,
                                thumbVisibility: true,
                                trackVisibility: true,
                                child: Scrollbar(
                                  key: scrollbarHKey,
                                  controller: scrollControllerHorizontal,
                                  thumbVisibility: true,
                                  trackVisibility: true,
                                  notificationPredicate: (notif) => notif.depth == 1, //スクロールバーを入れ子にする場合に必要なようだ
                                  child: SingleChildScrollView(
                                    padding: EdgeInsets.fromLTRB(0, 0, (scrollControllerVertical.positions.isNotEmpty ? (scrollControllerVertical.position.maxScrollExtent == 0 ? 0 : GlobalConst.pixelBufferForScrollBar) : 0), 0), //スクロールバー用のバッファ。スクロールバーの幅はプラットフォームによって異なるがWebの場合 8px
                                    controller: scrollControllerVertical,
                                    child: SingleChildScrollView(
                                      padding: const EdgeInsets.fromLTRB(0, 0, 0, GlobalConst.pixelBufferForScrollBar), //スクロールバー用のバッファ。スクロールバーの幅はプラットフォームによって異なるがWebの場合 8px
                                      controller: scrollControllerHorizontal,
                                      scrollDirection: Axis.horizontal,
                                      child: SizedBox( // 子に描画用のStackを配置するために土台が必要
                                        width: max(MediaQuery.of(context).size.width  * (1 / ref.watch(currentScale)) - horizontalLaneHeaderWidth, //レーン領域が小さい場合でも、スクロールバーは画面右側に配置する
                                          allSumVerticalHeaderViewWidths + 0 /*スクロールバー用のバッファ*/),
                                        height:
                                          max(
                                              (MediaQuery.of(context).size.height - globalTop - GlobalConst.verticalLaneHeaderHeight*ref.watch(currentScale)) * ( 1/ref.watch(currentScale) ) - GlobalConst.pixelBufferForScrollBar, //レーン領域が画面より小さい場合でも、スクロールバーは画面下部に配置する
                                              allSumHorizontalViewHeights + 0 /*バッファ*/
                                           ),
                                        child:Stack(
                                          children:
                                            buildIdeasAndRelations(this),
                                          ),
                                        ),
                                      ),
                                    ),
                                  ),
                                ),
                                if (ref.watch(canvasScrollDeltaX)>0)
                                  SizedBox( //スクロールがはみ出したときに区切り線を描画する(区切り線とヘッダーに余白ができる不自然さの対策)
                                    width: GlobalConst.canvasPanMargin,// ref.watch(canvasScrollDeltaY).abs(),
                                    child: Stack(
                                      children: buildDelimiterLineForPanMargin(this),
                                    ),
                                  ),
                              ],
                            ),
                          ),
                        ),
                      ),
                      if(ref.watch(showContextMenuFlg))
                        buildContextMenuWidget(this, focusedIdea), // コンテクストメニュー
                    ]
                  ),
                ),
              ),
            ),
          ]
        ),
      ),
    );
  }
}

class CanvasZoneInheritedWidget extends InheritedWidget {
  final CanvasZoneState canvasZoneState;
  const CanvasZoneInheritedWidget({required this.canvasZoneState, required Widget child, super.key}) : super(child: child);

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

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

class IdeasRelation {
  Idea fromIdea;
  Idea toIdea;
  RelationShape fromShape=RelationShape.dot;
  RelationShape toShape=RelationShape.dot;
  String note = '';

  IdeasRelation({required this.fromIdea, required this.toIdea});
}

class VerticalLaneHeader extends ConsumerStatefulWidget {
  @override
  final GlobalKey<VerticalLaneHeaderState> key = GlobalKey<VerticalLaneHeaderState>();
  int index;
  String title;
  late double viewWidth;
  double viewLeft=0;
  final double indentWidth = GlobalConst.ideaIndentWidth; //インデントの下げ幅

  VerticalLaneHeader({
    required this.index,
    required this.viewWidth,
    this.title = '',
  }) : super(key: ValueKey(GlobalKey<VerticalLaneHeaderState>().toString()));

  @override
  ConsumerState<VerticalLaneHeader> createState() => VerticalLaneHeaderState();
}

class VerticalLaneHeaderState extends ConsumerState<VerticalLaneHeader> {
  final TextEditingController titleController = TextEditingController();
  final FocusNode _titleFocusNode = FocusNode();

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

  @override
  Widget build(BuildContext context) {
    return VerticalLaneHeaderInheritedWidget(
      laneHeaderState: this,
      child: SizedBox(
        height: GlobalConst.verticalLaneHeaderHeight,
        width: widget.viewWidth,
        child: Focus(
          onKey: (FocusNode node, RawKeyEvent event) {
            if (event.runtimeType == RawKeyDownEvent) {
              if (event.logicalKey == LogicalKeyboardKey.arrowDown) { //カーソルキー ↓
                for(var aHorizontalLane in CanvasZoneInheritedWidget.of(context).horizontalLanes) {
                  if(aHorizontalLane.childVLaneBodies.length>widget.index) {
                    if (aHorizontalLane.childVLaneBodies[widget.index].ideas.isNotEmpty) {
                      aHorizontalLane.childVLaneBodies[widget.index].ideas[0].key.currentState?.textFocusNode.requestFocus();
                      break;
                    }
                  }
                }
              } 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).verticalLaneHeaders[widget.index - 1].key.currentState?._titleFocusNode.requestFocus();
                }
              }
            }
            return KeyEventResult.ignored;
          },
          child: Container( //タイトル
            color: Colors.blue[50],
            padding: const EdgeInsets.symmetric(horizontal: GlobalConst.ideaIndentWidth),
            child: TextField(
              autofocus: true,
              controller: titleController,
              focusNode: _titleFocusNode,
              decoration: InputDecoration(
                border: InputBorder.none,
                disabledBorder: const OutlineInputBorder(
                  borderSide: BorderSide(color: Colors.blue, width: 2.0),
                ),
                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();
              },
            ),
          )
        ),
      ),
    );
  }
}

class VerticalLaneHeaderInheritedWidget extends InheritedWidget {
  final VerticalLaneHeaderState laneHeaderState;

  VerticalLaneHeaderInheritedWidget({required this.laneHeaderState, required Widget child, super.key}) : super(child: child);

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

  static VerticalLaneHeaderState of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<VerticalLaneHeaderInheritedWidget>()!.laneHeaderState;
  }
}

class HorizontalLaneHeader extends ConsumerStatefulWidget {
  @override
  final GlobalKey<HorizontalLaneHeaderState> key = GlobalKey<HorizontalLaneHeaderState>();
  int index;
  String title;
  HorizontalLane relatedLane;

  HorizontalLaneHeader({
    required this.index,
    required this.relatedLane,
    this.title = '',
  }) : super(key: ValueKey(GlobalKey<HorizontalLaneHeaderState>().toString()));

  @override
  ConsumerState<HorizontalLaneHeader> createState() => HorizontalLaneHeaderState();
}

class HorizontalLaneHeaderState extends ConsumerState<HorizontalLaneHeader> {
  final TextEditingController titleController = TextEditingController();
  //final FocusNode _titleFocusNode = FocusNode();
  double _opacity=1.0;

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

  @override
  Widget build(BuildContext context) {
    return HorizontalLaneHeaderInheritedWidget(
      laneHeaderState: this,
      child: Container(
        height: widget.relatedLane.viewHeight,
        width: GlobalConst.horizontalLaneHeaderDefaultWidth,
        color: Colors.blue[50],
        child: Focus(
            onKey: (FocusNode node, RawKeyEvent event) {
              return KeyEventResult.ignored;
            },
            child: GestureDetector(
              onTap: () async {
                await horizontalLaneHeaderEditDialog(context,this);
                setState(() {});
              },
              child: MouseRegion(
                onEnter: (_) {
                  setState(() {
                    _opacity = 0.9;
                  });
                },
                onExit: (_) {
                  setState(() {
                    _opacity = 1.0;
                  });
                },
                child: Opacity(
                  opacity: _opacity,
                  child: FittedBox( //親コンテナ(横レーンのViewHeight)に合わせて文字を縮小させるため
                    fit: BoxFit.scaleDown,
                    child: Container( //タイトル
                      padding: const EdgeInsets.symmetric(vertical: GlobalConst.horizontalLaneHeaderPadding),
                      child: Align(
                        alignment: Alignment.center,
                        child: Tategaki(
                          widget.title,
                          style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.blue[900]),
                        ),
                      ),
/*
                      child: TextField(
                        autofocus: true,
                        controller: titleController,
                        focusNode: _titleFocusNode,
                        decoration: InputDecoration(
                          border: InputBorder.none,
                          disabledBorder: const OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.blue, width: 2.0),
                          ),
                          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();
                        },
                      ),
*/
                    ),
                  ),
                ),
              ),
            )
        ),
      ),
    );
  }
}

class HorizontalLaneHeaderInheritedWidget extends InheritedWidget {
  final HorizontalLaneHeaderState laneHeaderState;

  HorizontalLaneHeaderInheritedWidget({required this.laneHeaderState, required Widget child, super.key}) : super(child: child);

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

  static HorizontalLaneHeaderState of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<HorizontalLaneHeaderInheritedWidget>()!.laneHeaderState;
  }
}


class LaneBody extends ConsumerStatefulWidget {
  @override
  final GlobalKey<LaneBodyState> key = GlobalKey<LaneBodyState>();
  int indexX;
  int indexY;
  String title;
  List<Idea> ideas;
  double requiredHeight=0;
  ScrollController scrollControllerForLaneBody = ScrollController();

  LaneBody({
    required this.indexX,
    required this.indexY,
    required this.ideas,
    this.title = '',
  }) : super(key: ValueKey(GlobalKey<LaneBodyState>().toString()));

  void updateRequiredHeight() {
    requiredHeight = ideas.length * GlobalConst.eachIdeaHeight;
  }

  void rebuildItemIndexesAndUpdateRequiredHeight(int startIndex) {
    for (int i = startIndex; i < ideas.length; i++) {
      ideas[i].index = i;
    }
    updateRequiredHeight();
  }

  @override
  ConsumerState<LaneBody> createState() => LaneBodyState();
}

class LaneBodyState extends ConsumerState<LaneBody> {
  List<Idea> get ideas => widget.ideas;
  final TextEditingController titleController = TextEditingController();

  void addNewIdeaBelowTo({required int currentIndex, int indentAdjust = 0}) { //基準となる現在のIndexを指定するので注意
    int currentIndentLevel;
    setState(() {
      if(currentIndex<0) {
        currentIndentLevel = 0;
      } else {
        currentIndentLevel = ideas[currentIndex].indentLevel;
      }
      ideas.insert(
        currentIndex + 1,
        Idea(
          index: currentIndex + 1,
          parentLane: widget,
          indentLevel: currentIndentLevel + indentAdjust,
        ),
      );
      widget.rebuildItemIndexesAndUpdateRequiredHeight(currentIndex + 2);
    });
  }

  void addNewIdeaWithFocus(int currentIndex, {int indentAdjust = 0}) {
    addNewIdeaBelowTo(currentIndex: currentIndex, indentAdjust: indentAdjust);
    CanvasZoneInheritedWidget.of(context).optimizeHorizontalViewAndHeights(skipFlg: false);
    CanvasZoneInheritedWidget.of(context).setState(() {}); //画面の再描画

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

  void moveIdea(IdeaState state, int currentIdeaIndex, int direction, {bool limitSameLane = true}) {
    if (currentIdeaIndex + direction < 0) { //レーン内の一番上からさらに上に移動
      if(limitSameLane == false) {
        moveAndInsertIdeaToAnotherLane(state, widget.indexY - 1, currentIdeaIndex, 0);
      }
      return;
    } else if (currentIdeaIndex + direction >= ideas.length) { //レーン内の一番上からさらに下に移動
      if(limitSameLane == true) {
        addNewIdeaBelowTo(currentIndex: currentIdeaIndex);
      } else {
        moveAndInsertIdeaToAnotherLane(state, widget.indexY + 1, currentIdeaIndex, 0);
        return;
      }
    }
    if (currentIdeaIndex + direction >= 0 && currentIdeaIndex + direction < ideas.length) {
      WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.setState(() {
        Idea temp = ideas[currentIdeaIndex];
        ideas[currentIdeaIndex] = ideas[currentIdeaIndex + direction];
        ideas[currentIdeaIndex + direction] = temp;
        ideas[currentIdeaIndex].key.currentState?.updateIndex(currentIdeaIndex);
        ideas[currentIdeaIndex + direction].key.currentState?.updateIndex(currentIdeaIndex + direction);
      });
    }
  }

  void moveAndInsertIdeaToAnotherLane(IdeaState state, int targetHLaneIndex, int currentIdeaIndex, int vLaneDirection) {
    if(targetHLaneIndex < 0 || widget.indexX + vLaneDirection < 0) return;

    state.textFocusNode.unfocus(); //後でフォーカスを正しく反映させるために必要

    List<HorizontalLane> horizontalLanes = CanvasZoneInheritedWidget.of(context).horizontalLanes;
    LaneBody targetLaneBody;
    int targetIdeaIndex;

    if(vLaneDirection == 0) { //上下に移動の場合
      CanvasZoneInheritedWidget.of(context).checkAndFillIdeas( targetHLaneIndex, widget.indexX + vLaneDirection, 0, false);
      targetLaneBody = horizontalLanes[targetHLaneIndex].childVLaneBodies[widget.indexX];
      targetIdeaIndex = targetLaneBody.ideas.length;
    } else { //左右に移動の場合
      CanvasZoneInheritedWidget.of(context).checkAndFillIdeas( targetHLaneIndex, widget.indexX + vLaneDirection, currentIdeaIndex, false);
      targetLaneBody = horizontalLanes[targetHLaneIndex].childVLaneBodies[widget.indexX + vLaneDirection];
      targetIdeaIndex = currentIdeaIndex;
    }

    //新レーンへアイディアを移動
    targetLaneBody.ideas.insert(targetIdeaIndex, ideas[currentIdeaIndex]);
    targetLaneBody.ideas[targetIdeaIndex].parentLane = targetLaneBody;
    targetLaneBody.rebuildItemIndexesAndUpdateRequiredHeight(0);
    if (targetLaneBody.key.currentState != null) {
      targetLaneBody.key.currentState!.setState(() {}); //描画更新のために必要
    }

    //元のレーンから削除
    widget.ideas.removeAt(currentIdeaIndex);
    widget.rebuildItemIndexesAndUpdateRequiredHeight(0);
    if(widget.indexY == horizontalLanes.length - 1 && widget.ideas.isEmpty) {
      //横レーン内の中身が全て無い場合は削除
      bool ideaExistFlg = false;
      for(int i=0; i<horizontalLanes[widget.indexY].childVLaneBodies.length; i++){
        if(horizontalLanes[widget.indexY].childVLaneBodies[i].ideas.isNotEmpty) {
          ideaExistFlg = true;
          break;
        }
      }
      if(ideaExistFlg == false) {
        WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.removeLastHorizontalLane();
      }
    }
    WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.setState(() {});

    WidgetsBinding.instance.addPostFrameCallback((_) { //遅延にしないとフォーカス移動が正しく反映されない
      CanvasZoneInheritedWidget.of(state.context/*ここではstateのcontextを使わないとエラーになる*/).jumpFocusTo(targetLaneBody.indexY, targetLaneBody.indexX, targetIdeaIndex, fillToIndex: false);
    });
  }

  void removeFromRelation(Idea targetIdea) {
    //削除対象のリレーションを探す
    List<IdeasRelation> removeTargetList = [];
    if (ref.read(ideasRelationList).isNotEmpty) {
      for (var relationSet in ref.read(ideasRelationList)) {
        if (relationSet.fromIdea == targetIdea || relationSet.toIdea == targetIdea) {
          removeTargetList.add(relationSet);
        }
      }
    }
    //実際に削除する
    if (removeTargetList.isNotEmpty) {
      for (var relationSet in removeTargetList) {
        removeRelation(ref.watch(ideasRelationList.notifier).state, relationSet);
      }
    }
  }

  void removeIdea(int currentIndex) {
    if (ideas.length > 1) {
      removeFromRelation(ideas[currentIndex]);
      ideas.removeAt(currentIndex);
      for (int i = currentIndex; i < ideas.length; i++) {
        ideas[i].index = i;
      }
      widget.updateRequiredHeight();
      ideas[max(0, currentIndex - 1)].key.currentState?.textFocusNode.requestFocus();
      CanvasZoneInheritedWidget.of(context).setState(() {}); //画面の再描画
    }
  }

  void focusNextIdea(int currentIndex, int direction) {
    if (currentIndex == 0 && direction <= 0) { //レーンを超えて上に移動
      if(widget.indexY == 0) {
        //タイトルにフォーカスを移す
        CanvasZoneInheritedWidget.of(context).verticalLaneHeaders[widget.indexX].key.currentState!._titleFocusNode.requestFocus();
      } else {
        //上のレーンにアイディアがあればそこにフォーカスを移す
        for(int h=widget.indexY-1; h>=0; h--) {
          var targetHLane = CanvasZoneInheritedWidget.of(context).horizontalLanes[h];
          if(targetHLane.childVLaneBodies.length > widget.indexX && targetHLane.childVLaneBodies[widget.indexX].ideas.isNotEmpty) {
            targetHLane.childVLaneBodies[widget.indexX].ideas[
            targetHLane.childVLaneBodies[widget.indexX].ideas.length -1
            ].key.currentState?.textFocusNode.requestFocus();
            break;
          }
        }
      }
    } else if (currentIndex + direction >= ideas.length) { //レーンを超えて下に移動
      //下のレーンにアイディアがあればそこにフォーカスを移す
      for(int h=widget.indexY+1; h<CanvasZoneInheritedWidget.of(context).horizontalLanes.length; h++) {
        var targetHLane = CanvasZoneInheritedWidget.of(context).horizontalLanes[h];
        if(targetHLane.childVLaneBodies.length > widget.indexX && targetHLane.childVLaneBodies[widget.indexX].ideas.isNotEmpty) {
          targetHLane.childVLaneBodies[widget.indexX].ideas[
          0
          ].key.currentState?.textFocusNode.requestFocus();
          break;
        }
      }
    } else if (currentIndex + direction >= 0 && currentIndex + direction < ideas.length) { //レーン内の移動
      ideas[currentIndex + direction].key.currentState?.textFocusNode.requestFocus();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return LaneBodyInheritedWidget(
      laneState: this,
      child: SizedBox(
        width: double.infinity,
        height: (GlobalConst.eachIdeaHeight * ideas.length).toDouble(),
        child: Column(
          children: ideas
        ),
      ),
    );
  }
}

class LaneBodyInheritedWidget extends InheritedWidget {
  final LaneBodyState laneState;

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

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

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

class Idea extends ConsumerStatefulWidget {
  @override
  final GlobalKey<IdeaState> key = GlobalKey<IdeaState>();
  int index;
  int indentLevel;
  final double textFieldHeight; //テキスト欄の高さ
  String content;
  LaneBody parentLane;
  List<Idea> relatedIdeasTo = [];
  List<Idea> relatedIdeasFrom = [];

  bool textStyleBold          = false;
  bool textStyleItalic        = false;
  bool textStyleUnderLine     = false;
  bool textStyleStrikeThrough = false;
  Color textColor = Colors.black;

  FontWeight? myFontWeight;
  FontStyle? myFontStyle;
  TextStyle? myTextStyle;

  void reflectMyTextStyle() {
    myTextStyle = TextStyle(
      color: textColor,
      fontWeight: (textStyleBold)      ? FontWeight.bold : FontWeight.normal,
      fontStyle:  (textStyleItalic)    ? FontStyle.italic : FontStyle.normal,
      decoration: (textStyleUnderLine) ? TextDecoration.underline : (textStyleStrikeThrough) ? TextDecoration.lineThrough : TextDecoration.none,
    );
  }

  Idea({
    required this.index,
    required this.parentLane,
    this.textFieldHeight = 48.0, //デフォルトは48
    this.indentLevel = 0,
    this.content = '',
  }) : super(key: ValueKey(GlobalKey<IdeaState>().toString()));

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

class IdeaState extends ConsumerState<Idea> with AutomaticKeepAliveClientMixin<Idea> {
  final TextEditingController textController = TextEditingController();
  final FocusNode textFocusNode = FocusNode();

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

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

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

  void reflectMyTextStyle() {
    setState(() {
      widget.reflectMyTextStyle();
    });
  }

  void startRelationMode() {
    ref.watch(relatedIdeaSelectMode.notifier).state = true;
    ref.watch(originIdeaForSelectMode.notifier).state = widget;
    ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar( content: Text( '接続先を選んでEnterキーを押してください'),)
    );
  }

  void endRelationMode() {
    StateController<bool> mode = ref.watch(relatedIdeaSelectMode.notifier);
    Idea? origin = ref.watch(originIdeaForSelectMode.notifier).state;
    ScaffoldMessenger.of(context).hideCurrentSnackBar();
    if (origin != null) {
      bool cancelFlg = false;
      List<IdeasRelation> ideasRelations = ref.watch(ideasRelationList.notifier).state;

      //自分自身に接続しようとしていないかチェック
      if(origin == widget) {
        cancelFlg = true;
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar( content: Text('自分自身には接続できません'), ),
        );
      } else {
        //既に同じリレーションが無いかチェック
        for (var element in ideasRelations) {
          if ( (element.fromIdea == origin && element.toIdea == widget) || (element.fromIdea == widget && element.toIdea == origin) ){
            cancelFlg = true;
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar( content: Text('既に接続されています'), ),
            );
            break;
          }
        }
      }
      if(cancelFlg == false) {
        //リレーションを張る
        widget.relatedIdeasFrom.add(origin);
        origin.relatedIdeasTo.add(widget);
        ref.watch(ideasRelationList.notifier).state.add(IdeasRelation(fromIdea: origin, toIdea: widget));
        CanvasZoneInheritedWidget.of(context).setState(() {}); //Relationの線を描画
      }
      origin = null;
    }
    mode.state = false;
  }

  late final Map<Type, Action<Intent>> _actionMap;

  @override
  void initState() {
    super.initState();
    textController.text = widget.content;
    textFocusNode.addListener(_onFocusChange);
    _actionMap = <Type, Action<Intent>>{
      ActivateIntent: CallbackAction<Intent>(
        onInvoke: (Intent intent) => funcStartRelationMode(widget),//startRelationMode(),
      ),
    };
  }

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

  final Map<ShortcutActivator, Intent> _shortcutMap =
  const <ShortcutActivator, Intent>{
    SingleActivator(LogicalKeyboardKey.keyX, control: true): ActivateIntent(),
  };

  void _onFocusChange() {
    printWhenDebug("_onFocusChange(): ${textFocusNode.hasFocus.toString()}");
    CanvasZoneInheritedWidget.of(context).setState(() {
      if(textFocusNode.hasFocus) {
        //コンテキストメニュー用
        CanvasZoneInheritedWidget.of(context).focusedIdea = widget;
        CanvasZoneInheritedWidget.of(context).contextDxAdjust = 0;
        CanvasZoneInheritedWidget.of(context).contextDyAdjust = 0;

        //ツールバーの状態を更新する(ちょっと高コストかもしれないが暫定)
        WholeAppInheritedWidget.of(context).setState(() {});
      } else {
        ;//CanvasZoneInheritedWidget.of(context).focusedIdea = null;
      }
    });
  }

  //TextFieldでEnterを押した場合のキーの処理について
  // ・webだと、submit→key webの順で処理される
  // ・windowsだと、key → submitの順で処理される
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Focus(
      onKey: (FocusNode node, RawKeyEvent event) {
        //print("event.runtimeType:${event.runtimeType.toString()} Normal${event.logicalKey.keyLabel} , Web${ (event.data is RawKeyEventDataWeb) ? (event.data as RawKeyEventDataWeb).code : ''}");
        if (event.runtimeType == RawKeyDownEvent) {
          if ((event.logicalKey.keyLabel == 'Enter') || //Enterキー押下
              ( (event.data is RawKeyEventDataWeb)&&(event.data as RawKeyEventDataWeb).code == 'Enter') ) { //Enterの場合のみ特殊処理が必要
            if (event.isControlPressed) {
              funcStartRelationMode(widget);
            }
            else {
              if (ref.read(relatedIdeaSelectMode.notifier).state == true) { //relatedの指定モード
                endRelationMode();
                textFocusNode.requestFocus();
              } else {
                textFocusNode.unfocus();
                funcAddNewIdeaWithFocus(widget);
              }
            }
            return KeyEventResult.handled;
          } else if (event.logicalKey == LogicalKeyboardKey.capsLock) {
            // CapsLockキーを無視する条件を追加
            return KeyEventResult.ignored;
          } else if (event.isControlPressed && event.isAltPressed){        // Ctrl + Alt :冗長な判定だが、設定でCtrlとAltを入れ替える処理に備え別扱いとしている
            if (event.logicalKey == LogicalKeyboardKey.arrowRight) {       // Ctrl + Alt + →
              moveAndInsertIdeaToAnotherLaneRight(widget);
//              LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY, widget.index, 1);
              return KeyEventResult.handled;
            } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { //  Ctrl + Alt + ←
              moveAndInsertIdeaToAnotherLaneLeft(widget);
//              LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY, widget.index, -1);
              return KeyEventResult.handled;
            } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {   // Ctrl + Alt + ↑
              moveAndInsertIdeaToAnotherLaneUp(widget);
//              LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY - 1, widget.index, 0);
              return KeyEventResult.handled;
            } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { // Ctrl + Alt + ↓
              moveAndInsertIdeaToAnotherLaneDown(widget);
//              LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY + 1, widget.index, 0);
              return KeyEventResult.handled;
            }
          } else if (GlobalConst.swapCtrlAlt ? event.isAltPressed : event.isControlPressed) { // コントロールキーのみ押下:レーンを超えたフォーカスの移動
            if (event.logicalKey == LogicalKeyboardKey.tab) {              // Ctrl + TAB
              funcStartRelationMode(widget);
            } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {   // Ctrl + ↑
              CanvasZoneInheritedWidget.of(context).jumpFocusTo(widget.parentLane.indexY -1 , widget.parentLane.indexX, widget.index, fillToIndex: false);
              return KeyEventResult.handled; //余計なフォーカスの移動を防ぐ
            } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { // Ctrl + ↓
              CanvasZoneInheritedWidget.of(context).jumpFocusTo(widget.parentLane.indexY +1 , widget.parentLane.indexX, widget.index, fillToIndex: false);
              return KeyEventResult.handled; //余計なフォーカスの移動を防ぐ
            } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {// Ctrl + →
              CanvasZoneInheritedWidget.of(context).jumpFocusTo(widget.parentLane.indexY, widget.parentLane.indexX + 1, widget.index);
            } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { // Ctrl + ←
              CanvasZoneInheritedWidget.of(context).jumpFocusTo(widget.parentLane.indexY, widget.parentLane.indexX - 1, widget.index);
            }
          } else if (GlobalConst.swapCtrlAlt ? event.isControlPressed : event.isAltPressed) { // Altキー押下:アイディアの移動
            if (event.logicalKey == LogicalKeyboardKey.arrowRight) { // Alt + →
              LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY, widget.index, 1);
              return KeyEventResult.handled;
            } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { //  Alt + ←
              LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY, widget.index, -1);
              return KeyEventResult.handled;
            } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { // Alt + ↑
              if (event.isShiftPressed) {
                LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY - 1, widget.index, 0);
              } else {
                funcIdeaMoveUp(widget);
              }
              return KeyEventResult.handled;
            } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { // Alt + ↓
              if (event.isShiftPressed) {
                LaneBodyInheritedWidget.of(context).moveAndInsertIdeaToAnotherLane(this, widget.parentLane.indexY + 1, widget.index, 0);
              } else {
                funcIdeaMoveDown(widget);
              }
              return KeyEventResult.handled;
            }
          } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { //カーソルキー ↑
            funcIdeaFocusMoveUp(widget);
            return KeyEventResult.handled; //余計なフォーカスの移動を防ぐ
          } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { //カーソルキー ↓
            funcIdeaFocusMoveDown(widget);//            LaneBodyInheritedWidget.of(context).focusNextIdea(widget.index, 1);
            return KeyEventResult.handled; //余計なフォーカスの移動を防ぐ
          } else if (event.logicalKey == LogicalKeyboardKey.arrowRight && //カーソルキー →
              textController.selection.baseOffset == textController.text.length) {
            CanvasZoneInheritedWidget.of(context).jumpFocusTo(widget.parentLane.indexY, widget.parentLane.indexX + 1, widget.index);
          } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft && textController.selection.baseOffset == 0) {  //カーソルキー ←
            CanvasZoneInheritedWidget.of(context).jumpFocusTo(widget.parentLane.indexY, widget.parentLane.indexX - 1, widget.index);
          } else if (event.logicalKey == LogicalKeyboardKey.tab) { //TABキー
            if (event.isShiftPressed) { //SHIFT + TABキー
              funcIndentDecrease(widget);
            } else {
              funcIndentIncrease(widget);
            }
            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) {
            LaneBodyInheritedWidget.of(context).removeIdea(widget.index);
          } else if (event.logicalKey == LogicalKeyboardKey.f1) { // F1キー
            funcStartRelationMode(widget);
          }
        }
        return KeyEventResult.ignored;
      },
      child: RawKeyboardListener(
        focusNode: FocusNode(),
        child: Container( //1つのアイディア領域の描画
          margin: const EdgeInsets.symmetric(horizontal: ideaHorizontalMargin, vertical: ideaVerticalMargin),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              const SizedBox(width: ideaLeftSpace, child: DecoratedBox(decoration: BoxDecoration(color: Colors.red))), //左側の余白
              Expanded(
                flex: 4,
                child: Row(children: [
                  if(CanvasZoneInheritedWidget.of(context).verticalLaneHeaders[widget.parentLane.indexX].viewWidth > ideaHorizontalMargin*2 + ideaLeftSpace + GlobalConst.ideaIndentWidth * widget.indentLevel) // インデントの線を描画する幅があるかチェック
                    CustomPaint( //インデントの線
                      painter: _CustomPainter(_indentLinePaint),
                      child: Container(
                        width: GlobalConst.ideaIndentWidth * widget.indentLevel,
                      ),
                    ),
                  (CanvasZoneInheritedWidget.of(context).verticalLaneHeaders[widget.parentLane.indexX].viewWidth > ideaHorizontalMargin*2 + ideaLeftSpace + GlobalConst.ideaIndentWidth * widget.indentLevel +1 ) ? // アイディアを置く幅があるかチェック
                    Expanded(
                      child: SizedBox(
                        height: widget.textFieldHeight,
                        child: FocusableActionDetector(
                          actions: _actionMap,
                          shortcuts: _shortcutMap,
                          child: TextFormField(
                            style: widget.myTextStyle,
                            controller: textController,
                            focusNode: textFocusNode,
                            maxLines: 1,
                            decoration: const InputDecoration(
                              border: OutlineInputBorder(),
                            ),
                            keyboardType: TextInputType.multiline,
                            textInputAction: TextInputAction.done,
                            onFieldSubmitted: (value) {
                              textFocusNode.requestFocus(); //Enterはこれが無いとKeyEventが発生しない(おそらくフォーカスが無くなるから。少なくともWebでは必須)
                            },
                          ),
                        ),
                      ),
                    ) :
                    SizedBox(
                      height:widget.textFieldHeight,
                    ),
                ]),
              ),
            ],
          ),
        ),
      )
    );
  }
}

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;
}

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

import 'package:flutter/material.dart';
import 'main.dart';
import 'dart:convert'; //JSON形式のデータを扱う
import 'package:path_provider/path_provider.dart';
import 'dart:math';
import 'package:file_selector/file_selector.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'export_lib.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:google_sign_in/google_sign_in.dart';


//リレーションの情報
StateProvider<bool> relatedIdeaSelectMode = StateProvider((ref) => false);
StateProvider<Idea?> originIdeaForSelectMode = StateProvider((ref) => null);
StateProvider<List<IdeasRelation>> ideasRelationList = StateProvider((ref) => []);
StateProvider<IdeasRelation?> selectedRelation = StateProvider((ref) => null);
//リレーションの見た目
enum RelationShape{dot,arrow, none}
StateProvider<Color> relationColor = StateProvider((ref) => Colors.blue);
StateProvider<bool> relationHoverFlg = StateProvider((ref) => false);
const bool relationDebugPaint = false;

//スケール
StateProvider<double> currentScale = StateProvider((ref) => 1);

//各種ウィジェットのサイズと表現
StateProvider<double> defaultEachLaneWidth = StateProvider((ref) => 500);
StateProvider<double> relationDotSize   = StateProvider((ref) => 6);
StateProvider<double> relationArrowSize = StateProvider((ref) => 10);

class GlobalConst {
  static const debugPrintLevel = 1;// 0:ログ出力無し 1:重要なもののみ 2:細かいものも出す 3:更に細かいものも出す

  static const eachIdeaHeight = 64.0; //48+8+8 doubleと認識させるために64.0の小数点が必要
  static const verticalLaneHeaderHeight = 50.0;
  static const verticalLaneHeaderMinimumWidth = 60.0; //ideaHorizontalMargin + ideaIndentWidthより小さくすると描画が崩れるので注意
  static const horizontalLaneHeaderDefaultWidth = 50.0;
  static const laneSplitterBodyWidthOrHeight = 1.0;
  static const laneSplitterHeaderWidthOrHeight = 5.0;
  static const horizontalLaneDefaultViewHeight = 350.0;
  static const horizontalLaneHeaderMinimumHeight = 20.0;
  static const horizontalLaneHeaderPadding = 15.0; //ヘッダー文字の描画領域パディング
  static const pixelBufferForScrollBar = 15.0; // スクロールバー用の描画領域。スクロールバーの幅はプラットフォームによって異なるがWebの場合 8px
  static const canvasPanMargin = 20.0;
  static const ideaIndentWidth = 40.0;
  static const swapCtrlAlt = false;

  //以下設定としてON/OFFの検討 キャンバス用
  static const drawSplitterLineHorizontal = true; //横区切り線の描画
}

class GlobalSwitch {
  GlobalSwitch._();
  static final instance = GlobalSwitch._();

  bool showContextMenu = true;
}
StateProvider<bool> showContextMenuFlg = StateProvider((ref) => true);
StateProvider<bool> showContextMenuDebugFlg = StateProvider((ref) => false);

const double ideaLeftSpace = 26;
const double ideaHorizontalMargin = 16;
const double ideaVerticalMargin = 8;

const Color relationColorTransparent = Colors.transparent;//デバッグ時に変えられるよう定義

void printWhenDebug(String str, {int level=1}){
  if(level <= GlobalConst.debugPrintLevel) {
    debugPrint(str);
  }
}

//タイトル行のスクロール用
StateProvider<double> canvasScrollDeltaX = StateProvider((ref) => 0);
StateProvider<double> canvasScrollDeltaY = StateProvider((ref) => 0);

Future<dynamic> accountDialog(BuildContext context) {
  // 入力したメールアドレス・パスワード
  String email = 'fmdmk1@hotmail.co.jp';
  String password = '123456';
  User? user;
  // メッセージ表示用
  String infoText = '';

  Future<UserCredential?> googleLogin() async{
    GoogleSignInAccount? signInAccount = await GoogleSignIn(scopes: [
      'email',
      'https://www.googleapis.com/auth/contacts.readonly',
    ]).signIn();
    if (signInAccount == null) return null;

    GoogleSignInAuthentication auth = await signInAccount.authentication;
    final OAuthCredential credential = GoogleAuthProvider.credential(
      idToken: auth.idToken,
      accessToken: auth.accessToken,
    );
    return await FirebaseAuth.instance.signInWithCredential(credential);
  }

  return showDialog(
    context: context,
    builder: (context) {
      return StatefulBuilder(
        builder: (context, setState) {
          return SimpleDialog(
            children: [
              Column(
                children: [
                  Align( // 閉じるボタン
                    alignment: Alignment.centerRight,
                    child: Tooltip(
                      message: MaterialLocalizations
                          .of(context)
                          .closeButtonTooltip,
                      child: GestureDetector(
                        onTap: () => Navigator.pop(context),
                        child: const Icon(Icons.close, size: 20),
                      ),
                    ),
                  ),
                  Container(
                    padding: const EdgeInsets.all(24),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        if(user != null)
                          Container(
                            padding: const EdgeInsets.all(8),
                            child: Text('ログイン情報:${user!.email}'),
                          ),
                        if(user != null)
                          IconButton(
                            icon: const Icon(Icons.logout),
                            onPressed: () async {
                              await FirebaseAuth.instance.signOut(); // ログアウト処理
                              setState((){
                                user = null;
                              });
                            },
                          ),
                        const SizedBox(height: 10,),
                        ElevatedButton(
                          child: const Row(
                            children:[ Icon(Icons.g_mobiledata), Text('Googleログイン')],
                          ),
                          onPressed: () async {
                            try {
                              UserCredential? result = await googleLogin();
                              if(result == null) throw(e);
                              printWhenDebug("$result");
                              if(result.user != null) {
                                infoText = "ログインしました";
                                printWhenDebug("ログインしました ${result.user!.email} , ${result.user!.uid}");
                                setState(() {
                                  user = result.user!;
                                });
                              }
                            } catch (e) {
                              setState(() {
                                infoText = "ログインに失敗しました:\n${e.toString()}";
                              });
                              printWhenDebug(e.toString());
                            }
                          },
                        ),
                        const SizedBox(height: 10,),
                        TextFormField(
                          initialValue: email,
                          decoration: const InputDecoration(labelText: 'メールアドレス'),
                          onChanged: (String value) {
                            setState(() {
                              email = value;
                            });
                          },
                        ),
                        TextFormField(
                          initialValue: password,
                          decoration: const InputDecoration(labelText: 'パスワード'),
                          obscureText: true,
                          onChanged: (String value) {
                            setState(() {
                              password = value;
                            });
                          },
                        ),
                        Container(
                          padding: const EdgeInsets.all(8),
                          child: Text(infoText),  // メッセージ表示
                        ),
                        OutlinedButton(
                          child: const Text('ユーザ登録'),
                          onPressed: () async {
                            try {
                              // メール/パスワードでユーザー登録
                              final UserCredential result = (await FirebaseAuth.instance
                                  .createUserWithEmailAndPassword(
                                  email: email, password: password)
                              );
                              printWhenDebug("$result");
                              if(result.user != null) {
                                infoText = "ユーザ登録しました";
                                printWhenDebug("ユーザ登録しました ${result.user!.email} , ${result.user!.uid}");
                                setState(() {
                                  user = result.user!;
                                });
                              }
                            } catch (e) {
                              setState(() {
                                infoText = "登録に失敗しました:\n${e.toString()}";
                              });
                              printWhenDebug(e.toString());
                            }
                          },
                        ),
                        const SizedBox(height: 10,),
                        ElevatedButton(
                          child: const Text('ログイン'),
                          onPressed: () async {
                            try {
                              // メール/パスワードでログイン
                              final UserCredential result = (await FirebaseAuth.instance
                                  .signInWithEmailAndPassword(
                                  email: email, password: password));
                              printWhenDebug("$result");
                              if(result.user != null) {
                                infoText = "ログインしました";
                                printWhenDebug("ログインしました ${result.user!.email} , ${result.user!.uid}");
                                setState(() {
                                  user = result.user!;
                                });
                              }
                            } catch (e) {
                              setState(() {
                                infoText = "ログインに失敗しました:\n${e.toString()}";
                              });
                              printWhenDebug(e.toString());
                            }
                          },
                        ),
                        const SizedBox(height: 10,),
                        TextButton(
                            child: const Text('パスワードリセット'),
                            onPressed: () async {
                              try {
                                await FirebaseAuth.instance
                                    .sendPasswordResetEmail(email: email);
                                setState(() {
                                  infoText = "パスワードリセット用のメールを送信しました";
                                  printWhenDebug("パスワードリセット用のメールを送信しました");
                                });
                              } catch (e) {
                                printWhenDebug(e.toString());
                              }
                            }),
                      ],
                    ),
                  ),
                ]
              ),
            ]
          );
        },
      );
    },
  );
}


//2点間の距離を求める関数
double getDistance(double x, double y, double x2, double y2) {
  double distance = sqrt((x2 - x) * (x2 - x) + (y2 - y) * (y2 - y));
  return distance;
}
//2点間の角度を求める関数
double getRadian(double x, double y, double x2, double y2) {
  double radian = atan2(y2 - y,x2 - x);
  return radian;
}

//右向き矢印
class TrianglePainterR extends CustomPainter{
  Color color;
  TrianglePainterR(this.color);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()..color = color;

    // 三角(塗りつぶし)
    var path = Path();
    path.moveTo(0, 0);
    path.lineTo(size.width, size.height / 2);
    path.lineTo(0,              size.height);
    path.close();
    canvas.drawPath(path, paint);
  }

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

//左向き矢印
class TrianglePainterL extends CustomPainter{
  Color color;
  TrianglePainterL(this.color);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()..color = color;

    // 三角(塗りつぶし)
    var path = Path();
    path.moveTo(size.width, 0);
    path.lineTo(0, size.height / 2);
    path.lineTo(size.width, size.height);
    path.close();
    canvas.drawPath(path, paint);
  }

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

void removeRelation(List<IdeasRelation> ideasRelationList, IdeasRelation targetRelation) {
  targetRelation.fromIdea.relatedIdeasTo.remove(targetRelation.toIdea);
  targetRelation.toIdea.relatedIdeasFrom.remove(targetRelation.fromIdea);
  ideasRelationList.remove(targetRelation);
}

Future<dynamic> horizontalLaneHeaderEditDialog(BuildContext context, HorizontalLaneHeaderState state) {
  final TextEditingController textController = TextEditingController();

  textController.text = state.widget.title;

  return showDialog(
    context: context,
    builder: (context) {
      return StatefulBuilder(
        builder: (context, setState) {
          return SimpleDialog(
            contentPadding: const EdgeInsets.all(26),
            children: [
              Align( // 閉じるボタン
                alignment: Alignment.centerRight,
                child: Tooltip(
                  message: MaterialLocalizations.of(context).closeButtonTooltip,
                  child: GestureDetector(
                    onTap: () => Navigator.pop(context),
                    child: const Icon(Icons.close, size: 30),
                  ),
                ),
              ),
              TextField(
                controller: textController,
                decoration: InputDecoration(
                  border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
                ),
              ),
              Container(height: 15),
              const Divider(),
              Container(height: 15),
              Align(alignment: Alignment.center, child:
                SizedBox(
                  width: 120,
                  height: 40,
                  child: ElevatedButton( //決定ボタン
                      onPressed: () {
                        state.widget.title = textController.text;
                        Navigator.pop(context);
                      },
                      child: const Text('決定')
                  ),
                ),
              ),
            ],
          );
        },
      );
    },
  );
}

void showAlert( String msg, BuildContext context) {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        content: Text(msg),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(context, 'OK');
            },
            child: const Text('OK'))
        ],);
    } );
}

Future<dynamic> relationSelectDialog(BuildContext context, CanvasZoneState state){
  var isSelected = <bool>[false, false, false, false];
  GlobalKey toggleButtonsKey = GlobalKey();
  GlobalKey dialogKey = GlobalKey();
  final TextEditingController textController = TextEditingController();
  IdeasRelation? targetRelation = state.ref.read(selectedRelation);

  if(targetRelation != null) {
    if(targetRelation.fromShape == RelationShape.dot && targetRelation.toShape == RelationShape.dot) {
      isSelected[0] = true;
    } else if(targetRelation.fromShape == RelationShape.dot && targetRelation.toShape == RelationShape.arrow) {
      // originとrelationの左右の位置によって、反映対象を変える
      if (targetRelation.fromIdea.parentLane.indexX <= targetRelation.toIdea.parentLane.indexX) {
        isSelected[1] = true;
      } else {
        isSelected[2] = true;
      }
    } else if(targetRelation.fromShape == RelationShape.arrow && targetRelation.toShape == RelationShape.dot) {
      if (targetRelation.fromIdea.parentLane.indexX <= targetRelation.toIdea.parentLane.indexX) {
        isSelected[2] = true;
      } else {
        isSelected[1] = true;
      }
    } else if(targetRelation.fromShape == RelationShape.arrow && targetRelation.toShape == RelationShape.arrow) {
      isSelected[3] = true;
    }
    textController.text = targetRelation.note;
  }

  return showDialog(
    context: context,
    builder: (context) {
      return StatefulBuilder(
        builder: (context, setState) {
          return SimpleDialog(
            key: dialogKey,
            contentPadding: const EdgeInsets.all(26),
            children: [
              Align( // 閉じるボタン
                alignment: Alignment.centerRight,
                child: Tooltip(
                  message: MaterialLocalizations
                      .of(context)
                      .closeButtonTooltip,
                  child: GestureDetector(
                    onTap: () => Navigator.pop(context),
                    child: const Icon(Icons.close, size: 30),
                  ),
                ),
              ),
              const Text('矢印の種類'),
              Container(height: 10,),
              ToggleButtons(
                key: toggleButtonsKey,
                direction: Axis.vertical,
                borderColor: Colors.white,
                selectedColor: Colors.lightBlueAccent,
                selectedBorderColor: Colors.white,
                isSelected: isSelected,
                onPressed: (index) {
                  // The button that is tapped is set to true, and the others to false.
                  setState(() {
                    for (int i = 0; i < isSelected.length; i++) {
                      isSelected[i] = i == index;
                    }
                  });
                },
                children: [
                  SizedBox(
                    height: 40,
                    width: 180,
                    child: Stack(
                      children: drawRelationLineAt(state, 30,20,150,20, RelationShape.dot, RelationShape.dot, null),
                    ),
                  ),
                  SizedBox(
                    height: 40,
                    width: 180,
                    child: Stack(
                      children: drawRelationLineAt(state, 30,20,150,20, RelationShape.dot, RelationShape.arrow, null),
                    ),
                  ),
                  SizedBox(
                    height: 40,
                    width: 180,
                    child: Stack(
                      children: drawRelationLineAt(state, 30,20,150,20, RelationShape.arrow, RelationShape.dot, null),
                    ),
                  ),
                  SizedBox(
                    height: 40,
                    width: 180,
                    child: Stack(
                      children: drawRelationLineAt(state, 30,20,150,20, RelationShape.arrow, RelationShape.arrow, null),
                    ),
                  ),
                ],
              ),
              Container(height: 10),
              const Text('線の説明'),
              TextField(
                controller: textController,
                decoration: InputDecoration(
                  border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
                ),
              ),
              Container(height: 15),
              const Divider(),
              Container(height: 15),
              Align(alignment: Alignment.center, child:
                SizedBox(
                  width: 120,
                  height: 40,
                  child: ElevatedButton( //決定ボタン
                    onPressed: () {
                      if (isSelected[0]) {
                        targetRelation!.fromShape = RelationShape.dot;
                        targetRelation.toShape = RelationShape.dot;
                      } else if (isSelected[1]) {
                        // originとrelationの左右の位置によって、反映対象を変える
                        if (targetRelation!.fromIdea.parentLane.indexX <= targetRelation.toIdea.parentLane.indexX) {
                          targetRelation.fromShape = RelationShape.dot;
                          targetRelation.toShape = RelationShape.arrow;
                        } else {
                          targetRelation.fromShape = RelationShape.arrow;
                          targetRelation.toShape = RelationShape.dot;
                        }
                      } else if (isSelected[2]) {
                        // originとrelationの左右の位置によって、反映対象を変える
                        if (targetRelation!.fromIdea.parentLane.indexX <= targetRelation.toIdea.parentLane.indexX) {
                          targetRelation.fromShape = RelationShape.arrow;
                          targetRelation.toShape = RelationShape.dot;
                        } else {
                          targetRelation.fromShape = RelationShape.dot;
                          targetRelation.toShape = RelationShape.arrow;
                        }
                      } else if (isSelected[3]) {
                        targetRelation!.fromShape = RelationShape.arrow;
                        targetRelation.toShape = RelationShape.arrow;
                      }
                      targetRelation!.note = textController.text;
                      Navigator.pop(context);
                    },
                    child: const Text('決定')
                  ),
                ),
              ),
              Container(height: 15),
              Align(alignment: Alignment.bottomCenter, child:
                Consumer(
                  builder: (context, ref, child) {
                    return TextButton( //線の削除ボタン
                      style: ButtonStyle(
                        foregroundColor: MaterialStateProperty.all<Color>(Colors.redAccent),
                      ),
                      onPressed: () async {
                        // 確認ダイアログを表示
                        var result = await showDialog<bool>(
                          context: context,
                          barrierDismissible: false,
                          builder: (BuildContext context) {
                            return AlertDialog(
                              title: const Text('確認'),
                              content: const Text('線を削除してよろしいですか?'),
                              actions: <Widget>[
                                TextButton(
                                  child: const Text('Cancel'),
                                  onPressed: () => Navigator.of(context).pop(false),
                                ),
                                TextButton(
                                  child: const Text('OK'),
                                  onPressed: () => Navigator.of(context).pop(true),
                                ),
                              ],
                            );
                          },
                        );
                        if (result == true) {
                          //リレーション削除の実行
                          removeRelation( ref.watch(ideasRelationList.notifier).state , targetRelation! );
                          Navigator.pop(context);
                        }
                      },
                      child: const Text('線の削除')
                    );
                  }
                ),
              ),
            ],
          );
        },
      );
    },
  );
}

Size _getTextSize(String text, TextStyle style) {
  final TextPainter textPainter = TextPainter(
      text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr)
    ..layout(minWidth: 0, maxWidth: double.infinity);
  return textPainter.size;
}

// 指定した座標を元にリレーションの線と文字を描画
List <Widget> drawRelationLineAt(CanvasZoneState state, double startX, double startY, double endX, double endY, RelationShape startShape, RelationShape endShape, IdeasRelation? ideasRelation, {bool lightColor = false}) {
  List <Widget> list = [];
  final double dotSize = state.ref.watch(relationDotSize);
  final double arrowSize = state.ref.watch(relationArrowSize);
  double rotateAdjustX = 0;
  double rotateAdjustY = 0;
  double rotateAdjustXForTransparent = 0;
  double rotateAdjustYForTransparent = 0;
  double scrollDeltaX = 0;//state.ref.watch(canvasScrollDeltaX);
  bool hoverFlg = false;
  double lineWidthDiv2 = 1;
  Color drawColor = lightColor ? Colors.blue.withOpacity(0.4) : Colors.blue;

  void focusEnter() {
    state.ref.watch(relationHoverFlg.notifier).state = true;
    state.ref.watch(selectedRelation.notifier).state = ideasRelation;
  }
  void focusExit() {
    state.ref.watch(relationHoverFlg.notifier).state = false;
    state.ref.watch(selectedRelation.notifier).state = null;
  }

  if(ideasRelation != null) {
    if ( state.ref.watch(relationHoverFlg) && state.ref.read(selectedRelation) == ideasRelation ) {
      hoverFlg = true;
    }
  }

  // 指定座標にドットを描画
  Widget makeDotAt(double dx, double dy) {
    return Positioned(
      top: dy - (dotSize / 2),
      left: dx - (dotSize / 2) + scrollDeltaX,
      width: dotSize,
      height: dotSize,
      child: MouseRegion(
        onEnter: (_) => focusEnter(),
        onExit: (_) => focusExit(),
        child: Container(
          decoration: BoxDecoration(
            color: hoverFlg ? Colors.lightBlueAccent : drawColor,
            shape: BoxShape.circle,
          ),
        ),
      ),
    );
  }

  /* ドットと三角の描画 */
  if(startShape == RelationShape.dot) {
    list.add( //ドットの描画 Origin
      makeDotAt( startX, startY )
    );
  } else if(startShape == RelationShape.arrow){
    list.add( //三角の描画
      Positioned(
        top: startY - (arrowSize / 2),
        left: startX - (arrowSize / 2) + scrollDeltaX,
        child: Transform.rotate(
          angle: (startX == endX) /*レーンが同じ場合*/ ? 0 : getRadian(startX + scrollDeltaX, startY, endX + scrollDeltaX, endY),
          alignment: Alignment.center,
          child: MouseRegion(
            onEnter: (_) => focusEnter(),
            onExit: (_) => focusExit(),
            child: CustomPaint(
              painter: TrianglePainterL(hoverFlg ? Colors.lightBlueAccent : drawColor),
              child: SizedBox(
                width: arrowSize,
                height: arrowSize,
              ),
            ),
          ),
        ),
      ),
    );
  }
  if(endShape == RelationShape.dot) {
    list.add( //ドットの描画 Relation
      makeDotAt( endX, endY )
    );
  } else if(endShape == RelationShape.arrow){
    list.add( //三角の描画
      Positioned(
        top: endY - (arrowSize / 2),
        left: endX - (arrowSize / 2) + scrollDeltaX,
        child: Transform.rotate(
          angle: (startX == endX) /*レーンが同じ場合*/ ? (180 * pi / 180) : getRadian(startX + scrollDeltaX, startY, endX + scrollDeltaX, endY),
          alignment: Alignment.center,
          child: MouseRegion(
            onEnter: (_) => focusEnter(),
            onExit: (_) => focusExit(),
            child: CustomPaint(
              painter: TrianglePainterR(hoverFlg ? Colors.lightBlueAccent : drawColor),
              child: SizedBox(
                width: arrowSize,
                height: arrowSize,
              ),
            ),
          ),
        ),
      ),
    );
  }

  double lineTop=0,lineLeft=0,lineHeight=0,lineWidth=0;
  /* 線の描画 */
  if (startX != endX) {
    /*レーンが違う場合*/
    if ( (endY - startY) > 30) { //右側の点が下側。回転すると、線の太さだけずれて見えてしまうので、それを調整する
      rotateAdjustX = 1;
      rotateAdjustY = 1;
      rotateAdjustXForTransparent = 2;
      rotateAdjustYForTransparent = 0;
    } else if ( (endY - startY) < -30) { //右側の点が上側。回転すると、線の太さだけずれて見えてしまうので、それを調整する
      rotateAdjustX = -1;
      rotateAdjustY = 0;
      rotateAdjustXForTransparent = -2;
      rotateAdjustYForTransparent = 0;
    }
    lineTop   = startY - lineWidthDiv2 + rotateAdjustY;
    lineLeft  = startX + lineWidthDiv2 + scrollDeltaX + rotateAdjustX;
    lineWidth = getDistance(startX + scrollDeltaX, startY, endX + scrollDeltaX, endY) - 2;
    lineHeight= 2;
    list.add( //線の描画
      Stack(
        children:[
          Positioned( //実際の線の描画
            top:  lineTop,
            left: lineLeft,
            child: Transform.rotate(
              angle: getRadian(startX + scrollDeltaX, startY, endX + scrollDeltaX, endY),
              alignment: Alignment.topLeft,
              origin: const Offset( -0.5, -0.5 ),
              child: Container(
                width: lineWidth,
                height: lineHeight,
                decoration: BoxDecoration(
                  color: hoverFlg ? Colors.lightBlueAccent : drawColor,
                ),
              ),
            ),
          ),
          Positioned( //当たり判定用の透明な線
            top:  startY - lineWidthDiv2 + rotateAdjustY + rotateAdjustYForTransparent - 3/*線の太さ分を調整*/,
            left: startX + lineWidthDiv2 + scrollDeltaX + rotateAdjustXForTransparent + rotateAdjustX - 1,
            child: Transform.rotate(
              angle: getRadian(startX + scrollDeltaX, startY, endX + scrollDeltaX, endY),
              alignment: Alignment.topLeft,
              //          origin: Offset( state.ref.watch(relationDotSize)/2 , state.ref.watch(relationDotSize)/2 ),
              child: MouseRegion(
                onEnter: (_) => focusEnter(),
                onExit: (_) => focusExit(),
                child: Container(
                  width: getDistance(startX + scrollDeltaX, startY, endX + scrollDeltaX, endY),
                  height: 8,
                  decoration: const BoxDecoration( color: relationColorTransparent ),
                ),
              ),
            ),
          ),
        ]
      ),
    );
  } else {
    /*レーンが同じ場合*/
    double horizontalMargin = ideaHorizontalMargin - 2;
    lineTop   = startY - lineWidthDiv2;
    lineLeft  = startX + scrollDeltaX + horizontalMargin;
    lineWidth = 2;
    lineHeight= endY- startY + (lineWidthDiv2*2) /*線の太さ分を調整*/;
    list.add( //線の描画
      Stack(
        children:[
          Positioned( //実際の線の描画(縦)
            top:  lineTop,
            left: lineLeft,
            child: Container(
              width: 2,
              height: lineHeight,
              decoration: BoxDecoration(
                color: hoverFlg ? Colors.lightBlueAccent : drawColor,
              ),
            ),
          ),
          Positioned( //実際の線の描画(横1)
            top:  startY - lineWidthDiv2,
            left: startX + lineWidthDiv2 + scrollDeltaX,
            child: Container(
              width: horizontalMargin,
              height: 2,
              decoration: BoxDecoration(
                color: hoverFlg ? Colors.lightBlueAccent : drawColor,
              ),
            ),
          ),
          Positioned( //実際の線の描画(横2)
            top:  endY - lineWidthDiv2,
            left: endX + lineWidthDiv2 + scrollDeltaX,
            child: Container(
              width: horizontalMargin,
              height: 2,
              decoration: BoxDecoration(
                color: hoverFlg ? Colors.lightBlueAccent : drawColor,
              ),
            ),
          ),
          Positioned( //当たり判定用の透明な線(縦)
            top:  startY - lineWidthDiv2 - 2 /*線の太さ分を調整*/,
            left: startX + scrollDeltaX + horizontalMargin - 2,
            child: MouseRegion(
              onEnter: (_) => focusEnter(),
              onExit:  (_) => focusExit(),
              child: Container(
                width: 2 + 4,
                height: endY - startY + (lineWidthDiv2*2) /*線の太さ分を調整*/ + 4,
                decoration: const BoxDecoration( color: relationColorTransparent ),
              ),
            ),
          ),
          Positioned( //当たり判定用の透明な線(横1)
            top:  startY - lineWidthDiv2 - 2 /*線の太さ分を調整*/,
            left: startX + lineWidthDiv2 + scrollDeltaX,
            child: MouseRegion(
              onEnter: (_) => focusEnter(),
              onExit:  (_) => focusExit(),
              child: Container(
                width: horizontalMargin,
                height: 2 + 4,
                decoration: const BoxDecoration( color: relationColorTransparent ),
              ),
            ),
          ),
          Positioned( //当たり判定用の透明な線(横2)
            top:  endY - lineWidthDiv2 - 2 /*線の太さ分を調整*/,
            left: endX + lineWidthDiv2 + scrollDeltaX,
            child: MouseRegion(
              onEnter: (_) => focusEnter(),
              onExit:  (_) => focusExit(),
              child: Container(
                width: horizontalMargin,
                height: 2 + 4,
                decoration: const BoxDecoration( color: relationColorTransparent ),
              ),
            ),
          ),
        ]
      ),
    );
  }

  String note = '';
  if(ideasRelation != null) {
    note = ideasRelation.note;
  }
  /* 文字の描画 */
  if ( note != '' ) {
    /* 線の描画 */
    if (startX != endX) {
      /*レーンが違う場合*/
      double radian = getRadian(startX + scrollDeltaX, startY, endX + scrollDeltaX, endY);

      // radianのイメージ
      // -90° = (-90* pi / 180) =-1.5707963267948966
      // -45° = (-45* pi / 180) =-0.7853981633974483
      //  0°  = ( 0 * pi / 180) = 0
      //  45° = (45 * pi / 180) = 0.7853981633974483
      //  90° = (90 * pi / 180) = 1.5707963267948966
      // 180° = pi              = 3.14

      // 手入力での調整結果
      // -90° left: - 18,   top:  - 0,
      // -75° left: - 15,   top:  - 8,
      // -45° left: - 12,   top: - 12,
      // -30° left: - 5,    top: - 15,
      //  0°  left: - 0,    top: - 18,
      //
      // Offset(13*radian,  -19 + (5*radian.abs())),
      // -90° left: -20.4,  top: - 19+7.8= -11.2
      // -45° left: -10.2,  top: - 19+3.9= -15.1
      //  0°  left:   0,    top: - 19,
      //  45° left:  10.2,  top: - 19+3.9= -15.1
      //  90° left:  20.4,  top: - 19+7.8= -11.2
      //
      //print("$radian (-45 * pi / 180):${(-45 * pi / 180)} (90 * pi / 180):${(90 * pi / 180)} (180 * pi / 180):${(180 * pi / 180)}");

      list.add( //文字の描画
        Stack(
          children: [
            Positioned( //文字の描画
              top: lineTop,
              left: lineLeft,
              width: lineWidth,
              child: Align(
                alignment: Alignment.center,
                child: Transform.translate(
                  offset: Offset(13 * radian, -19 + (4 * radian.abs())),
                  child: Transform.rotate(
                    angle: radian,
                    alignment: Alignment.topLeft,
                    origin: const Offset(-0.5, -0.5),
                    child: Container(
                      color: Colors.transparent,
                      alignment: Alignment.center,
                      child: Text(note, style: const TextStyle(color: Colors.blue),),
                    ),
                  ),
                ),
              ),
            ),
          ]
        ),
      );
    } else {
      /*レーンが同じ場合*/
      double radian = getRadian(lineLeft, lineTop, lineLeft, lineTop+lineHeight);
      Size textSize = _getTextSize(note, const TextStyle(color: Colors.blue));
      list.add( //文字の描画
        Stack(
          children: [
            Positioned( //文字の描画
              top: lineTop,
              left: lineLeft,
              child: Align(
                alignment: Alignment.center,
                child: Transform.translate(
                  offset: Offset(textSize.height + 4, (lineHeight/2) - (textSize.width/2) ),
                  child: Transform.rotate(
                    angle: radian,
                    alignment: Alignment.topLeft,
                    origin: const Offset(-0.5, -0.5),
                    child: Container(
                      color: Colors.transparent,
                      alignment: Alignment.center,
                      child: Text(note, style: const TextStyle(color: Colors.blue),),
                    ),
                  ),
                ),
              ),
            ),
          ]
        ),
      );
    }
  }

  //デバッグ用
  if(relationDebugPaint) {
    list.add(
      Positioned( //デバッグ用
        top: startY,
        left: startX + scrollDeltaX,
        width: 1,
        height: 1,
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.red,
            shape: BoxShape.circle,
          ),
        ),
      ),
    );
    list.add(
      Positioned( //デバッグ用
        top: startY - lineWidthDiv2 + rotateAdjustY,
        left: startX + lineWidthDiv2 + scrollDeltaX,
        width: 1,
        height: 1,
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.orange,
            shape: BoxShape.circle,
          ),
        ),
      ),
    );
  }

  return list;
}

// relationを元にリレーション描画ウィジェットを作成
List<Widget> buildRelationLineWidget(CanvasZoneState state, IdeasRelation relationSet) {
  Idea startIdea, endIdea;
  RelationShape startShape, endShape;
  double startX, startY, endY, endX;
  const double ideaHeight = GlobalConst.eachIdeaHeight;
  bool stickOutFlg = false;

  if (relationSet.fromIdea.parentLane.indexX != relationSet.toIdea.parentLane.indexX ) {
    // leftを必ずstartIdeaとしてしまう。左右逆のパターンもロジックを記述すると、回転の計算などが面倒になったため
    if (relationSet.fromIdea.parentLane.indexX <= relationSet.toIdea.parentLane.indexX) {
      startIdea = relationSet.fromIdea;
      startShape = relationSet.fromShape;
      endIdea = relationSet.toIdea;
      endShape = relationSet.toShape;
    } else {
      startIdea = relationSet.toIdea;
      startShape = relationSet.toShape;
      endIdea = relationSet.fromIdea;
      endShape = relationSet.fromShape;
    }

    // 描画位置から求めると、パン・スクロールしたときにどんどんずれていく
    // RenderBox originIdeaTextFieldBox = origin.textFieldKey.currentContext?.findRenderObject() as RenderBox;
    startY = state.horizontalLanes[startIdea.parentLane.indexY].viewTop + (ideaHeight * startIdea.index) + (ideaHeight/2) - 0.5/*調整用。これが無いとずれて見える*/ ;
    startX = state.verticalLaneHeaders[startIdea.parentLane.indexX].viewLeft + state.verticalLaneHeaders[startIdea.parentLane.indexX].viewWidth - ideaHorizontalMargin - 0.5/*調整用。これが無いとずれて見える*/;
    endY = state.horizontalLanes[endIdea.parentLane.indexY].viewTop +(ideaHeight * endIdea.index) + (ideaHeight/2) - 0.5/*調整用。これが無いとずれて見える*/;
    endX = state.verticalLaneHeaders[endIdea.parentLane.indexX].viewLeft + ideaHorizontalMargin + ideaLeftSpace + (GlobalConst.ideaIndentWidth * endIdea.indentLevel) + 0.5/*調整用。これが無いとずれて見える*/;

    /* 見やすくするためのずらし調整(インデント) */
    if (startIdea.parentLane.indexX < endIdea.parentLane.indexX &&
        endIdea.indentLevel >= 1) { //Relatedが右側にあり、インデントされている場合
      endY += 3; //Related側のインデントの線と重なるのを避けるために少しずらす
    }

    /* 見やすくするためのずらし調整(他の矢印やドットとの重なり:右側) */
    if ( (endY - startY).abs() > 10) { // 線に角度が付いている場合
      bool avoidFlg = false;
      //対象のアイディアに、他にリレーションが付いているか探す
      if(endIdea.relatedIdeasFrom.length >= 2) {
        avoidFlg = true;
      }
      if (avoidFlg) {
        if (startY < endY - 10) { // 矢印が斜めの場合
          endY -= 6; //Related側の矢印と重なるのを避けるために少しずらす
        } else if (startY > endY) {
          endY += 6;
        }
      }
      /* 見やすくするためのずらし調整(他の矢印やドットとの重なり:右側) */
      avoidFlg = false;
      //対象のアイディアに、他にリレーションが付いているか探す
      if(startIdea.relatedIdeasTo.length >= 2) {
        avoidFlg = true;
      }
      if (avoidFlg) {
        if (startY < endY - 10) { // 矢印が斜めの場合
          startY += 6; //Related側の矢印と重なるのを避けるために少しずらす
        } else if (startY > endY) {
          startY -= 6;
        }
      }
    }

  } else{
    /* 同じレーン同士の場合 */
    // 計算を簡単にするため、上を必ずstartIdeaとしてしまう
    if (relationSet.fromIdea.parentLane.indexY < relationSet.toIdea.parentLane.indexY ||
        ( relationSet.fromIdea.parentLane.indexY == relationSet.toIdea.parentLane.indexY && relationSet.fromIdea.index <= relationSet.toIdea.index) ) {
      startIdea = relationSet.fromIdea;
      startShape = relationSet.fromShape;
      endIdea = relationSet.toIdea;
      endShape = relationSet.toShape;
    } else {
      startIdea = relationSet.toIdea;
      startShape = relationSet.toShape;
      endIdea = relationSet.fromIdea;
      endShape = relationSet.fromShape;
    }

    startY = state.horizontalLanes[startIdea.parentLane.indexY].viewTop + (ideaHeight * startIdea.index) + (ideaHeight/2) - 0.5/*調整用。これが無いとずれて見える*/ ;
    startX = state.verticalLaneHeaders[startIdea.parentLane.indexX].viewLeft + state.verticalLaneHeaders[startIdea.parentLane.indexX].viewWidth - ideaHorizontalMargin - 0.5/*調整用。これが無いとずれて見える*/;
    endY = state.horizontalLanes[endIdea.parentLane.indexY].viewTop + (ideaHeight * endIdea.index) + (ideaHeight/2) - 0.5/*調整用。これが無いとずれて見える*/;
    endX = startX;

    /* 見やすくするためのずらし調整(他の矢印やドットとの重なり:右側) */
    bool avoidFlg = false;
    //対象のアイディアに、他にリレーションが付いているか探す
    if(startIdea.relatedIdeasTo.length >= 2) {
      avoidFlg = true;
    }
    if (avoidFlg) {
      if (startY < endY - 10) { // 矢印が斜めの場合
        startY += 6; //Related側の矢印と重なるのを避けるために少しずらす
      } else if (startY > endY) {
        startY -= 6;
      }
    }
  }

  //横レーン内のスクロールに対応した調整
  if (state.horizontalLanes[startIdea.parentLane.indexY].scrollControllerForHLane.hasClients) {
    var aHorizontalLane = state.horizontalLanes[startIdea.parentLane.indexY];
    printWhenDebug("aStartHorizontalLane.scrollControllerForLane.position.pixels:${aHorizontalLane.scrollControllerForHLane.position.pixels}", level:3 );

    startY -= aHorizontalLane.scrollControllerForHLane.position.pixels; //スクロール座標を反映

    if (startY > aHorizontalLane.viewTop + aHorizontalLane.viewHeight) { //レーン内のビューをはみ出している場合(下)
      startY = aHorizontalLane.viewTop + aHorizontalLane.viewHeight;
      startX -= 5;//少しずらす
      startShape = RelationShape.none;
      stickOutFlg = true;
    } else if ( startY < 0 ) { //レーン内のビューをはみ出している場合(上)
      startY = 0;
      startX += 5;//少しずらす
      startShape = RelationShape.none;
      stickOutFlg = true;
    }
  }

  if (state.horizontalLanes[endIdea.parentLane.indexY].scrollControllerForHLane.hasClients) {
    var aHorizontalLane = state.horizontalLanes[endIdea.parentLane.indexY];
    printWhenDebug("aEndHorizontalLane.scrollControllerForLane.position.pixels:${aHorizontalLane.scrollControllerForHLane.position.pixels}", level:3 );

    endY -= aHorizontalLane.scrollControllerForHLane.position.pixels; //スクロール座標を反映

    if (endY > aHorizontalLane.viewTop + aHorizontalLane.viewHeight) { //レーン内のビューをはみ出している場合(下)
      if(stickOutFlg) { //Startが画面外の場合
        return []; //描画しない
      }
      endY = aHorizontalLane.viewTop + aHorizontalLane.viewHeight;
      endX -= 5;
      endShape = RelationShape.none;
      stickOutFlg = true;
    } else if ( endY < 0 ){ //レーン内のビューをはみ出している場合(上)
      if(stickOutFlg) { //Startが画面外の場合
        return []; //描画しない
      }
      endY = 0;
      endX += 5;
      endShape = RelationShape.none;
      stickOutFlg = true;
    }
  }

  //ビューポートを超えた線を描画する場合
  if( state.scrollControllerHorizontal.hasClients )
  {
    if( startX < state.ref.watch(canvasScrollDeltaX).abs() ) {
      stickOutFlg = true;
    } else if ( endX - state.ref.watch(canvasScrollDeltaX).abs() > MediaQuery.of(state.context).size.width  * (1 / state.ref.watch(currentScale)) - GlobalConst.horizontalLaneHeaderDefaultWidth) {
      stickOutFlg = true;
    }
    if( startY < state.ref.watch(canvasScrollDeltaY).abs() ) {
      stickOutFlg = true;
    } else if ( endY - state.ref.watch(canvasScrollDeltaY).abs() > (MediaQuery.of(state.context).size.height - state.globalTop - ( GlobalConst.verticalLaneHeaderHeight*state.ref.watch(currentScale) )  * (1/state.ref.watch(currentScale) ) ) ){
      stickOutFlg = true;
    }
  }

  //描画ロジックへ
  if(relationDebugPaint) {
    //デバッグ用
    List<Widget> result = drawRelationLineAt(state, startX,startY,endX,endY, startShape,endShape, relationSet) ;
    //座標に関するデバッグ用 originの表示
    result.add(
        Stack(children: [
          Positioned( //デバッグ用 originの表示
            top: startIdea == relationSet.fromIdea ? startY : endY,
            left: startIdea == relationSet.fromIdea ? startX + state.ref.watch(canvasScrollDeltaX) : endX + state.ref.watch(canvasScrollDeltaX),
            child: Container(
              width: 3,
              height: 3,
              decoration: const BoxDecoration(color: Colors.deepPurpleAccent,),
            ),
          ),
        ])
    );
    //デバッグ用 dotの中心線
    result.add(
        Stack(children: [
          Positioned( //デバッグ用
            top: endY,
            left: endX + state.ref.watch(canvasScrollDeltaX),
            child: Container(
              width: 500,
              height: 0.5,
              decoration: const BoxDecoration(color: Colors.red,),
            ),
          ),
        ])
    );
    return result;
  } else {
    return drawRelationLineAt(state, startX,startY,endX,endY, startShape,endShape, relationSet, lightColor: stickOutFlg) ;
  }
}

List <Widget> buildDelimiterLineForPanMargin(CanvasZoneState state) {
  List<Widget> list = [];
  for (int i=0; i<state.horizontalLanes.length; i++) {
    var aHorizontalLane = state.horizontalLanes[i];
    list.add(
      Positioned( //横の区切り線 ------
        top: state.ref.watch(canvasScrollDeltaY) + aHorizontalLane.viewTop + aHorizontalLane.viewHeight,
        left: 0,
        child: Container (
            width: state.ref.watch(canvasScrollDeltaX).abs(),
            height: GlobalConst.laneSplitterBodyWidthOrHeight,
            child: Align(
              alignment: Alignment.centerLeft,
              child: Container (
                height: 1.0,
                color: Colors.blue,
              ),
            )
        ),
      ),
    );
  }
  return list;
}

List <Widget> buildIdeasAndRelations(CanvasZoneState state) {
  List<Widget> list = [];

  /* Ideas */
  for (int i=0; i<state.horizontalLanes.length; i++){
    var aHorizontalLane = state.horizontalLanes[i];
    list.add(
      Positioned( //横レーンの1つの配置。ビューポート
        top: aHorizontalLane.viewTop,
        left: 0,
        //width: MediaQuery.of(state.context).size.width-62,
        width: state.allSumVerticalHeaderViewWidths + GlobalConst.laneSplitterBodyWidthOrHeight,
        height: aHorizontalLane.viewHeight,
        child: Scrollbar(
          controller: aHorizontalLane.scrollControllerForHLane,
          thumbVisibility: true,
          trackVisibility: true,
          child: SingleChildScrollView(
            controller: aHorizontalLane.scrollControllerForHLane,
            child: Container(  // スクロール領域用のキャンバスの土台
              width: state.allSumVerticalHeaderViewWidths,
              height: max(aHorizontalLane.getRealHeight(), aHorizontalLane.viewHeight), //縦のスプリッタを描画するために、最低でもviewHeightの高さを確保
              child: ListView.builder( //横レーン内の、縦1レーンごとの箱の生成
                scrollDirection: Axis.horizontal,
                itemCount: aHorizontalLane.childVLaneBodies.length,
                itemBuilder: (context, index) {
                  return Container( //ビューポート用
                    width: state.verticalLaneHeaders[index].viewWidth + GlobalConst.laneSplitterBodyWidthOrHeight,
                    height: aHorizontalLane.viewHeight,
                    color: Colors.transparent,
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children:[
                        ( aHorizontalLane.childVLaneBodies[index].requiredHeight <= aHorizontalLane.viewHeight ? // 横レーンのビュー高さと縦1レーンの高さを比較
                          // 縦1レーンの高さが小さい場合
                          Container( //ボディを描画
                            width: state.verticalLaneHeaders[index].viewWidth,
                            color: Colors.transparent,
                            child: aHorizontalLane.childVLaneBodies[index],
                          ) :
                          // 縦1レーンの高さが大きい場合
                          SizedBox( //レーンの箱を描画
                            width: state.verticalLaneHeaders[index].viewWidth,
                            height: aHorizontalLane.childVLaneBodies[index].requiredHeight,
                            child: SizedBox( //ボディを描画
                              width: state.verticalLaneHeaders[index].viewWidth,
                              child: aHorizontalLane.childVLaneBodies[index],
                            ),
                          )
                        ),
                        Container( //スプリッタ分のスペースを埋める
                          width: GlobalConst.laneSplitterBodyWidthOrHeight,
                          color: Colors.transparent,
                          child: Center(
                            child: Container(
                              width: 1.0,
                              color: (GlobalConst.drawSplitterLineHorizontal ? Colors.blue : Colors.transparent),
                            ),
                          ),
                        ),
                      ],
                    ),
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );

    list.add(
      Positioned( //横の区切り線 ------
        top: aHorizontalLane.viewTop + aHorizontalLane.viewHeight,
        left: 0,
        child: SizedBox (
            width: state.allSumVerticalHeaderViewWidths,
            height: GlobalConst.laneSplitterBodyWidthOrHeight,
            child: Align(
              alignment: Alignment.centerLeft,
              child: Container (
                height: 1.0,
                color: (GlobalConst.drawSplitterLineHorizontal ? Colors.blue : Colors.transparent),
              ),
            )
        ),
      ),
    );

  }

  if (state.verticalLaneHeaders.isEmpty) return list;
  if (state.horizontalLanes.isEmpty) return list;

  /* Relation */
  IdeasRelation? lazyBuildTarget;
  for (IdeasRelation relationSet in state.ref.watch(ideasRelationList)) {
    if ( state.ref.watch(relationHoverFlg) && state.ref.read(selectedRelation) == relationSet ) {
      //アクティブな線は、最後に描画することで手前に持ってくる(そうしないと他の線に隠れてしまう)
      lazyBuildTarget = relationSet;
    } else {
      list.addAll( buildRelationLineWidget(state, relationSet) );
    }
  }
  if(lazyBuildTarget != null) {
    list.addAll( buildRelationLineWidget(state, lazyBuildTarget) );
  }

/*
  //デバッグ用
  list.add(
    Stack(children: [
      Positioned( //デバッグ用
        top:  0,
        left: state.ref.watch(eachLaneWidth) - ideaHorizontalMargin - 50 + state.ref.watch(canvasScrollDeltaX),
        child: Container(
          width: 500,
          height: 1,
          decoration: BoxDecoration(color: Colors.red,),
        ),
      ),
      Positioned( //デバッグ用
        top:  (ideaHeight / 2),
        left: state.ref.watch(eachLaneWidth) - ideaHorizontalMargin - 50 + state.ref.watch(canvasScrollDeltaX),
        child: Container(
          width: 1000,
          height: 0.5,
          decoration: BoxDecoration(color: Colors.green,),
        ),
      ),
    ],
    ),
  );
*/

  return list;
}

void indentLinePaint(Canvas canvas,Size size,BuildContext context, int index, int indentLevel, double textFieldHeight, 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 ((LaneBodyInheritedWidget.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) - (ideaVerticalMargin * 2);

      //縦の線:自分の領域外でどのくらい上まで引くかを計算
      upperIndentLevel = LaneBodyInheritedWidget.of(context)
          .ideas[upperIndex].indentLevel;
      if (indentLevel <= upperIndentLevel) {
        while (indentLevel < upperIndentLevel && upperIndex >= 0) {
          startY -= (textFieldHeight + ideaVerticalMargin * 2);
          upperIndex--;
          upperIndentLevel = LaneBodyInheritedWidget.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 (LaneBodyInheritedWidget.of(context)
        .ideas[i].indentLevel == myIndentLevel - 1) {
      startIndex = i;
      break;
    }
  }
  // Find the end index
  for (int i = myIndex + 1;
  i < LaneBodyInheritedWidget.of(context).ideas.length;
  i++) {
    if ((LaneBodyInheritedWidget.of(context)
        .ideas[i].indentLevel) >= myIndentLevel) {
      endIndex = i;
      break;
    }
  }
  CanvasZoneInheritedWidget.of(context).setState(() {}); //Relationの線を描画
}

void funcIdeaStyleToggleBold(Idea? focusedIdea, {bool fromToolbar = false}) {
  focusedIdea?.key.currentState?.setState(() {
    focusedIdea.textStyleBold = !focusedIdea.textStyleBold;
    focusedIdea.key.currentState?.reflectMyTextStyle();
  });
  if(fromToolbar) {
    focusedIdea?.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcIdeaStyleToggleItalic(Idea? focusedIdea, {bool fromToolbar = false}) {
  focusedIdea?.key.currentState?.setState(() {
    focusedIdea.textStyleItalic = !focusedIdea.textStyleItalic;
    focusedIdea.key.currentState?.reflectMyTextStyle();
  });
  if(fromToolbar) {
    focusedIdea?.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcIdeaStyleToggleUnderLine(Idea? focusedIdea, {bool fromToolbar = false}) {
  focusedIdea?.key.currentState?.setState(() {
    focusedIdea.textStyleUnderLine = !focusedIdea.textStyleUnderLine;
    if(focusedIdea.textStyleStrikeThrough) {
      focusedIdea.textStyleStrikeThrough = false;//取り消し線とは両立しないためオフにする
    }
    focusedIdea.key.currentState?.reflectMyTextStyle();
  });
  if(fromToolbar) {
    focusedIdea?.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcIdeaStyleToggleLineThrough(Idea? focusedIdea, {bool fromToolbar = false}) {
  focusedIdea?.key.currentState?.setState(() {
    focusedIdea.textStyleStrikeThrough = !focusedIdea.textStyleStrikeThrough;
    if(focusedIdea.textStyleUnderLine) {
      focusedIdea.textStyleUnderLine = false;//下線とは両立しないためオフにする
    }
    focusedIdea.key.currentState?.reflectMyTextStyle();
  });
  if(fromToolbar) {
    focusedIdea?.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcIdeaStyleTextColor(Idea? focusedIdea, {bool fromToolbar = false}) {
  Color pickerColor = const Color(0xff443a49);
  Color currentColor = focusedIdea!.textColor;

  void changeColor(Color color) {
    focusedIdea.key.currentState?.setState(() {
      pickerColor = color;
    });
  }

  showDialog(
    context: focusedIdea.key.currentContext!,
    builder: (context) {
      return StatefulBuilder(
          builder: (context, setState) {
            return AlertDialog(
              title: const Text('文字色の選択'),
              content: SingleChildScrollView(
                child: BlockPicker(
                  pickerColor: currentColor,
                  onColorChanged: changeColor,
                ),
                // child: MaterialPicker( //showLabel: true, // only on portrait mode // ),
                // child: ColorPicker(),
                // child: BlockPicker(),
                // child: MultipleChoiceBlockPicker(),
              ),
              actions: <Widget>[
                OutlinedButton(
                  child: const Text('Cancel'),
                  onPressed: () {
                    LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).setState(() {
                      //currentColor = pickerColor;
                    });
                    Navigator.of(LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).context).pop();
                  },
                ),
                ElevatedButton(
                  child: const Text(' O K '),
                  onPressed: () {
                    LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).setState(() {
                      focusedIdea.textColor = pickerColor;
                      focusedIdea.key.currentState!.reflectMyTextStyle();
                    });
                    Navigator.of(LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).context).pop();
                  },
                ),
              ],
            );
          }
      );
    }
  );

  focusedIdea.key.currentState?.setState(() {
    focusedIdea.key.currentState?.reflectMyTextStyle();
  });
  if(fromToolbar) {
    focusedIdea.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcIdeaFocusMoveUp(Idea? focusedIdea, {bool fromToolbar = false}) {
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).focusNextIdea(focusedIdea.index, -1);
}

void funcIdeaFocusMoveDown(Idea? focusedIdea, {bool fromToolbar = false}) {
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).focusNextIdea(focusedIdea.index, 1);
}

void funcIdeaMoveUp(Idea? focusedIdea, {bool fromToolbar = false}){
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).setState(() {
    LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).moveIdea(focusedIdea.key.currentState!, focusedIdea.index, -1);
    LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).widget.rebuildItemIndexesAndUpdateRequiredHeight(0); //再度構築しなおさないと、インデックスがおかしくなる
  });
  if(fromToolbar) {
    focusedIdea.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcIdeaMoveDown(Idea? focusedIdea, {bool fromToolbar = false}){
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).setState(() {
    LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).moveIdea(focusedIdea.key.currentState!, focusedIdea.index, 1);
    LaneBodyInheritedWidget.of(focusedIdea.key.currentContext!).widget.rebuildItemIndexesAndUpdateRequiredHeight(0); //再度構築しなおさないと、インデックスがおかしくなる
  });
  if(fromToolbar) {
    focusedIdea.key.currentState?.textFocusNode.requestFocus();
  }
}

void moveAndInsertIdeaToAnotherLaneUp(Idea? focusedIdea, {bool fromToolbar = false}) {
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).moveAndInsertIdeaToAnotherLane(focusedIdea.key.currentState!, focusedIdea.parentLane.indexY - 1, focusedIdea.index, 0);
  if(fromToolbar) { //アイディアが移動しなかった場合フォーカスがツールバーに移動してしまうため、その対策
    focusedIdea.key.currentState?.textFocusNode.requestFocus();
  }
}
void moveAndInsertIdeaToAnotherLaneDown(Idea? focusedIdea, {bool fromToolbar = false}) {
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).moveAndInsertIdeaToAnotherLane(focusedIdea.key.currentState!, focusedIdea.parentLane.indexY + 1, focusedIdea.index, 0);
}
void moveAndInsertIdeaToAnotherLaneLeft(Idea? focusedIdea, {bool fromToolbar = false}) {
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).moveAndInsertIdeaToAnotherLane(focusedIdea.key.currentState!, focusedIdea.parentLane.indexY, focusedIdea.index, - 1);
  if(fromToolbar) { //アイディアが移動しなかった場合フォーカスがツールバーに移動してしまうため、その対策
    focusedIdea.key.currentState?.textFocusNode.requestFocus();
  }
}
void moveAndInsertIdeaToAnotherLaneRight(Idea? focusedIdea, {bool fromToolbar = false}) {
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).moveAndInsertIdeaToAnotherLane(focusedIdea.key.currentState!, focusedIdea.parentLane.indexY, focusedIdea.index,   1);
}

void funcIndentIncrease(Idea? focusedIdea, {bool fromToolbar = false}){
  focusedIdea?.key.currentState?.setState(() {
    focusedIdea.indentLevel += 1;
  });
  if(fromToolbar) {
    focusedIdea?.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcIndentDecrease(Idea? focusedIdea, {bool fromToolbar = false}){
  focusedIdea?.key.currentState?.setState(() {
    focusedIdea.indentLevel = max(0, focusedIdea.indentLevel - 1);
  });
  if(fromToolbar) {
    focusedIdea?.key.currentState?.textFocusNode.requestFocus();
  }
}

void funcStartRelationMode(Idea? focusedIdea, {bool fromToolbar = false}){
  focusedIdea?.key.currentState?.ref.watch(relatedIdeaSelectMode.notifier).state = true;
  focusedIdea?.key.currentState?.ref.watch(originIdeaForSelectMode.notifier).state = focusedIdea;
  if(fromToolbar) {
    focusedIdea?.key.currentState?.textFocusNode.requestFocus();
  }
  ScaffoldMessenger.of(focusedIdea!.key.currentState!.context).showSnackBar(
    const SnackBar( content: Text( '接続先を選んでEnterキーを押してください'),)
  );
}

void funcAddNewIdeaWithFocus(Idea? focusedIdea, {bool fromToolbar = false}){
  focusedIdea?.key.currentState?.textFocusNode.unfocus();
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).addNewIdeaWithFocus(focusedIdea.index);
}

void funcAddNewChildIdeaWithFocus(Idea? focusedIdea, {bool fromToolbar = false}){
  focusedIdea?.key.currentState?.textFocusNode.unfocus();
  LaneBodyInheritedWidget.of(focusedIdea!.key.currentContext!).addNewIdeaWithFocus(focusedIdea.index, indentAdjust: 1);
}

void funcCloseContextMenu(Idea? focusedIdea, {bool fromToolbar = false}) {
  CanvasZoneInheritedWidget.of(focusedIdea!.key.currentContext!).focusedIdea = null;
  CanvasZoneInheritedWidget.of(focusedIdea.key.currentContext!).setState(() {});
}

Widget buildContextMenuWidget(CanvasZoneState state, Idea? focusedIdea) {
  Widget result;
  double basisTop, basisLeft;
  double menuWidth = 300, menuHeight = 120;
  bool paintShowCase = state.ref.watch(showContextMenuDebugFlg);
  double showCaseHeight = 550;

  if(paintShowCase) {
    menuHeight += showCaseHeight;
  }

  if(focusedIdea == null) {
    return Container(width: 1, height: 1, color:Colors.transparent);
  } else {
    //座標の基準はCanvasTop。globalTopが含まれた場所が0地点で、ヘッダー高さは含まれていない
    basisTop = GlobalConst.verticalLaneHeaderHeight + state.horizontalLanes[focusedIdea.parentLane.indexY].viewTop + GlobalConst.eachIdeaHeight*(focusedIdea.index+1);
    basisLeft= GlobalConst.horizontalLaneHeaderDefaultWidth + state.verticalLaneHeaders[focusedIdea.parentLane.indexX].viewLeft + state.verticalLaneHeaders[focusedIdea.parentLane.indexX].viewWidth - ideaHorizontalMargin - menuWidth - 50;
//    basisLeft= GlobalConst.horizontalLaneHeaderDefaultWidth + state.verticalLaneHeaders[focusedIdea.parentLane.indexX].viewLeft + ideaHorizontalMargin + ideaLeftSpace + (GlobalConst.ideaIndentWidth * focusedIdea.indentLevel) + 50;
  }

  Widget buildButton(double top, double left, Widget icon, String toolTipStr, void Function(Idea? focusedIdea, {bool fromToolbar}) f, {bool selected=false, Color? selectedColor}) {
    return
      Positioned(
        top:  top,
        left: left,
        child:
        IconButton(
          padding: EdgeInsets.zero,
          color: (selected) ? Colors.blue : ( selectedColor != null ) ? selectedColor : Colors.black,
          splashRadius: 18,
          tooltip: toolTipStr,
          onPressed: () { f(focusedIdea, fromToolbar: true);},
          icon: icon,
        ),
      );
  }

  List<Widget> getIcons() {
    List<Widget> result = [];
    double xSize=25, dx=0;
    double groupY, groupX;

    groupY= -5; groupX=  5;
    result.add( buildButton(groupY +  0, groupX + menuWidth-40, const Icon(Icons.close), "閉じる", funcCloseContextMenu ) , );
    dx = 0;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_indent_decrease), "", funcIndentDecrease ), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_indent_increase), "", funcIndentIncrease ), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_down), "", funcIdeaMoveDown), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_up), "", funcIdeaMoveUp), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_left), "", moveAndInsertIdeaToAnotherLaneLeft), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_right), "", moveAndInsertIdeaToAnotherLaneRight), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_down), "", moveAndInsertIdeaToAnotherLaneDown), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_up), "", moveAndInsertIdeaToAnotherLaneUp), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.polyline_outlined), "関係性を指定", funcStartRelationMode), ); dx +=xSize;
    groupY= 35; dx=0;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add), "新規項目", funcAddNewIdeaWithFocus), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add_circle_outline), "新規項目(子)", funcAddNewChildIdeaWithFocus), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_bold), "太字", funcIdeaStyleToggleBold, selected: focusedIdea.textStyleBold)); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_italic), "斜線", funcIdeaStyleToggleItalic, selected: focusedIdea.textStyleItalic), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_underline), "下線", funcIdeaStyleToggleUnderLine, selected: focusedIdea.textStyleUnderLine), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_strikethrough), "取消線", funcIdeaStyleToggleLineThrough, selected: focusedIdea.textStyleStrikeThrough), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_color_text), "文字色", funcIdeaStyleTextColor, selectedColor: focusedIdea.textColor), ); dx +=xSize;
    groupY= 70; dx=0;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.check_box_outlined), "", funcIndentDecrease), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_list_numbered), "", funcIndentDecrease), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.undo), "", funcIndentDecrease), ); dx +=xSize;
    result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.redo), "", funcIndentDecrease), ); dx +=xSize;
    if(paintShowCase) {
      groupY=120; groupX= 10; dx=0;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_list_bulleted), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_size), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.drive_file_rename_outline), "", funcIndentDecrease), ); dx +=xSize;
      dx += 10;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add_comment_outlined), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add_link), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.link), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add_photo_alternate_outlined), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.image_outlined), "", funcIndentDecrease), ); dx +=xSize;
      groupY=150; groupX= 10; dx=0;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_color_fill), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_align_left), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_align_center), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.format_align_right), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.add_reaction_outlined), "", funcIndentDecrease), ); dx +=xSize;

      groupX = 130;
      groupY = menuHeight - showCaseHeight + 150;
      result.add( buildButton(groupY +  0, groupX + 20, const Icon(Icons.keyboard_arrow_up), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX +  0, const Icon(Icons.keyboard_arrow_left), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX + 40, const Icon(Icons.keyboard_arrow_right), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 40, groupX + 20, const Icon(Icons.keyboard_arrow_down), "", funcIndentDecrease), );
      groupY += 70;
      result.add( buildButton(groupY +  0, groupX + 20, const Icon(Icons.keyboard_double_arrow_up), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX +  0, const Icon(Icons.keyboard_double_arrow_left), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX + 40, const Icon(Icons.keyboard_double_arrow_right), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 40, groupX + 20, const Icon(Icons.keyboard_double_arrow_down), "", funcIndentDecrease), );
      groupY += 70;
      result.add( buildButton(groupY +  0, groupX + 20, const Icon(Icons.keyboard_arrow_up), "", funcIndentDecrease), );
      result.add( buildButton(groupY +  0, groupX + 40, const Icon(Icons.keyboard_double_arrow_up), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX +  0, const Icon(Icons.keyboard_double_arrow_left), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX + 60, const Icon(Icons.keyboard_double_arrow_right), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 40, groupX + 20, const Icon(Icons.keyboard_arrow_down), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 40, groupX + 40, const Icon(Icons.keyboard_double_arrow_down), "", funcIndentDecrease), );
      groupY += 70;
      result.add( buildButton(groupY +  0, groupX + 20, const Icon(Icons.arrow_upward), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX +  0, const Icon(Icons.arrow_back), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 20, groupX + 40, const Icon(Icons.arrow_forward), "", funcIndentDecrease), );
      result.add( buildButton(groupY + 40, groupX + 20, const Icon(Icons.arrow_downward), "", funcIndentDecrease), );
      dx = 0; groupY += 75;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_left), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_up), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_down), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_up), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_down), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_right), "", funcIndentDecrease), ); dx +=xSize;
      dx = 0; groupY += 30;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_down), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_up), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_down), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_up), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_left), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_right), "", funcIndentDecrease), ); dx +=xSize;
      dx = 0; groupY += 30;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_up), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_arrow_down), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_up), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_down), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_left), "", funcIndentDecrease), ); dx +=xSize;
      result.add( buildButton(groupY +  0, groupX + dx, const Icon(Icons.keyboard_double_arrow_right), "", funcIndentDecrease), ); dx +=xSize;

      result.add(
        Positioned( left: 0, top: menuHeight - showCaseHeight + 150,
          child: Container(
            width: 120, height: showCaseHeight,
            color: Colors.yellow[50],
            child: Column(
              children: [
                IconButton(splashRadius: 20, onPressed: () {},
                  icon: const Icon(Icons.format_indent_decrease),
                ),
                OutlinedButton.icon(
                  icon: const Icon(Icons.arrow_forward),
                  label: const Text(''),
                  onPressed: () {},
                ),
                ElevatedButton.icon(
                  icon: const Icon(Icons.arrow_forward, color: Colors.white,),
                  label: const Text(''),
                  onPressed: () {},
                ),
                const SizedBox(height: 10,),
                ElevatedButton.icon(
                  icon: const Icon(Icons.add_road, color: Colors.white,),
                  label: const Text(''),
                  onPressed: () {},
                ),
                const SizedBox(height: 5,),
                ElevatedButton.icon(
                  icon: const Icon(Icons.remove_road, color: Colors.white,),
                  label: const Text(''),
                  onPressed: () {},
                ),
              ],
            ),
          ),
        ),
      );
    }
    return result;
  }
  
  result =
    Positioned( // コンテクストメニュー
      top: basisTop  + state.contextDyAdjust,
      left:basisLeft + state.contextDxAdjust,
      child: GestureDetector(
        onPanUpdate: (details) {
          state.setState(() {
            state.contextDxAdjust += details.delta.dx;
            state.contextDyAdjust += details.delta.dy;
          });
        },
        child: Container (
          width: menuWidth,
          height: menuHeight,
          color: Colors.black12,
          child: Stack(
            children: getIcons(),
          ),
        )
      ),
    );

  return result;
}

Future<String> get defaultFileName async {
  final directory = await getApplicationDocumentsDirectory(); //webでは使えない
  return '${directory.path}/default.json';
}

List<Map<String, dynamic>> getVerticalLaneHeadersJson (CanvasZoneState state) {
  List<Map<String, dynamic>> resultMapList = [];

  for (var verticalLaneHeader in state.verticalLaneHeaders ) {
    resultMapList.add({
      'index': verticalLaneHeader.index,
      'verticalTitle': verticalLaneHeader.key.currentState?.titleController.text ?? '',
      'verticalViewWidth': verticalLaneHeader.viewWidth,
    });
  }
  return resultMapList;
}

List<Map<String, dynamic>> getHorizontalLanesJson (CanvasZoneState state) {
  List<Map<String, dynamic>> resultMapList = [];

  for (var horizontalLane in state.horizontalLanes ) {
    List<Map<String, dynamic>> verticalLanesJson = [];
    for (var verticalLane in horizontalLane.childVLaneBodies) {
      List<Map<String, dynamic>> ideasJson = [];
      for (var idea in verticalLane.ideas) {
        List<Map<String, dynamic>> textStyleJson = [];
        if(idea.textStyleBold) {
          textStyleJson.add({'bold': true});
        }
        if(idea.textStyleItalic) {
          textStyleJson.add({'italic': true});
        }
        if(idea.textStyleUnderLine) {
          textStyleJson.add({'underLine': true});
        }
        if(idea.textStyleStrikeThrough) {
          textStyleJson.add({'strikeThrough': true});
        }
        if(idea.textColor != Colors.black) {
          textStyleJson.add({'textColor': idea.textColor.value});
        }

        ideasJson.add({
          'index': idea.index,
          'indentLevel': idea.indentLevel,
          'content': idea.key.currentState?.textController.text ?? '',
          'textStyles': textStyleJson,
        });
      }
      verticalLanesJson.add({
        'index': verticalLane.indexX,
        'ideas': ideasJson,
      });
    }

    resultMapList.add({
      'index': horizontalLane.index,
      'horizontalTitle': horizontalLane.header.title,
      'horizontalViewHeight': horizontalLane.viewHeight,
      'verticalLanes': verticalLanesJson,
    });
  }
  return resultMapList;
}

List<Map<String, dynamic>> getIdeasRelationJson (CanvasZoneState state) {
  List<Map<String, dynamic>> resultMapList = [];

  state.ref.read(ideasRelationList).forEach((relationSet) {
    resultMapList.add({
      'from_lane_indexY': relationSet.fromIdea.parentLane.indexY,
      'from_lane_indexX': relationSet.fromIdea.parentLane.indexX,
      'from_idea_index': relationSet.fromIdea.index,
      'from_shape':      relationSet.fromShape == RelationShape.dot ? 'dot' : 'arrow',
      'to_lane_indexY': relationSet.toIdea.parentLane.indexY,
      'to_lane_indexX': relationSet.toIdea.parentLane.indexX,
      'to_idea_index': relationSet.toIdea.index,
      'to_shape':      relationSet.toShape == RelationShape.dot ? 'dot' : 'arrow',
      'note':               relationSet.note,
    });
  });
  return resultMapList;
}


Future<void> saveDataToFile({String? fileName, required CanvasZoneState? state}) async {
  fileName ??= await defaultFileName;

  if(state == null) return;

  List<Map<String, dynamic>> verticalLaneHeadersJson = getVerticalLaneHeadersJson(state);
  List<Map<String, dynamic>> horizontalLanesJson = getHorizontalLanesJson(state);
  List<Map<String, dynamic>> ideasRelationJson = getIdeasRelationJson(state);

  String jsonString = jsonEncode({'verticalLaneHeaders': verticalLaneHeadersJson, 'horizontalLanes': horizontalLanesJson, 'ideasRelation': ideasRelationJson});

  await ExportFile().exportFile(fileName, jsonString);
}

Future<void> saveDataToCloud({required CanvasZoneState? state}) async {
  final userID = FirebaseAuth.instance.currentUser?.uid ?? 'test';

  printWhenDebug("【saveDataToCloud】 userID:$userID");
  if(state == null) return;

  List<Map<String, dynamic>> verticalLaneHeadersJson = getVerticalLaneHeadersJson(state);
  List<Map<String, dynamic>> horizontalLanesJson = getHorizontalLanesJson(state);
  List<Map<String, dynamic>> ideasRelationJson = getIdeasRelationJson(state);

  //コレクションがフォルダで、ドキュメントがファイルのイメージ。ただしドキュメントは軽量にする必要があり、サブコレクションを使用することが推奨される
  DocumentReference doc1 = FirebaseFirestore.instance.collection('users_data').doc(userID).collection('canvas').doc('canvas1');
  try {
    for (var element in verticalLaneHeadersJson) {
      int index = element['index'];
      await doc1.collection('verticalLaneHeaders').doc(index.toString()).set(element);
    }
    for (var element in horizontalLanesJson) {
      int index = element['index'];
      await doc1.collection('horizontalLanes').doc(index.toString()).set(element);
    }
    int index = 0;
    for (var element in ideasRelationJson) {
      await doc1.collection('ideasRelation').doc(index.toString()).set(element);
      index++;
    }
  } catch (e) {
    printWhenDebug('Error : $e');
  }

/*
  await FirebaseFirestore.instance
      .collection('users') // コレクションID
      .doc('id_abc') // ドキュメントID
      .collection('orders')
      .doc('id_567')
      .set({'name': '鈴木', 'age': 41}); // データ
*/
}

VerticalLaneHeader getVerticalLaneHeaderFromMap( Map<String, dynamic> map, CanvasZoneState? state ) {
  var newLaneHeader = VerticalLaneHeader(
    index: map['index'] ?? 0,
    viewWidth: map['verticalViewWidth'] ?? state!.ref.watch(defaultEachLaneWidth),
  );
  newLaneHeader.title = map['verticalTitle'] ?? '';

  return newLaneHeader;
}

HorizontalLane getHorizontalLaneFromMap( Map<String, dynamic> map, CanvasZoneState? state ) {
  var newHorizontalLane = HorizontalLane(
      map['index'] ?? 0,
      map['horizontalTitle'] ?? ''
  );
  newHorizontalLane.viewHeight = map['horizontalViewHeight'] ?? GlobalConst.horizontalLaneDefaultViewHeight;

  printWhenDebug("getHorizontalLaneFromMap index:${newHorizontalLane.index}");
  List<LaneBody> newVerticalLanes = [];
  for (var verticalLaneJson in map['verticalLanes']) {
    List<Idea> loadedIdeas = [];

    var newLane = LaneBody(
      indexX: verticalLaneJson['index'] ?? 0,
      indexY: newHorizontalLane.index,
      title: verticalLaneJson['verticalTitle'] ?? '',
      ideas: loadedIdeas,
    );
    printWhenDebug(" newLane indexX:${newLane.indexX} title:${newLane.title}");

    for (var ideaJson in verticalLaneJson['ideas'] ?? false) {
      Idea newIdea = Idea(
        index: ideaJson['index'],
        indentLevel: ideaJson['indentLevel'],
        parentLane: newLane,
      );
      if(ideaJson['textStyles'] != null) {
        for (var styleStringJson in ideaJson['textStyles']) {
          if(styleStringJson['bold'] ?? false == true) {
            newIdea.textStyleBold = true;
          }
          if(styleStringJson['italic'] ?? false == true) {
            newIdea.textStyleItalic = true;
          }
          if(styleStringJson['underLine'] ?? false == true) {
            newIdea.textStyleUnderLine = true;
          }
          if(styleStringJson['strikeThrough'] ?? false == true) {
            newIdea.textStyleStrikeThrough = true;
          }
          if(styleStringJson['textColor'] != null) {
            newIdea.textColor = Color(styleStringJson['textColor']);
          }
        }
        newIdea.reflectMyTextStyle();
      }
      newIdea.content = ideaJson['content'];
      loadedIdeas.add(newIdea);
    }

    newLane.updateRequiredHeight();
    newVerticalLanes.add(newLane);
  }
  newHorizontalLane.childVLaneBodies = newVerticalLanes;

  return newHorizontalLane;
}

IdeasRelation getIdeasRelationFromMap( Map<String, dynamic> map, CanvasZoneState state ) {
  printWhenDebug("getIdeasRelationFromMap");
  var fromLaneIndexY = map['from_lane_indexY'];
  var fromLaneIndexX = map['from_lane_indexX'];
  var fromIdeaIndex = map['from_idea_index'];
  var toLaneIndexY = map['to_lane_indexY'];
  var toLaneIndexX = map['to_lane_indexX'];
  var toIdeaIndex = map['to_idea_index'];
  IdeasRelation relationSet = IdeasRelation(
      fromIdea: state.horizontalLanes[fromLaneIndexY].childVLaneBodies[fromLaneIndexX].ideas[fromIdeaIndex], toIdea: state.horizontalLanes[toLaneIndexY].childVLaneBodies[toLaneIndexX].ideas[toIdeaIndex]);
  if(map['from_shape'] == 'arrow') {
    relationSet.fromShape = RelationShape.arrow;
  }
  if(map['to_shape'] == 'arrow') {
    relationSet.toShape = RelationShape.arrow;
  }
  relationSet.note = map['note'];
  state.ref.watch(ideasRelationList.notifier).state.add(relationSet);

  state.horizontalLanes[fromLaneIndexY].childVLaneBodies[fromLaneIndexX].ideas[fromIdeaIndex].relatedIdeasTo.add(state.horizontalLanes[toLaneIndexY].childVLaneBodies[toLaneIndexX].ideas[toIdeaIndex]);
  state.horizontalLanes[toLaneIndexY].childVLaneBodies[toLaneIndexX].ideas[toIdeaIndex].relatedIdeasFrom.add(state.horizontalLanes[fromLaneIndexY].childVLaneBodies[fromLaneIndexX].ideas[fromIdeaIndex]);

  return relationSet;
}

Future<void> loadDataFromCloud({required CanvasZoneState? state}) async {
  if(state == null) return;

  final userID = FirebaseAuth.instance.currentUser?.uid ?? 'test';
  final canvasData = FirebaseFirestore.instance.collection('users_data').doc(userID).collection('canvas').doc('canvas1');
  printWhenDebug("【loadDataFromCloud】 userID:$userID");

  final verticalLaneHeadersCollection = await canvasData.collection('verticalLaneHeaders').get();
  List<DocumentSnapshot> verticalLaneHeadersDoc = verticalLaneHeadersCollection.docs;
  final horizontalLanesCollection = await canvasData.collection('horizontalLanes').get();
  List<DocumentSnapshot> horizontalLanesDoc = horizontalLanesCollection.docs;
  final ideasRelationCollection = await canvasData.collection('ideasRelation').get();
  List<DocumentSnapshot> ideasRelationDoc = ideasRelationCollection.docs;

  state.clearCurrentData();

  for (var verticalLaneHeaderItem in verticalLaneHeadersDoc) {
    state.verticalLaneHeaders.add( getVerticalLaneHeaderFromMap(verticalLaneHeaderItem.data() as Map<String, dynamic>, state) );
  }

  for (var horizontalLaneItem in horizontalLanesDoc) {
    state.horizontalLanes.add( getHorizontalLaneFromMap(horizontalLaneItem.data() as Map<String, dynamic>, state) );
  }

  for (var ideaRelationItem in ideasRelationDoc) {
    getIdeasRelationFromMap(ideaRelationItem.data() as Map<String, dynamic>, state);
  }
}


Future<void> loadDataFromFile({required XFile xFile, required CanvasZoneState? state}) async {
  if(state == null) return;

  Map<String, dynamic> jsonMap = jsonDecode(await xFile.readAsString());

  state.clearCurrentData();

  if (jsonMap['verticalLaneHeaders']!=null) {
    for (var verticalLaneHeadersJson in jsonMap['verticalLaneHeaders']) {
      state.verticalLaneHeaders.add( getVerticalLaneHeaderFromMap(verticalLaneHeadersJson, state) );
    }
  }

  for (var horizontalLaneJson in jsonMap['horizontalLanes']) {
    state.horizontalLanes.add( getHorizontalLaneFromMap(horizontalLaneJson, state) );
  }

  if (jsonMap['ideasRelation'] != null) {
    for (var ideaRelationJson in jsonMap['ideasRelation']) {
      getIdeasRelationFromMap(ideaRelationJson, state);
    }
  }
}

class Tategaki extends StatelessWidget {
  Tategaki(
      this.text, {
        this.style,
        this.space = 12,
      });

  final String text;
  final TextStyle? style;
  final double space;

  @override
  Widget build(BuildContext context) {
    final splitText = text.split("\n");
    return Column(
      textDirection: TextDirection.rtl,
      children: [
        for (var s in splitText) _textBox(s.runes),
      ],
    );
  }

  Widget _textBox(Runes runes) {
    return Wrap(
      textDirection: TextDirection.rtl,
      direction: Axis.vertical,
      children: [
        for (var rune in runes)
          Row(
            children: [
//              SizedBox(width: space),
              SizedBox(
                width: 30,
                height: 25,
                child:
                Align(
                  alignment: (String.fromCharCode(rune) == '、' || String.fromCharCode(rune) == '。') ? Alignment.topRight : Alignment.center,
                  child: _character(String.fromCharCode(rune)),
                ),
              ),
            ],
          )
      ],
    );
  }

  Widget _character(String char) {
    if (VerticalRotated.map[char] != null) {
      return Text(VerticalRotated.map[char]!, style: style);
    } else {
      return Text(char, style: style);
    }
  }
}

class VerticalRotated {
  static const map = {
    ' ': ' ',
    '↑': '→',
    '↓': '←',
    '←': '↑',
    '→': '↓',
    '。': '︒',
    '、': '︑',
    'ー': '丨',
    '─': '丨',
    '-': '丨',
    'ー': '丨',
    '_': '丨 ',
    '−': '丨',
    '-': '丨',
    '—': '丨',
    '〜': '丨',
    '~': '丨',
    '/': '\',
    '…': '︙',
    '‥': '︰',
    '︙': '…',
    ':': '︓',
    ':': '︓',
    ';': '︔',
    ';': '︔',
    '=': '॥',
    '=': '॥',
    '(': '︵',
    '(': '︵',
    ')': '︶',
    ')': '︶',
    '[': '﹇',
    "[": '﹇',
    ']': '﹈',
    ']': '﹈',
    '{': '︷',
    '{': '︷',
    '<': '︿',
    '<': '︿',
    '>': '﹀',
    '>': '﹀',
    '}': '︸',
    '}': '︸',
    '「': '﹁',
    '」': '﹂',
    '『': '﹃',
    '』': '﹄',
    '【': '︻',
    '】': '︼',
    '〖': '︗',
    '〗': '︘',
    '「': '﹁',
    '」': '﹂',
    ',': '︐',
    '、': '︑',
  };
}

pubspec.yaml

name: in_lane
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=2.19.4 <3.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  path:
  file_selector:
  file_picker: 5.3.0
  path_provider: ^2.0.7
  flutter_riverpod:
  shared_preferences:
  settings_ui : ^2.0.1
  universal_html: ^2.0.8
  flutter_colorpicker: ^1.0.3
  firebase_core:   #Firebase系は重い可能性があるので注意
  firebase_auth:   #Firebase系は重い可能性があるので注意
  cloud_firestore: #Firebase系は重い可能性があるので注意
  google_sign_in:
  uuid:

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

その他のファイルは変更が無いため、以前の日記を参考にしてください。


※本記事は当時の記録をもとに作成し、必要に応じて加筆・補足しています

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

コメント

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