diff --git a/win_text_editor/lib/app/components/text_editor.dart b/win_text_editor/lib/app/components/text_editor.dart index a456803..04b60b3 100644 --- a/win_text_editor/lib/app/components/text_editor.dart +++ b/win_text_editor/lib/app/components/text_editor.dart @@ -1,9 +1,8 @@ -import 'dart:convert'; -import 'dart:io'; import 'dart:ui'; + import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:file_picker/file_picker.dart'; +import 'text_editor_controller.dart'; +import 'text_editor_actions.dart'; class TextEditor extends StatefulWidget { final String tabId; @@ -28,301 +27,128 @@ class TextEditor extends StatefulWidget { } class TextEditorState extends State { - late TextEditingController _controller; - - late ScrollController _scrollController; - bool _isLoading = false; - static const int maxFileSize = 1024 * 1024; // 1MB - - bool get hasFocus => _focusNode.hasFocus; - FocusNode get _focusNode => focusNode; // 将原来的_focusNode改为focusNode - late FocusNode focusNode = FocusNode(); // 修改声明方式 - - void loadFile(BuildContext context, String filePath) async { - await _loadFile(context, filePath); - } + late TextEditorController _editorController; @override void initState() { super.initState(); - _controller = TextEditingController(text: widget.initialContent ?? ''); - focusNode = FocusNode(); - _scrollController = ScrollController(); - WidgetsBinding.instance.addPostFrameCallback((_) { - FocusScope.of(context).requestFocus(focusNode); - }); + _editorController = TextEditorController( + initialContent: widget.initialContent, + onContentChanged: (content, fileName) { + widget.onContentChanged?.call(content, fileName); + }, + onFileLoaded: widget.onFileLoaded, + ); } @override void didUpdateWidget(TextEditor oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.initialContent != widget.initialContent) { - _controller.text = widget.initialContent ?? ''; + _editorController.updateContent(widget.initialContent ?? ''); } } @override void dispose() { - _controller.dispose(); - _focusNode.dispose(); - _scrollController.dispose(); + _editorController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return Column( + return Column(children: [_buildToolbar(context), Expanded(child: _buildEditorField(context))]); + } + + Widget _buildToolbar(BuildContext context) { + return Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration(color: Colors.grey[100]), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${widget.title}${_editorController.isEmpty ? '' : ' (${widget.fileName ?? ''}${_editorController.contentLength}字符)'}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + _buildActionButtons(context), + ], + ), + ); + } + + Widget _buildActionButtons(BuildContext context) { + return Row( children: [ - Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: hasFocus ? Colors.blue[50] : Colors.grey[100], - border: Border( - bottom: BorderSide( - color: hasFocus ? Colors.blue : Colors.grey[300]!, - width: hasFocus ? 2.0 : 1.0, - ), + IconButton( + icon: const Icon(Icons.folder_open, size: 20), + tooltip: '打开文件', + onPressed: + _editorController.isLoading + ? null + : () => TextEditorActions.openFile(context, _editorController), + ), + IconButton( + icon: const Icon(Icons.content_copy, size: 20), + tooltip: '复制内容', + onPressed: + _editorController.isEmpty + ? null + : () => TextEditorActions.copyToClipboard(context, _editorController.content), + ), + IconButton( + icon: const Icon(Icons.save, size: 20), + tooltip: '保存到文件', + onPressed: + _editorController.isEmpty + ? null + : () => TextEditorActions.saveFile(context, _editorController.content), + ), + if (_editorController.isLoading) + const Padding( + padding: EdgeInsets.only(left: 8), + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${widget.title}${_controller.text.isEmpty ? '' : ' (${widget.fileName ?? ''}${_controller.text.length}字符)'}', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Row( - children: [ - IconButton( - icon: const Icon(Icons.folder_open, size: 20), - tooltip: '打开文件', - onPressed: _isLoading ? null : () => _openFile(context), - ), - IconButton( - icon: const Icon(Icons.content_copy, size: 20), - tooltip: '复制内容', - onPressed: - _controller.text.isEmpty - ? null - : () => _copyToClipboard(context, _controller.text), - ), - IconButton( - icon: const Icon(Icons.save, size: 20), - tooltip: '保存到文件', - onPressed: - _controller.text.isEmpty - ? null - : () => _saveFile(context, _controller.text), - ), - if (_isLoading) - const Padding( - padding: EdgeInsets.only(left: 8), - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), - ], - ), - ], - ), + ], + ); + } + + Widget _buildEditorField(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: _editorController.hasFocus ? Colors.blue[50] : Colors.white, + border: Border.all( + color: _editorController.hasFocus ? Colors.blue : Colors.grey[300]!, + width: _editorController.hasFocus ? 2.0 : 1.0, ), - Expanded( - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - scrollbars: true, - dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, + ), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: true, + dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, + ), + child: SingleChildScrollView( + controller: _editorController.scrollController, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - 40, // 减去工具栏高度 ), - child: SingleChildScrollView( - controller: _scrollController, - child: Stack( - children: [ - TextField( - controller: _controller, - focusNode: _focusNode, - maxLines: null, - onChanged: (text) { - widget.onContentChanged?.call(text, widget.fileName); - }, - decoration: const InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.all(16), - ), - style: const TextStyle( - fontFamily: 'Courier New', - fontSize: 16, - color: Colors.black, - ), - ), - ], - ), + child: TextField( + controller: _editorController.textController, + focusNode: _editorController.focusNode, + maxLines: null, + onChanged: _editorController.handleContentChanged, + decoration: InputDecoration.collapsed(hintText: ''), + style: const TextStyle(fontFamily: 'Courier New', fontSize: 16, color: Colors.black), ), ), ), - ], + ), ); } - - Future _openFile(BuildContext context) async { - final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); - if (result != null && result.files.single.path != null) { - await _loadFile(context, result.files.single.path!); - } - } - - Future _copyToClipboard(BuildContext context, String content) async { - await Clipboard.setData(ClipboardData(text: content)); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); - } - } - - Future _saveFile(BuildContext context, String content) async { - try { - String? outputPath = await FilePicker.platform.saveFile( - dialogTitle: '保存文件', - fileName: 'untitled.txt', - allowedExtensions: ['txt'], - type: FileType.any, - ); - - if (outputPath == null) return; - - final file = File(outputPath); - - if (await file.exists()) { - final shouldOverwrite = await showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('文件已存在'), - content: const Text('要覆盖现有文件吗?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('取消'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('覆盖'), - ), - ], - ), - ); - - if (shouldOverwrite != true) return; - } - - await file.writeAsString(content); - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已保存到: ${file.path}'))); - } - } on FileSystemException catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.message}'))); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'))); - } - } - } - - Future _loadFile(BuildContext context, String filePath) async { - try { - setState(() => _isLoading = true); - final file = File(filePath); - final fileSize = await file.length(); - - // 检查文件大小 - if (fileSize > maxFileSize) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); - } - return; - } - - // 判断当前是否有内容 - if (_controller.text.isNotEmpty) { - final confirm = await showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('确认提示'), - content: const Text('确认是否打开新的文件?打开后将覆盖当前内容'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('取消'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('确认'), - ), - ], - ), - ); - - if (confirm != true) { - return; - } - } - - final fileName = file.path.split('\\').last; - - // 清空当前内容 - _controller.text = ''; - widget.onContentChanged?.call('', fileName); - - // 逐行读取文件 - final stream = file.openRead(); - final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); - - await for (final line in lines) { - if (!mounted) break; - - setState(() { - _controller.text += '$line\n'; - widget.onContentChanged?.call(_controller.text, fileName); - }); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - }); - - await Future.delayed(const Duration(milliseconds: 10)); - } - - widget.onFileLoaded?.call(file.path); - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); - } - } on FormatException { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); - } - } on FileSystemException catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); - } - } finally { - if (mounted) { - setState(() => _isLoading = false); - } - } - } } diff --git a/win_text_editor/lib/app/components/text_editor_actions.dart b/win_text_editor/lib/app/components/text_editor_actions.dart new file mode 100644 index 0000000..456b646 --- /dev/null +++ b/win_text_editor/lib/app/components/text_editor_actions.dart @@ -0,0 +1,163 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:file_picker/file_picker.dart'; +import 'text_editor_controller.dart'; + +class TextEditorActions { + static Future openFile(BuildContext context, TextEditorController controller) async { + final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); + + if (result != null && result.files.single.path != null) { + await _loadFile(context, controller, result.files.single.path!); + } + } + + static Future copyToClipboard(BuildContext context, String content) async { + await Clipboard.setData(ClipboardData(text: content)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); + } + } + + static Future saveFile(BuildContext context, String content) async { + try { + String? outputPath = await FilePicker.platform.saveFile( + dialogTitle: '保存文件', + fileName: 'untitled.txt', + allowedExtensions: ['txt'], + type: FileType.any, + ); + + if (outputPath == null) return; + + final file = File(outputPath); + + if (await file.exists()) { + final shouldOverwrite = await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('文件已存在'), + content: const Text('要覆盖现有文件吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('覆盖'), + ), + ], + ), + ); + + if (shouldOverwrite != true) return; + } + + await file.writeAsString(content); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已保存到: ${file.path}'))); + } + } on FileSystemException catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.message}'))); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'))); + } + } + } + + static Future _loadFile( + BuildContext context, + TextEditorController controller, + String filePath, + ) async { + try { + controller.setLoading(true); + final file = File(filePath); + final fileSize = await file.length(); + + if (fileSize > controller.maxFileSize) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); + } + return; + } + + if (!controller.isEmpty) { + final confirm = await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('确认提示'), + content: const Text('确认是否打开新的文件?打开后将覆盖当前内容'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('确认'), + ), + ], + ), + ); + + if (confirm != true) return; + } + + final fileName = file.path.split('\\').last; + controller.updateContent(''); + controller.onContentChanged?.call('', fileName); + + final stream = file.openRead(); + final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); + + await for (final line in lines) { + if (!controller.textController.hasListeners) break; + + controller.textController.text += '$line\n'; + controller.onContentChanged?.call(controller.content, fileName); + + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.scrollController.jumpTo(controller.scrollController.position.maxScrollExtent); + }); + + await Future.delayed(const Duration(milliseconds: 10)); + } + + controller.onFileLoaded?.call(file.path); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); + } + } on FormatException { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); + } + } on FileSystemException catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); + } + } finally { + controller.setLoading(false); + } + } +} diff --git a/win_text_editor/lib/app/components/text_editor_controller.dart b/win_text_editor/lib/app/components/text_editor_controller.dart new file mode 100644 index 0000000..03f4db1 --- /dev/null +++ b/win_text_editor/lib/app/components/text_editor_controller.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class TextEditorController { + final TextEditingController textController; + final FocusNode focusNode; + final ScrollController scrollController; + final Function(String, String?)? onContentChanged; + final Function(String)? onFileLoaded; + + bool _isLoading = false; + static const int _maxFileSize = 1024 * 1024; // 1MB + + TextEditorController({String? initialContent, this.onContentChanged, this.onFileLoaded}) + : textController = TextEditingController(text: initialContent ?? ''), + focusNode = FocusNode(), + scrollController = ScrollController(); + + bool get isLoading => _isLoading; + bool get hasFocus => focusNode.hasFocus; + bool get isEmpty => textController.text.isEmpty; + String get content => textController.text; + int get contentLength => textController.text.length; + int get maxFileSize => _maxFileSize; + + void updateContent(String content) { + textController.text = content; + } + + void handleContentChanged(String text) { + onContentChanged?.call(text, null); + } + + void setLoading(bool loading) { + _isLoading = loading; + } + + void dispose() { + textController.dispose(); + focusNode.dispose(); + scrollController.dispose(); + } +} diff --git a/win_text_editor/lib/app/core/app_scaffold.dart b/win_text_editor/lib/app/core/app_scaffold.dart index 5180abc..3620ac3 100644 --- a/win_text_editor/lib/app/core/app_scaffold.dart +++ b/win_text_editor/lib/app/core/app_scaffold.dart @@ -18,6 +18,7 @@ class AppScaffold extends StatelessWidget { ChangeNotifierProvider(create: (_) => TabManager()), // 改名为TabManager ], child: Scaffold( + backgroundColor: Colors.grey[100], body: Column( children: [ const AppMenu(), // 顶部菜单 diff --git a/win_text_editor/lib/app/core/tab_view.dart b/win_text_editor/lib/app/core/tab_view.dart index 1c21c92..670c5af 100644 --- a/win_text_editor/lib/app/core/tab_view.dart +++ b/win_text_editor/lib/app/core/tab_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:win_text_editor/app/components/text_editor.dart'; import 'package:win_text_editor/app/core/tab_manager.dart'; import 'package:win_text_editor/app/models/tab_model.dart'; +import 'package:win_text_editor/app/modules/content_search/content_search_view.dart'; import 'package:win_text_editor/app/modules/template_parser/template_parser_view.dart'; import 'package:provider/provider.dart'; @@ -58,7 +59,7 @@ class TabView extends StatelessWidget { return TemplateParserView(tabId: tab.id); // 使用选项卡的ID而不是新生成 case 'content_search': // return const ContentSearchView(); - return Container(); // 临时占位 + return ContentSearchView(tabId: tab.id); // 临时占位 default: return TextEditor(tabId: tab.id, initialContent: tab.content); } diff --git a/win_text_editor/lib/app/modules/content_search/content_search_controller.dart b/win_text_editor/lib/app/modules/content_search/content_search_controller.dart new file mode 100644 index 0000000..95447ee --- /dev/null +++ b/win_text_editor/lib/app/modules/content_search/content_search_controller.dart @@ -0,0 +1,67 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:win_text_editor/app/core/tab_manager.dart'; + +class ContentSearchController { + final TabManager tabManager; + + String searchQuery = ''; + String searchDirectory = ''; + String fileType = '*.*'; + bool caseSensitive = false; + bool wholeWord = false; + bool useRegex = false; + SearchMode searchMode = SearchMode.locate; + final List results = []; + + ContentSearchController({required this.tabManager}); + + Future startSearch() async { + results.clear(); + // 模拟搜索结果 + results.addAll([ + SearchResult( + filePath: 'lib/main.dart', + lineNumber: 42, + lineContent: 'void main() => runApp(MyApp());', + matches: const [MatchResult(start: 5, end: 9)], + ), + SearchResult( + filePath: 'lib/home_page.dart', + lineNumber: 17, + lineContent: 'class HomePage extends StatelessWidget {', + matches: const [MatchResult(start: 6, end: 10)], + ), + ]); + } + + Future pickDirectory() async { + final dir = await FilePicker.platform.getDirectoryPath(); + if (dir != null) { + searchDirectory = dir; + } + } +} + +enum SearchMode { locate, count } + +class SearchResult { + final String filePath; + final int lineNumber; + final String lineContent; + final List matches; + + SearchResult({ + required this.filePath, + required this.lineNumber, + required this.lineContent, + required this.matches, + }); +} + +class MatchResult { + final int start; + final int end; + + const MatchResult({required this.start, required this.end}); +} diff --git a/win_text_editor/lib/app/modules/content_search/content_search_view.dart b/win_text_editor/lib/app/modules/content_search/content_search_view.dart new file mode 100644 index 0000000..b15eef7 --- /dev/null +++ b/win_text_editor/lib/app/modules/content_search/content_search_view.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:win_text_editor/app/core/tab_manager.dart'; +import 'content_search_controller.dart'; +import 'directory_settings.dart'; +import 'search_settings.dart'; +import 'results_view.dart'; + +class ContentSearchView extends StatefulWidget { + final String tabId; + + const ContentSearchView({super.key, required this.tabId}); + + @override + State createState() => ContentSearchViewState(); +} + +class ContentSearchViewState extends State { + late final ContentSearchController _controller; + + @override + void initState() { + super.initState(); + _controller = ContentSearchController( + tabManager: Provider.of(context, listen: false), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + DirectorySettings(controller: _controller), + const SizedBox(height: 16), + SearchSettings(controller: _controller), + const SizedBox(height: 16), + Expanded(child: ResultsView(controller: _controller)), + ], + ), + ); + } +} diff --git a/win_text_editor/lib/app/modules/content_search/directory_settings.dart b/win_text_editor/lib/app/modules/content_search/directory_settings.dart new file mode 100644 index 0000000..9cebb33 --- /dev/null +++ b/win_text_editor/lib/app/modules/content_search/directory_settings.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; + +class DirectorySettings extends StatelessWidget { + final ContentSearchController controller; + + const DirectorySettings({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const Icon(Icons.folder, color: Colors.blue), + const SizedBox(width: 8), + Expanded( + child: TextField( + decoration: const InputDecoration(labelText: '搜索目录', border: OutlineInputBorder()), + onChanged: (value) => controller.searchDirectory = value, + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 100, + child: TextField( + decoration: const InputDecoration( + labelText: '文件类型', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 12), + ), + controller: TextEditingController(text: controller.fileType), + onChanged: (value) => controller.fileType = value, + ), + ), + const SizedBox(width: 8), + IconButton(icon: const Icon(Icons.folder_open), onPressed: controller.pickDirectory), + ], + ), + ), + ); + } +} diff --git a/win_text_editor/lib/app/modules/content_search/results_view.dart b/win_text_editor/lib/app/modules/content_search/results_view.dart new file mode 100644 index 0000000..ab691b2 --- /dev/null +++ b/win_text_editor/lib/app/modules/content_search/results_view.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; + +class ResultsView extends StatelessWidget { + final ContentSearchController controller; + + const ResultsView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + controller.searchMode == SearchMode.locate ? '定位结果' : '计数结果', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Expanded( + child: + controller.searchMode == SearchMode.locate + ? _buildLocateResults() + : _buildCountResults(), + ), + ], + ), + ); + } + + Widget _buildLocateResults() { + return ListView.builder( + itemCount: controller.results.length, + itemBuilder: (ctx, index) { + final result = controller.results[index]; + return ExpansionTile( + title: Text('${result.filePath}:${result.lineNumber}'), + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text.rich(_buildHighlightedText(result.lineContent, result.matches)), + ), + ], + ); + }, + ); + } + + Widget _buildCountResults() { + final counts = {}; + for (var r in controller.results) { + counts[r.filePath] = (counts[r.filePath] ?? 0) + r.matches.length; + } + + return ListView.builder( + itemCount: counts.length, + itemBuilder: (ctx, index) { + final entry = counts.entries.elementAt(index); + return ListTile( + leading: const Icon(Icons.insert_drive_file), + title: Text(entry.key), + trailing: Chip(label: Text('${entry.value}处')), + ); + }, + ); + } + + TextSpan _buildHighlightedText(String text, List matches) { + final spans = []; + int lastEnd = 0; + + for (final match in matches) { + if (match.start > lastEnd) { + spans.add(TextSpan(text: text.substring(lastEnd, match.start))); + } + spans.add( + TextSpan( + text: text.substring(match.start, match.end), + style: const TextStyle(backgroundColor: Colors.yellow, fontWeight: FontWeight.bold), + ), + ); + lastEnd = match.end; + } + if (lastEnd < text.length) { + spans.add(TextSpan(text: text.substring(lastEnd))); + } + + return TextSpan(children: spans); + } +} diff --git a/win_text_editor/lib/app/modules/content_search/search_settings.dart b/win_text_editor/lib/app/modules/content_search/search_settings.dart new file mode 100644 index 0000000..a8c3971 --- /dev/null +++ b/win_text_editor/lib/app/modules/content_search/search_settings.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:win_text_editor/app/components/text_editor.dart'; +import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; + +class SearchSettings extends StatelessWidget { + final ContentSearchController controller; + final GlobalKey _searchEditorKey = GlobalKey(); + + SearchSettings({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 搜索内容框 + SizedBox( + width: MediaQuery.of(context).size.width * 0.5, // 占据一半宽度 + height: 300, + child: TextEditor( + key: _searchEditorKey, + tabId: 'search_content', + title: '搜索内容', + onContentChanged: (content, _) => controller.searchQuery = content, + ), + ), + const SizedBox(width: 8), + // 设置按钮区域 + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), // 设置边框颜色 + borderRadius: BorderRadius.circular(4), // 设置边框圆角 + ), + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 搜索方式 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('搜索方式:', style: TextStyle(fontSize: 12)), + Row( + children: [ + Radio( + value: SearchMode.locate, + groupValue: controller.searchMode, + onChanged: (value) => controller.searchMode = value!, + ), + const Text('定位', style: TextStyle(fontSize: 12)), + ], + ), + Row( + children: [ + Radio( + value: SearchMode.count, + groupValue: controller.searchMode, + onChanged: (value) => controller.searchMode = value!, + ), + const Text('计数', style: TextStyle(fontSize: 12)), + ], + ), + ], + ), + const SizedBox(height: 8), + // 匹配规则 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('匹配规则:', style: TextStyle(fontSize: 12)), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + title: const Text('大小写敏感', style: TextStyle(fontSize: 12)), + value: controller.caseSensitive, + onChanged: (value) => controller.caseSensitive = value!, + ), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + title: const Text('全字匹配', style: TextStyle(fontSize: 12)), + value: controller.wholeWord, + onChanged: (value) => controller.wholeWord = value!, + ), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + title: const Text('正则匹配', style: TextStyle(fontSize: 12)), + value: controller.useRegex, + onChanged: (value) => controller.useRegex = value!, + ), + ], + ), + const SizedBox(height: 8), + // 开始搜索按钮 + Align( + alignment: Alignment.centerLeft, + child: ElevatedButton.icon( + icon: const Icon(Icons.search, size: 20), + label: const Text('开始搜索'), + onPressed: controller.startSearch, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart b/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart index 56eee18..780b369 100644 --- a/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart +++ b/win_text_editor/lib/app/modules/template_parser/template_parser_view.dart @@ -23,19 +23,7 @@ class TemplateParserViewState extends State { final GlobalKey _editor2Key = GlobalKey(); // 添加焦点监听 - void _setupFocusListeners() { - _editor1Key.currentState?.focusNode.addListener(() { - if (_editor1Key.currentState?.hasFocus ?? false) { - setState(() => _activeEditorIndex = 0); - } - }); - - _editor2Key.currentState?.focusNode.addListener(() { - if (_editor2Key.currentState?.hasFocus ?? false) { - setState(() => _activeEditorIndex = 1); - } - }); - } + void _setupFocusListeners() {} @override void initState() { @@ -55,13 +43,7 @@ class TemplateParserViewState extends State { } // 修改loadFile方法使用_activeEditorIndex - Future loadFile(BuildContext context, String filePath) async { - if (_activeEditorIndex == 0) { - _editor1Key.currentState?.loadFile(context, filePath); - } else { - _editor2Key.currentState?.loadFile(context, filePath); - } - } + Future loadFile(BuildContext context, String filePath) async {} @override Widget build(BuildContext context) { diff --git a/win_text_editor/lib/main.dart b/win_text_editor/lib/main.dart index 570b305..d2b0584 100644 --- a/win_text_editor/lib/main.dart +++ b/win_text_editor/lib/main.dart @@ -10,7 +10,7 @@ void main() async { // 配置窗口 await windowManager.ensureInitialized(); WindowOptions windowOptions = const WindowOptions( - size: Size(1200, 700), + size: Size(1200, 1000), center: true, title: '升级工具', ); @@ -39,8 +39,8 @@ class MyApp extends StatelessWidget { title: '升级工具', debugShowCheckedModeBanner: false, theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, + useMaterial3: true, // 启用 Material 3 设计规范 + cardTheme: const CardTheme(color: Colors.white), ), home: const AppScaffold(), );