Browse Source

输入框无法使用前

master
hejl 2 months ago
parent
commit
9899e3bf55
  1. 368
      win_text_editor/lib/app/components/text_editor.dart
  2. 163
      win_text_editor/lib/app/components/text_editor_actions.dart
  3. 42
      win_text_editor/lib/app/components/text_editor_controller.dart
  4. 1
      win_text_editor/lib/app/core/app_scaffold.dart
  5. 3
      win_text_editor/lib/app/core/tab_view.dart
  6. 67
      win_text_editor/lib/app/modules/content_search/content_search_controller.dart
  7. 44
      win_text_editor/lib/app/modules/content_search/content_search_view.dart
  8. 44
      win_text_editor/lib/app/modules/content_search/directory_settings.dart
  9. 91
      win_text_editor/lib/app/modules/content_search/results_view.dart
  10. 117
      win_text_editor/lib/app/modules/content_search/search_settings.dart
  11. 22
      win_text_editor/lib/app/modules/template_parser/template_parser_view.dart
  12. 6
      win_text_editor/lib/main.dart

368
win_text_editor/lib/app/components/text_editor.dart

@ -1,9 +1,8 @@ @@ -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 { @@ -28,301 +27,128 @@ class TextEditor extends StatefulWidget {
}
class TextEditorState extends State<TextEditor> {
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<void> _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<void> _copyToClipboard(BuildContext context, String content) async {
await Clipboard.setData(ClipboardData(text: content));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板')));
}
}
Future<void> _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<bool>(
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<void> _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<bool>(
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);
}
}
}
}

163
win_text_editor/lib/app/components/text_editor_actions.dart

@ -0,0 +1,163 @@ @@ -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<void> 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<void> 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<void> 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<bool>(
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<void> _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<bool>(
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);
}
}
}

42
win_text_editor/lib/app/components/text_editor_controller.dart

@ -0,0 +1,42 @@ @@ -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();
}
}

1
win_text_editor/lib/app/core/app_scaffold.dart

@ -18,6 +18,7 @@ class AppScaffold extends StatelessWidget { @@ -18,6 +18,7 @@ class AppScaffold extends StatelessWidget {
ChangeNotifierProvider(create: (_) => TabManager()), // TabManager
],
child: Scaffold(
backgroundColor: Colors.grey[100],
body: Column(
children: [
const AppMenu(), //

3
win_text_editor/lib/app/core/tab_view.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.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 { @@ -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);
}

67
win_text_editor/lib/app/modules/content_search/content_search_controller.dart

@ -0,0 +1,67 @@ @@ -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<SearchResult> results = [];
ContentSearchController({required this.tabManager});
Future<void> 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<void> 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<MatchResult> 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});
}

44
win_text_editor/lib/app/modules/content_search/content_search_view.dart

@ -0,0 +1,44 @@ @@ -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<ContentSearchView> createState() => ContentSearchViewState();
}
class ContentSearchViewState extends State<ContentSearchView> {
late final ContentSearchController _controller;
@override
void initState() {
super.initState();
_controller = ContentSearchController(
tabManager: Provider.of<TabManager>(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)),
],
),
);
}
}

44
win_text_editor/lib/app/modules/content_search/directory_settings.dart

@ -0,0 +1,44 @@ @@ -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),
],
),
),
);
}
}

91
win_text_editor/lib/app/modules/content_search/results_view.dart

@ -0,0 +1,91 @@ @@ -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 = <String, int>{};
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<MatchResult> matches) {
final spans = <TextSpan>[];
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);
}
}

117
win_text_editor/lib/app/modules/content_search/search_settings.dart

@ -0,0 +1,117 @@ @@ -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<TextEditorState> _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<SearchMode>(
value: SearchMode.locate,
groupValue: controller.searchMode,
onChanged: (value) => controller.searchMode = value!,
),
const Text('定位', style: TextStyle(fontSize: 12)),
],
),
Row(
children: [
Radio<SearchMode>(
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,
),
),
],
),
),
),
],
),
),
);
}
}

22
win_text_editor/lib/app/modules/template_parser/template_parser_view.dart

@ -23,19 +23,7 @@ class TemplateParserViewState extends State<TemplateParserView> { @@ -23,19 +23,7 @@ class TemplateParserViewState extends State<TemplateParserView> {
final GlobalKey<TextEditorState> _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<TemplateParserView> { @@ -55,13 +43,7 @@ class TemplateParserViewState extends State<TemplateParserView> {
}
// loadFile方法使用_activeEditorIndex
Future<void> loadFile(BuildContext context, String filePath) async {
if (_activeEditorIndex == 0) {
_editor1Key.currentState?.loadFile(context, filePath);
} else {
_editor2Key.currentState?.loadFile(context, filePath);
}
}
Future<void> loadFile(BuildContext context, String filePath) async {}
@override
Widget build(BuildContext context) {

6
win_text_editor/lib/main.dart

@ -10,7 +10,7 @@ void main() async { @@ -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 { @@ -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(),
);

Loading…
Cancel
Save