diff --git a/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart b/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart index f6f9978..bcdef37 100644 --- a/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart +++ b/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart @@ -16,6 +16,7 @@ class ContentSearchController extends BaseContentController { String _searchQuery = ''; String _searchDirectory = ''; String _fileType = '*.*'; + String _jumpFiles = ''; bool _caseSensitive = false; bool _wholeWord = false; bool _useRegex = false; @@ -31,6 +32,7 @@ class ContentSearchController extends BaseContentController { String get searchQuery => _searchQuery; String get searchDirectory => _searchDirectory; String get fileType => _fileType; + String get jumpFiles => _jumpFiles; bool get caseSensitive => _caseSensitive; bool get wholeWord => _wholeWord; bool get useRegex => _useRegex; @@ -91,6 +93,11 @@ class ContentSearchController extends BaseContentController { notifyListeners(); } + set jumpFiles(String value) { + _jumpFiles = value; + notifyListeners(); + } + set caseSensitive(bool value) { _caseSensitive = value; notifyListeners(); @@ -198,6 +205,7 @@ class ContentSearchController extends BaseContentController { directory: searchDirectory, query: searchQuery, fileType: fileType, + jumpFiles: jumpFiles, caseSensitive: caseSensitive, wholeWord: wholeWord, useRegex: useRegex, diff --git a/win_text_editor/lib/modules/content_search/services/count_search_service.dart b/win_text_editor/lib/modules/content_search/services/count_search_service.dart index 3bf4b95..eb10092 100644 --- a/win_text_editor/lib/modules/content_search/services/count_search_service.dart +++ b/win_text_editor/lib/modules/content_search/services/count_search_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; import 'dart:math'; +import 'package:path/path.dart' as path; import 'package:win_text_editor/modules/content_search/services/base_search_service.dart'; import 'package:win_text_editor/shared/utils/file_utils.dart'; @@ -23,6 +24,7 @@ class CountSearchService { required String directory, required String query, required String fileType, + required String jumpFiles, required bool caseSensitive, required bool wholeWord, required bool useRegex, @@ -38,7 +40,7 @@ class CountSearchService { final queryBatches = _chunkList(allQueries, _queriesPerBatch); // 获取所有目标文件路径 - final filePaths = await _collectFilePaths(directory, fileType, shouldStop); + final filePaths = await _collectFilePaths(directory, fileType, jumpFiles, shouldStop); if (filePaths.isEmpty) return counts; // 启动Isolate池 @@ -123,13 +125,17 @@ class CountSearchService { static Future> _collectFilePaths( String directory, String fileType, + String jumpFiles, bool Function()? shouldStop, ) async { final dir = Directory(directory); final paths = []; + final jumpFileList = BaseSearchService.splitQuery(jumpFiles.toLowerCase()); await for (final entity in dir.list(recursive: true)) { if (shouldStop?.call() == true) break; - if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { + if (entity is File && + !jumpFileList.contains(path.basename(entity.path).toLowerCase()) && + FileUtils.matchesFileType(entity.path, fileType)) { paths.add(entity.path); } } diff --git a/win_text_editor/lib/modules/content_search/widgets/directory_settings.dart b/win_text_editor/lib/modules/content_search/widgets/directory_settings.dart index 0561a14..11dc2be 100644 --- a/win_text_editor/lib/modules/content_search/widgets/directory_settings.dart +++ b/win_text_editor/lib/modules/content_search/widgets/directory_settings.dart @@ -12,6 +12,7 @@ class DirectorySettings extends StatefulWidget { class _DirectorySettingsState extends State { late TextEditingController _searchDirectoryController; late TextEditingController _fileTypeController; + late TextEditingController _jumpFilesController; @override void initState() { @@ -19,12 +20,14 @@ class _DirectorySettingsState extends State { final controller = context.read(); _searchDirectoryController = TextEditingController(text: controller.searchDirectory); _fileTypeController = TextEditingController(text: controller.fileType); + _jumpFilesController = TextEditingController(text: controller.jumpFiles); } @override void dispose() { _searchDirectoryController.dispose(); _fileTypeController.dispose(); + _jumpFilesController.dispose(); super.dispose(); } @@ -56,6 +59,17 @@ class _DirectorySettingsState extends State { onChanged: (value) => controller.searchDirectory = value, ), ), + + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.folder_open), + onPressed: () async { + await controller.pickDirectory(); + // 不需要手动更新 _searchDirectoryController.text, + // 因为 Consumer 会触发重建并自动同步 + }, + ), + const SizedBox(width: 8), SizedBox( width: 100, @@ -68,14 +82,18 @@ class _DirectorySettingsState extends State { onChanged: (value) => controller.fileType = value, ), ), + const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.folder_open), - onPressed: () async { - await controller.pickDirectory(); - // 不需要手动更新 _searchDirectoryController.text, - // 因为 Consumer 会触发重建并自动同步 - }, + SizedBox( + width: 200, + child: TextField( + controller: _jumpFilesController, + decoration: const InputDecoration( + labelText: '跳过文件(列表)', + border: OutlineInputBorder(), + ), + onChanged: (value) => controller.jumpFiles = value, + ), ), ], ), diff --git a/win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart b/win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart index e4b4ee7..c9df59a 100644 --- a/win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart +++ b/win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; -import 'package:win_text_editor/shared/base/base_data_source.dart'; +import 'package:win_text_editor/shared/base/selectable_data_source.dart'; import 'package:win_text_editor/modules/memory_table/models/memory_table.dart'; // 索引数据源 diff --git a/win_text_editor/lib/modules/memory_table/models/memory_table.dart b/win_text_editor/lib/modules/memory_table/models/memory_table.dart index babc117..6117750 100644 --- a/win_text_editor/lib/modules/memory_table/models/memory_table.dart +++ b/win_text_editor/lib/modules/memory_table/models/memory_table.dart @@ -1,3 +1,4 @@ +import 'package:win_text_editor/shared/base/selectable_item.dart'; import 'package:win_text_editor/shared/models/std_filed.dart'; class Index implements SelectableItem { diff --git a/win_text_editor/lib/modules/uft_component/controllers/component_source.dart b/win_text_editor/lib/modules/uft_component/controllers/component_source.dart index 708389b..2e6a9f2 100644 --- a/win_text_editor/lib/modules/uft_component/controllers/component_source.dart +++ b/win_text_editor/lib/modules/uft_component/controllers/component_source.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:win_text_editor/modules/uft_component/models/uft_component.dart'; -import 'package:win_text_editor/shared/base/base_data_source.dart'; +import 'package:win_text_editor/shared/base/selectable_data_source.dart'; class ComponentSource extends SelectableDataSource { ComponentSource( diff --git a/win_text_editor/lib/modules/uft_component/models/uft_component.dart b/win_text_editor/lib/modules/uft_component/models/uft_component.dart index e4e275b..03ebc74 100644 --- a/win_text_editor/lib/modules/uft_component/models/uft_component.dart +++ b/win_text_editor/lib/modules/uft_component/models/uft_component.dart @@ -1,4 +1,5 @@ import 'package:win_text_editor/modules/memory_table/models/memory_table.dart'; +import 'package:win_text_editor/shared/base/selectable_item.dart'; import 'package:win_text_editor/shared/models/std_filed.dart'; class UftComponent implements SelectableItem { diff --git a/win_text_editor/lib/shared/base/my_sf_data_grid.dart b/win_text_editor/lib/shared/base/my_sf_data_grid.dart new file mode 100644 index 0000000..ad31054 --- /dev/null +++ b/win_text_editor/lib/shared/base/my_sf_data_grid.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:win_text_editor/shared/base/selectable_data_source.dart'; +import 'package:win_text_editor/shared/base/selectable_item.dart'; +import 'package:win_text_editor/shared/components/my_sf_data_source.dart'; + +class MySfDataGrid extends StatelessWidget { + final MySfDataSource dataSource; + final Function(int index, bool isSelected)? onSelectionChanged; + final List columns; + + const MySfDataGrid({ + super.key, + required this.dataSource, + required this.columns, + this.onSelectionChanged, + }); + + Widget _buildCheckboxHeader(BuildContext context, SelectableDataSource dataSource) { + final allSelected = + dataSource.items.isNotEmpty && dataSource.items.every((item) => item.isSelected); + final someSelected = + dataSource.items.isNotEmpty && + dataSource.items.any((item) => item.isSelected) && + !allSelected; + + return Container( + alignment: Alignment.center, + color: Colors.grey[200], + child: Transform.scale( + scale: 0.75, + child: Checkbox( + value: allSelected, + tristate: someSelected, + onChanged: (value) => dataSource.toggleAllSelection(value ?? false), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: constraints.maxWidth, + child: SfDataGrid( + rowHeight: 32, + headerRowHeight: 32, + source: dataSource, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columnWidthMode: ColumnWidthMode.fitByCellValue, + selectionMode: SelectionMode.none, + allowEditing: true, + columns: [ + GridColumn( + columnName: 'select', + label: ValueListenableBuilder( + valueListenable: dataSource.selectionNotifier, + builder: (context, _, __) => _buildCheckboxHeader(context, dataSource), + ), + width: 60, + ), + ...columns, + ], + onCellTap: (details) { + if (details.column.columnName == 'select') { + final rowIndex = details.rowColumnIndex.rowIndex - 1; + if (rowIndex >= 0 && rowIndex < dataSource.items.length) { + dataSource.toggleRowSelection( + rowIndex, + !dataSource.items[rowIndex].isSelected, + ); + onSelectionChanged?.call(rowIndex, dataSource.items[rowIndex].isSelected); + } + } + }, + onCellSecondaryTap: (details) { + final rowIndex = details.rowColumnIndex.rowIndex - 1; + final columnName = details.column.columnName; + final isHeader = rowIndex < 0; + + final renderBox = context.findRenderObject() as RenderBox; + final position = details.globalPosition; + + showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy, + position.dx + renderBox.size.width - position.dx, + position.dy + renderBox.size.height - position.dy, + ), + items: [ + if (!isHeader) + PopupMenuItem(value: 'copyCell', child: Text('复制单元格 ($columnName)')), + if (!isHeader) + const PopupMenuItem(value: 'copyRow', child: Text('复制当前行')), + PopupMenuItem(value: 'copyColumn', child: Text('复制整列 ($columnName)')), + ], + ).then((value) async { + if (value != null) { + switch (value) { + case 'copyCell': + final row = dataSource.effectiveRows[rowIndex]; + await dataSource.copyCellValue(row, columnName); + break; + case 'copyRow': + final row = dataSource.effectiveRows[rowIndex]; + await dataSource.copyRowValues(row); + break; + case 'copyColumn': + await dataSource.copyColumnValues( + dataSource.effectiveRows, + columnName, + ); + break; + } + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); + } + }); + }, + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/win_text_editor/lib/shared/base/base_data_source.dart b/win_text_editor/lib/shared/base/selectable_data_source.dart similarity index 95% rename from win_text_editor/lib/shared/base/base_data_source.dart rename to win_text_editor/lib/shared/base/selectable_data_source.dart index 85f4b35..cb44759 100644 --- a/win_text_editor/lib/shared/base/base_data_source.dart +++ b/win_text_editor/lib/shared/base/selectable_data_source.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; -import 'package:win_text_editor/shared/models/std_filed.dart'; +import 'package:win_text_editor/shared/base/selectable_item.dart'; abstract class SelectableDataSource extends DataGridSource { SelectableDataSource(this.items, {this.onSelectionChanged}) { diff --git a/win_text_editor/lib/shared/base/selectable_item.dart b/win_text_editor/lib/shared/base/selectable_item.dart new file mode 100644 index 0000000..7db463a --- /dev/null +++ b/win_text_editor/lib/shared/base/selectable_item.dart @@ -0,0 +1,3 @@ +abstract class SelectableItem { + bool isSelected = false; +} diff --git a/win_text_editor/lib/shared/components/my_grid_column.dart b/win_text_editor/lib/shared/components/my_grid_column.dart index f7f5281..8b23338 100644 --- a/win_text_editor/lib/shared/components/my_grid_column.dart +++ b/win_text_editor/lib/shared/components/my_grid_column.dart @@ -7,7 +7,7 @@ class MyGridColumn extends GridColumn { double minimumWidth = 100, double maximumWidth = double.infinity, required String label, - bool allowEditing = false, + bool allowEditing = true, }) : super( columnName: columnName, minimumWidth: minimumWidth, diff --git a/win_text_editor/lib/shared/components/my_sf_data_source.dart b/win_text_editor/lib/shared/components/my_sf_data_source.dart new file mode 100644 index 0000000..2d9e599 --- /dev/null +++ b/win_text_editor/lib/shared/components/my_sf_data_source.dart @@ -0,0 +1,43 @@ +import 'package:flutter/services.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:win_text_editor/shared/base/selectable_data_source.dart'; +import 'package:win_text_editor/shared/base/selectable_item.dart'; + +abstract class MySfDataSource extends SelectableDataSource { + MySfDataSource( + List items, { + required Null Function(dynamic index, dynamic isSelected) onSelectionChanged, + }) : super(items, onSelectionChanged: onSelectionChanged); + + @override + List get rows; + + @override + DataGridRowAdapter buildRow(DataGridRow row); + + Future copyCellValue(DataGridRow row, String columnName) async { + final cell = row.getCells().firstWhere( + (cell) => cell.columnName == columnName, + orElse: () => throw Exception('Column not found'), + ); + await Clipboard.setData(ClipboardData(text: cell.value.toString())); + } + + Future copyRowValues(DataGridRow row) async { + final values = row.getCells().map((cell) => cell.value.toString()).join('\t'); + await Clipboard.setData(ClipboardData(text: values)); + } + + Future copyColumnValues(List rows, String columnName) async { + final values = rows + .map((row) { + final cell = row.getCells().firstWhere( + (cell) => cell.columnName == columnName, + orElse: () => throw Exception('Column not found'), + ); + return cell.value.toString(); + }) + .join('\n'); + await Clipboard.setData(ClipboardData(text: values)); + } +} diff --git a/win_text_editor/lib/shared/models/std_filed.dart b/win_text_editor/lib/shared/models/std_filed.dart index 3084f1e..c85c2f4 100644 --- a/win_text_editor/lib/shared/models/std_filed.dart +++ b/win_text_editor/lib/shared/models/std_filed.dart @@ -1,4 +1,5 @@ import 'package:hive/hive.dart'; +import 'package:win_text_editor/shared/base/selectable_item.dart'; @HiveType(typeId: 7) // 确保与适配器的typeId一致 class StdField { @@ -14,10 +15,6 @@ class StdField { StdField({required this.name, required this.chineseName, required this.dateType}); } -abstract class SelectableItem { - bool isSelected = false; -} - class Field implements SelectableItem { Field(this.id, this.name, this.chineseName, this.type, [this.isSelected = false]); diff --git a/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart b/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart index 605afe2..6eacfa4 100644 --- a/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart +++ b/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; -import 'package:win_text_editor/shared/base/base_data_source.dart'; +import 'package:win_text_editor/shared/components/my_sf_data_source.dart'; import 'package:win_text_editor/shared/models/std_filed.dart'; -class FieldsDataSource extends SelectableDataSource { +class FieldsDataSource extends MySfDataSource { FieldsDataSource( List fields, { required Null Function(dynamic index, dynamic isSelected) onSelectionChanged, @@ -36,7 +36,7 @@ class FieldsDataSource extends SelectableDataSource { if (cell.columnName == 'select') { return Center( child: Transform.scale( - scale: 0.75, // 调整这个值来改变大小 + scale: 0.75, child: Checkbox( value: items[rowIndex].isSelected, onChanged: (value) => toggleRowSelection(rowIndex, value), diff --git a/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart b/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart index 2c4943e..9efaecf 100644 --- a/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart +++ b/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:syncfusion_flutter_datagrid/datagrid.dart'; -import 'package:win_text_editor/shared/base/base_data_source.dart'; +import 'package:win_text_editor/shared/base/my_sf_data_grid.dart'; import 'package:win_text_editor/shared/components/my_grid_column.dart'; import 'package:win_text_editor/shared/models/std_filed.dart'; import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart'; @@ -11,86 +10,17 @@ class FieldsDataGrid extends StatelessWidget { const FieldsDataGrid({super.key, required this.fieldsSource, this.onSelectionChanged}); - Widget _buildCheckboxHeader( - BuildContext context, - SelectableDataSource dataSource, - ) { - final allSelected = - dataSource.items.isNotEmpty && dataSource.items.every((item) => item.isSelected); - final someSelected = - dataSource.items.isNotEmpty && - dataSource.items.any((item) => item.isSelected) && - !allSelected; - - return Container( - alignment: Alignment.center, - color: Colors.grey[200], - child: Transform.scale( - scale: 0.75, // 调整这个值来改变大小 - child: Checkbox( - value: allSelected, - tristate: someSelected, - onChanged: (value) => dataSource.toggleAllSelection(value ?? false), - ), - ), - ); - } - @override Widget build(BuildContext context) { - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - width: constraints.maxWidth, - child: SfDataGrid( - rowHeight: 32, - headerRowHeight: 32, - source: fieldsSource, - gridLinesVisibility: GridLinesVisibility.both, - headerGridLinesVisibility: GridLinesVisibility.both, - columnWidthMode: ColumnWidthMode.fitByCellValue, - selectionMode: SelectionMode.none, - columns: [ - GridColumn( - columnName: 'select', - label: ValueListenableBuilder( - valueListenable: fieldsSource.selectionNotifier, - builder: (context, _, __) => _buildCheckboxHeader(context, fieldsSource), - ), - width: 60, - ), - MyGridColumn(columnName: 'id', label: '序号', minimumWidth: 80), - MyGridColumn(columnName: 'name', label: '名称', minimumWidth: 120), - MyGridColumn(columnName: 'chineseName', label: '中文名', minimumWidth: 120), - MyGridColumn(columnName: 'type', label: '类型', minimumWidth: 120), - ], - onCellTap: (details) { - if (details.column.columnName == 'select') { - final rowIndex = details.rowColumnIndex.rowIndex - 1; - if (rowIndex >= 0 && rowIndex < fieldsSource.items.length) { - fieldsSource.toggleRowSelection( - rowIndex, - !fieldsSource.items[rowIndex].isSelected, - ); - onSelectionChanged?.call( - rowIndex, - fieldsSource.items[rowIndex].isSelected, - ); - } - } - }, - ), - ); - }, - ), - ), - ], - ), + return MySfDataGrid( + dataSource: fieldsSource, + onSelectionChanged: onSelectionChanged, + columns: [ + MyGridColumn(columnName: 'id', label: '序号', minimumWidth: 80), + MyGridColumn(columnName: 'name', label: '名称', minimumWidth: 120), + MyGridColumn(columnName: 'chineseName', label: '中文名', minimumWidth: 120), + MyGridColumn(columnName: 'type', label: '类型', minimumWidth: 120), + ], ); } } diff --git a/win_text_editor/test/widget_test.dart b/win_text_editor/test/widget_test.dart deleted file mode 100644 index 85399d2..0000000 --- a/win_text_editor/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:win_text_editor/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}