import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../models/note_plus_models.dart'; import '../../providers/note_plus_provider.dart'; import 'note_plus_block_widget.dart'; import 'slash_command_menu.dart'; /// Note Plus 块编辑器 /// /// 管理 block 列表、焦点切换、键盘事件处理。 /// 使用 ReorderableListView 支持拖拽排序。 class NotePlusEditor extends StatefulWidget { const NotePlusEditor({super.key}); @override State createState() => _NotePlusEditorState(); } class _NotePlusEditorState extends State { final ScrollController _scrollController = ScrollController(); final Map _controllers = {}; final Map _focusNodes = {}; OverlayEntry? _slashMenuOverlay; @override void dispose() { _scrollController.dispose(); for (final c in _controllers.values) { c.dispose(); } for (final fn in _focusNodes.values) { fn.dispose(); } _removeSlashMenu(); super.dispose(); } TextEditingController _getController(NoteBlock block) { return _controllers.putIfAbsent(block.id, () { final c = TextEditingController(text: block.text); c.addListener(() => _onControllerChanged(block)); return c; }); } FocusNode _getFocusNode(NoteBlock block, int index) { return _focusNodes.putIfAbsent(block.id, () { final fn = FocusNode(onKeyEvent: (node, event) => _handleKeyEvent(index, event)); fn.addListener(() { if (fn.hasFocus) { context.read().setFocusedBlock(index); } }); return fn; }); } /// 清理已删除 block 对应的控制器和焦点节点 void _cleanupStaleEntries(List currentBlocks) { final currentIds = currentBlocks.map((b) => b.id).toSet(); final staleIds = _controllers.keys.where((id) => !currentIds.contains(id)).toList(); for (final id in staleIds) { _controllers.remove(id)?.dispose(); } final staleFocusIds = _focusNodes.keys.where((id) => !currentIds.contains(id)).toList(); for (final id in staleFocusIds) { _focusNodes.remove(id)?.dispose(); } } void _onControllerChanged(NoteBlock block) { final provider = context.read(); final idx = provider.blocks.indexWhere((b) => b.id == block.id); if (idx < 0) return; final text = _controllers[block.id]!.text; // 更新 block 文本(静默,不 rebuild) provider.updateBlockSilent( idx, provider.blocks[idx].copyWith(text: text), ); // 检测斜杠命令 if (text == '/' || (text.startsWith('/') && text.length > 1)) { _showSlashMenu(idx, text); } else { _removeSlashMenu(); } } void _onBlockTap(int index) { final provider = context.read(); provider.setFocusedBlock(index); final block = provider.blocks[index]; if (block.type == NoteBlockType.divider) return; final node = _getFocusNode(block, index); if (!node.hasFocus) { node.requestFocus(); } } void _onToggleChecklist(int index) { context.read().toggleChecklist(index); } /// 处理键盘事件 KeyEventResult _handleKeyEvent(int index, KeyEvent event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; final provider = context.read(); final block = provider.blocks[index]; final controller = _controllers[block.id]; if (controller == null) return KeyEventResult.ignored; final sel = controller.selection; // Enter → 分割 block if (event.logicalKey == LogicalKeyboardKey.enter && !HardwareKeyboard.instance.isShiftPressed) { final pos = sel.isValid ? sel.baseOffset : controller.text.length; provider.splitBlock(index, pos); // 聚焦新 block WidgetsBinding.instance.addPostFrameCallback((_) { _focusBlockAtIndex(index + 1, atStart: true); }); return KeyEventResult.handled; } // Backspace 在位置0 → 合并到前一个 if (event.logicalKey == LogicalKeyboardKey.backspace && sel.isValid && sel.isCollapsed && sel.baseOffset == 0 && index > 0) { final cursorPos = provider.mergeWithPrevious(index); WidgetsBinding.instance.addPostFrameCallback((_) { _focusBlockAtIndex(index - 1, atCursor: cursorPos); }); return KeyEventResult.handled; } // ↑ 在位置0 → 聚焦上一个 if (event.logicalKey == LogicalKeyboardKey.arrowUp && sel.isValid && sel.isCollapsed && sel.baseOffset == 0 && index > 0) { _focusBlockAtIndex(index - 1, atEnd: true); return KeyEventResult.handled; } // ↓ 在末位 → 聚焦下一个 if (event.logicalKey == LogicalKeyboardKey.arrowDown && sel.isValid && sel.isCollapsed && sel.baseOffset == controller.text.length && index < provider.blocks.length - 1) { _focusBlockAtIndex(index + 1, atStart: true); return KeyEventResult.handled; } return KeyEventResult.ignored; } void _focusBlockAtIndex(int index, {bool atStart = false, bool atEnd = false, int? atCursor}) { final provider = context.read(); if (index < 0 || index >= provider.blocks.length) return; final block = provider.blocks[index]; if (block.type == NoteBlockType.divider) return; provider.setFocusedBlock(index); final node = _getFocusNode(block, index); final controller = _getController(block); // 等 controller 同步后设置光标 WidgetsBinding.instance.addPostFrameCallback((_) { if (!node.hasFocus) node.requestFocus(); int cursorPos; if (atCursor != null) { cursorPos = atCursor; } else if (atEnd) { cursorPos = controller.text.length; } else { cursorPos = 0; } controller.selection = TextSelection.collapsed(offset: cursorPos); }); } // ========== Slash Menu ========== void _showSlashMenu(int blockIndex, String text) { _removeSlashMenu(); final query = text.length > 1 ? text.substring(1) : ''; _slashMenuOverlay = OverlayEntry( builder: (context) => Positioned( bottom: MediaQuery.of(context).viewInsets.bottom + 60, left: 16, right: 16, child: Material( elevation: 8, borderRadius: BorderRadius.circular(12), child: SlashCommandMenu( query: query, onSelect: (type) { _removeSlashMenu(); final provider = context.read(); // 清除 / 文本 provider.updateBlockSilent( blockIndex, provider.blocks[blockIndex].copyWith(text: ''), ); _controllers[provider.blocks[blockIndex].id]?.text = ''; // 转换类型 provider.convertBlockType(blockIndex, type); }, onDismiss: _removeSlashMenu, ), ), ), ); Overlay.of(context).insert(_slashMenuOverlay!); } void _removeSlashMenu() { _slashMenuOverlay?.remove(); _slashMenuOverlay = null; } // ========== Build ========== @override Widget build(BuildContext context) { return Consumer( builder: (context, provider, _) { final blocks = provider.blocks; // 清理已删除 block 对应的控制器和焦点节点,防止内存泄漏 _cleanupStaleEntries(blocks); return ListView.builder( controller: _scrollController, itemCount: blocks.length, itemBuilder: (context, index) { final block = blocks[index]; final isFocused = provider.focusedBlockIndex == index; // 计算有序列表序号 int numberedIndex = 1; if (block.type == NoteBlockType.numberedList) { for (int i = index - 1; i >= 0; i--) { if (blocks[i].type == NoteBlockType.numberedList) { numberedIndex++; } else { break; } } } // 同步 controller 文本 final controller = isFocused ? _getController(block) : null; if (controller != null && controller.text != block.text) { // 用 addPostFrameCallback 避免 build 期间修改 WidgetsBinding.instance.addPostFrameCallback((_) { if (controller.text != block.text) { controller.text = block.text; } }); } return Padding( key: ValueKey(block.id), padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Block 内容 Expanded( child: NotePlusBlockWidget( block: block, index: index, numberedIndex: numberedIndex, isFocused: isFocused, controller: controller, focusNode: isFocused ? _getFocusNode(block, index) : null, onTap: () => _onBlockTap(index), onToggleChecklist: block.type == NoteBlockType.checklist ? () => _onToggleChecklist(index) : null, ), ), ], ), ); }, ); }, ); } }