実装開始 2023/03/17
まずは文字を縦に並べる。
そして階層化出来るようにしてみた。
といってもいきなりこんなにコーディング出来たわけでなく、リリースされたばかりのChatGPTにコードを生成してもらったものだ。
正直あまりflutterもdartも分からない状態で、AIが作ってくれたコードを元になんとか動かしたというところ。
しかしChatGPTが生成するコードを動かそうとすると、ほぼ必ずエラーが出てビルドに失敗する。でもそのエラーを見ても何のことか全くわからないので、ネットでエラーメッセージを調べて適当に直すという感じだった。
今振り返ると、大抵はflutterの仕様変更のせいで、nullチェックやmaterial関連の記述変更が必要というものが多かったです。
今なら null チェックが必要ならこうすれば良いとすぐ分かるのですが……。
当時は大量に出るエラーを一つずつ理解するには時間がかかりすぎたので、とにかくネットの情報を見ながら適当に直していました。
コードはこんな感じ。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:math';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SafeArea(child: Lane()),
),
);
}
}
class Lane extends StatefulWidget {
@override
_LaneState createState() => _LaneState();
}
class _LaneState extends State<Lane> {
List<Idea> ideas = [Idea(key: GlobalKey<_IdeaState>(), index: 0)];
void addNewIdea(int currentIndex) {
setState(() {
ideas.insert(currentIndex + 1,
Idea(key: GlobalKey<_IdeaState>(), index: currentIndex + 1));
for (int i = currentIndex + 2; i < ideas.length; i++) {
ideas[i].index = i;
}
});
}
void moveIdea(int currentIndex, int direction) {
if (currentIndex + direction >= 0 &&
currentIndex + direction < ideas.length) {
setState(() {
Idea temp = ideas[currentIndex];
ideas[currentIndex] = ideas[currentIndex + direction];
ideas[currentIndex + direction] = temp;
ideas[currentIndex].key.currentState?.updateIndex(currentIndex);
ideas[currentIndex + direction]
.key
.currentState
?.updateIndex(currentIndex + direction);
});
}
}
//Ideaが削除された後に、一つ上のIdeaにフォーカスが移動するようになっています。もし削除されたIdeaが最初のIdeaだった場合、その下のIdeaにフォーカスが移動します
void deleteIdea(int currentIndex) {
if (ideas.length > 1) {
setState(() {
ideas.removeAt(currentIndex);
for (int i = currentIndex; i < ideas.length; i++) {
ideas[i].index = i;
}
if (currentIndex > 0) {
ideas[currentIndex - 1]
.key
.currentState
?._textFocusNode
.requestFocus();
} else {
ideas[currentIndex].key.currentState?._textFocusNode.requestFocus();
}
});
}
}
void focusAdjacentIdea(int currentIndex, int direction) {
if (currentIndex + direction >= 0 &&
currentIndex + direction < ideas.length) {
ideas[currentIndex + direction]
.key
.currentState
?._textFocusNode
.requestFocus();
}
}
@override
Widget build(BuildContext context) {
return LaneInheritedWidget(
laneState: this,
child: ListView.builder(
itemCount: ideas.length,
itemBuilder: (context, index) {
return ideas[index];
},
),
);
}
}
class LaneInheritedWidget extends InheritedWidget {
final _LaneState laneState;
LaneInheritedWidget({required this.laneState, required Widget child})
: super(child: child);
@override
bool updateShouldNotify(LaneInheritedWidget old) => true;
static _LaneState of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<LaneInheritedWidget>()!
.laneState;
}
}
class Idea extends StatefulWidget {
int index;
final GlobalKey<_IdeaState> key;
Idea({required this.key, required this.index}) : super(key: key);
@override
_IdeaState createState() => _IdeaState();
}
class _IdeaState extends State<Idea> {
TextEditingController _textController = TextEditingController();
int _indentLevel = 0;
FocusNode _textFocusNode = FocusNode();
void updateIndex(int newIndex) {
setState(() {
widget.index = newIndex;
});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) {
FocusScope.of(context).requestFocus(_textFocusNode);
});
}
@override
Widget build(BuildContext context) {
return Focus(
onKey: (FocusNode node, RawKeyEvent event) {
if (event.runtimeType == RawKeyDownEvent) {
if (event.isControlPressed) { //コントロールキー
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
LaneInheritedWidget.of(context).moveIdea(widget.index, -1);
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
LaneInheritedWidget.of(context).moveIdea(widget.index, 1);
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
LaneInheritedWidget.of(context).focusAdjacentIdea(widget.index, -1);
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
LaneInheritedWidget.of(context).focusAdjacentIdea(widget.index, 1);
} else if (event.logicalKey == LogicalKeyboardKey.tab) { //TABキー
if (event.isShiftPressed) {
setState(() {
_indentLevel = max(0, _indentLevel - 1);
});
} else {
setState(() {
_indentLevel += 1;
});
}
return KeyEventResult.handled;; //デフォルトのタブの挙動を無効化して、フォーカスの移動を防ぐ
} else if ((event.logicalKey == LogicalKeyboardKey.delete ||
event.logicalKey == LogicalKeyboardKey.backspace) &&
_textController.text.isEmpty) {
LaneInheritedWidget.of(context).deleteIdea(widget.index);
}
}
return KeyEventResult.ignored;
},
child: RawKeyboardListener(
focusNode: FocusNode(),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 1,
child: Container(
color: Colors.grey[300],
child: CustomPaint(
painter: HeaderCanvas(),
),
),
),
SizedBox(width: 16),
Expanded(
flex: 4,
child: Container(
padding: EdgeInsets.only(left: 20.0 * _indentLevel),
child: TextField(
controller: _textController,
focusNode: _textFocusNode,
maxLines: 1,
decoration: InputDecoration(
hintText: 'Enter your idea',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.done,
onSubmitted: (value) {
_textFocusNode.unfocus(); // Unfocus the current TextField
LaneInheritedWidget.of(context).addNewIdea(widget.index);
},
),
),
),
],
),
),
)
);
}
}
class HeaderCanvas extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Your custom painting logic goes here
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
アプリのタイトルは、レーンの中に色々書けるものを目指したので in Lane とした。(当時)
※本記事は、過去の記録(メモ)をもとに、他の方が読んで分かるように加筆・補足して掲載しています
テキストベースの思考整理ツール「アイディア・レーン」最新版はこちら。
コメント