2023/05/29
- ツールバーの実装に着手。
- 設定画面をコンテキストメニューに対応させた。
- スイッチ切り替えがなぜか動かなくて苦労したが、最終的にはSetStateが出来ていなかっただけだった……。
- ついでに画面の上部を少しスッキリさせた。
2023/05/30
- アイディアの文字スタイル指定(太字・斜体・下線・取り消し線)の実装。
- フォーカスしているアイディアの文字スタイルの状態と、ツールバーのアイコンの状態も連動させた。
- 文字スタイルの状態をファイル保存・読込にも対応させた。
- ツールバーの見た目も整えてそれっぽくなってきた。
2024/05/31
- テキストの色変更に対応。
この時点でのflutterソースファイル:main.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:math';
import 'dart:convert'; //JSON形式のデータを扱う
import 'package:path/path.dart' as path_dart; //asが無いとcontextを上書きされてしまう
import 'package:file_selector/file_selector.dart';
/*
class MyCustomScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch, // 通常のタッチ入力デバイス
PointerDeviceKind.mouse, // これを追加! https://qiita.com/Kurunp/items/5112b72e618002a93939
};
}
*/
void main() => 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 callTest() => WholeAppInheritedWidget.of(context).canvasZoneKey.currentState?.test();
void callSettingsScreen() async {
await setting_dart.SettingDialog(context);
setState(() {});
}
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 newCanvas() {
if (WholeAppInheritedWidget.of(context).canvasZoneKey.currentState == null) return;
WholeAppInheritedWidget.of(context).canvasZoneKey.currentState!.makeNewCanvas();
}
Future<void> loadData( XFile xFile ) async {
await loadDataFromFile( xFile:xFile, 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(() {}); //画面の再描画
}
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 loadData(xFile);
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 ?? false) != 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: [
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("テスト"), 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 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() {
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);
for(int h=horizontalLanes.length-1; h>=0; h--) {
removeLastHorizontalLane();
}
verticalLaneHeaders.clear();
horizontalLanes.clear();
horizontalLaneHeaderWidth = GlobalConst.horizontalLaneHeaderDefaultWidth;
ref.watch(ideasRelationList.notifier).state.clear();
focusedIdea = null; //コンテキストメニュー対策
}
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';
//リレーションの情報
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);
//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 = 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 ?? false) != 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';
}
Future<void> saveDataToFile({String? fileName, required CanvasZoneState? state}) async {
fileName ??= await defaultFileName;
if(state == null) return;
List<Map<String, dynamic>> verticalLaneHeadersJson = [];
for (var verticalLaneHeader in state.verticalLaneHeaders ) {
verticalLaneHeadersJson.add({
'index': verticalLaneHeader.index,
'verticalTitle': verticalLaneHeader.key.currentState?.titleController.text ?? '',
'verticalViewWidth': verticalLaneHeader.viewWidth,
});
}
List<Map<String, dynamic>> horizontalLanesJson = [];
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,
});
}
horizontalLanesJson.add({
'index': horizontalLane.index,
'horizontalTitle': horizontalLane.header.title,
'horizontalViewHeight': horizontalLane.viewHeight,
'verticalLanes': verticalLanesJson,
});
}
List<Map<String, dynamic>> ideasRelationJson = [];
state.ref.read(ideasRelationList).forEach((relationSet) {
ideasRelationJson.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,
});
});
String jsonString = jsonEncode({'verticalLaneHeaders': verticalLaneHeadersJson, 'horizontalLanes': horizontalLanesJson, 'ideasRelation': ideasRelationJson});
await ExportFile().exportFile(fileName, jsonString);
}
Future<void> loadDataFromFile({required XFile xFile, required CanvasZoneState? state}) async {
if(state == null) {
return;
}
String jsonString = await xFile.readAsString();
Map<String, dynamic> jsonMap = jsonDecode(jsonString);
printWhenDebug("clearCurrentData() Before", level: 2);
state.clearCurrentData();
printWhenDebug("clearCurrentData() After", level: 2);
List<VerticalLaneHeader> loadedVerticalLaneHeaders = [];
if (jsonMap['verticalLaneHeaders']!=null) {
for (var verticalLaneHeadersJson in jsonMap['verticalLaneHeaders']) {
var newLaneHeader = VerticalLaneHeader(index: verticalLaneHeadersJson['index'] ?? 0, viewWidth: verticalLaneHeadersJson['verticalViewWidth'] ?? state.ref.watch(defaultEachLaneWidth),);
newLaneHeader.title = verticalLaneHeadersJson['verticalTitle'] ?? '';
loadedVerticalLaneHeaders.add(newLaneHeader);
}
}
for (var horizontalLaneJson in jsonMap['horizontalLanes']) {
var newHorizontalLane = HorizontalLane(
horizontalLaneJson['index'] ?? 0,
horizontalLaneJson['horizontalTitle'] ?? ''
);
newHorizontalLane.viewHeight = horizontalLaneJson['horizontalViewHeight'] ?? GlobalConst.horizontalLaneDefaultViewHeight;
List<LaneBody> newVerticalLanes = [];
for (var verticalLaneJson in horizontalLaneJson['verticalLanes']) {
List<Idea> loadedIdeas = [];
var newLane = LaneBody(
indexX: verticalLaneJson['index'] ?? 0,
indexY: newHorizontalLane.index,
title: verticalLaneJson['verticalTitle'] ?? '',
ideas: loadedIdeas,
);
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();
}
loadedIdeas.add(newIdea);
}
newLane.updateRequiredHeight();
newVerticalLanes.add(newLane);
for (int i = 0; i < newVerticalLanes.length; i++) {
for (int j = 0; j < newVerticalLanes[i].ideas.length; j++) {
newVerticalLanes[i].ideas[j].content = jsonMap['horizontalLanes'][newHorizontalLane.index]['verticalLanes'][i]['ideas'][j]['content'];
}
}
}
newHorizontalLane.childVLaneBodies = newVerticalLanes;
state.horizontalLanes.add(newHorizontalLane);
}
state.verticalLaneHeaders = loadedVerticalLaneHeaders;
if (jsonMap['ideasRelation'] != null) {
for (var ideaRelationJson in jsonMap['ideasRelation']) {
var fromLaneIndexY = ideaRelationJson['from_lane_indexY'];
var fromLaneIndexX = ideaRelationJson['from_lane_indexX'];
var fromIdeaIndex = ideaRelationJson['from_idea_index'];
var toLaneIndexY = ideaRelationJson['to_lane_indexY'];
var toLaneIndexX = ideaRelationJson['to_lane_indexX'];
var toIdeaIndex = ideaRelationJson['to_idea_index'];
IdeasRelation relationSet = IdeasRelation(
fromIdea: state.horizontalLanes[fromLaneIndexY].childVLaneBodies[fromLaneIndexX].ideas[fromIdeaIndex], toIdea: state.horizontalLanes[toLaneIndexY].childVLaneBodies[toLaneIndexX].ideas[toIdeaIndex]);
if(ideaRelationJson['from_shape'] == 'arrow') {
relationSet.fromShape = RelationShape.arrow;
}
if(ideaRelationJson['to_shape'] == 'arrow') {
relationSet.toShape = RelationShape.arrow;
}
relationSet.note = ideaRelationJson['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]);
}
}
}
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は重い可能性があるので注意
# 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
その他のファイルは変更が無いため、以前の日記を参考にしてください。
※本記事は当時の記録をもとに作成し、必要に応じて加筆・補足しています
テキストベースの思考整理ツール「アイディア・レーン」最新版はこちら。
コメント