Browse Source

剥离出抽象DataGrid

master
hejl 1 month ago
parent
commit
38721a8752
  1. 8
      win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart
  2. 10
      win_text_editor/lib/modules/content_search/services/count_search_service.dart
  3. 32
      win_text_editor/lib/modules/content_search/widgets/directory_settings.dart
  4. 2
      win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart
  5. 1
      win_text_editor/lib/modules/memory_table/models/memory_table.dart
  6. 2
      win_text_editor/lib/modules/uft_component/controllers/component_source.dart
  7. 1
      win_text_editor/lib/modules/uft_component/models/uft_component.dart
  8. 140
      win_text_editor/lib/shared/base/my_sf_data_grid.dart
  9. 2
      win_text_editor/lib/shared/base/selectable_data_source.dart
  10. 3
      win_text_editor/lib/shared/base/selectable_item.dart
  11. 2
      win_text_editor/lib/shared/components/my_grid_column.dart
  12. 43
      win_text_editor/lib/shared/components/my_sf_data_source.dart
  13. 5
      win_text_editor/lib/shared/models/std_filed.dart
  14. 6
      win_text_editor/lib/shared/uft_std_fields/field_data_source.dart
  15. 90
      win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart
  16. 30
      win_text_editor/test/widget_test.dart

8
win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart

@ -16,6 +16,7 @@ class ContentSearchController extends BaseContentController {
String _searchQuery = ''; String _searchQuery = '';
String _searchDirectory = ''; String _searchDirectory = '';
String _fileType = '*.*'; String _fileType = '*.*';
String _jumpFiles = '';
bool _caseSensitive = false; bool _caseSensitive = false;
bool _wholeWord = false; bool _wholeWord = false;
bool _useRegex = false; bool _useRegex = false;
@ -31,6 +32,7 @@ class ContentSearchController extends BaseContentController {
String get searchQuery => _searchQuery; String get searchQuery => _searchQuery;
String get searchDirectory => _searchDirectory; String get searchDirectory => _searchDirectory;
String get fileType => _fileType; String get fileType => _fileType;
String get jumpFiles => _jumpFiles;
bool get caseSensitive => _caseSensitive; bool get caseSensitive => _caseSensitive;
bool get wholeWord => _wholeWord; bool get wholeWord => _wholeWord;
bool get useRegex => _useRegex; bool get useRegex => _useRegex;
@ -91,6 +93,11 @@ class ContentSearchController extends BaseContentController {
notifyListeners(); notifyListeners();
} }
set jumpFiles(String value) {
_jumpFiles = value;
notifyListeners();
}
set caseSensitive(bool value) { set caseSensitive(bool value) {
_caseSensitive = value; _caseSensitive = value;
notifyListeners(); notifyListeners();
@ -198,6 +205,7 @@ class ContentSearchController extends BaseContentController {
directory: searchDirectory, directory: searchDirectory,
query: searchQuery, query: searchQuery,
fileType: fileType, fileType: fileType,
jumpFiles: jumpFiles,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
wholeWord: wholeWord, wholeWord: wholeWord,
useRegex: useRegex, useRegex: useRegex,

10
win_text_editor/lib/modules/content_search/services/count_search_service.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:math'; 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/modules/content_search/services/base_search_service.dart';
import 'package:win_text_editor/shared/utils/file_utils.dart'; import 'package:win_text_editor/shared/utils/file_utils.dart';
@ -23,6 +24,7 @@ class CountSearchService {
required String directory, required String directory,
required String query, required String query,
required String fileType, required String fileType,
required String jumpFiles,
required bool caseSensitive, required bool caseSensitive,
required bool wholeWord, required bool wholeWord,
required bool useRegex, required bool useRegex,
@ -38,7 +40,7 @@ class CountSearchService {
final queryBatches = _chunkList(allQueries, _queriesPerBatch); 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; if (filePaths.isEmpty) return counts;
// Isolate池 // Isolate池
@ -123,13 +125,17 @@ class CountSearchService {
static Future<List<String>> _collectFilePaths( static Future<List<String>> _collectFilePaths(
String directory, String directory,
String fileType, String fileType,
String jumpFiles,
bool Function()? shouldStop, bool Function()? shouldStop,
) async { ) async {
final dir = Directory(directory); final dir = Directory(directory);
final paths = <String>[]; final paths = <String>[];
final jumpFileList = BaseSearchService.splitQuery(jumpFiles.toLowerCase());
await for (final entity in dir.list(recursive: true)) { await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) break; 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); paths.add(entity.path);
} }
} }

32
win_text_editor/lib/modules/content_search/widgets/directory_settings.dart

@ -12,6 +12,7 @@ class DirectorySettings extends StatefulWidget {
class _DirectorySettingsState extends State<DirectorySettings> { class _DirectorySettingsState extends State<DirectorySettings> {
late TextEditingController _searchDirectoryController; late TextEditingController _searchDirectoryController;
late TextEditingController _fileTypeController; late TextEditingController _fileTypeController;
late TextEditingController _jumpFilesController;
@override @override
void initState() { void initState() {
@ -19,12 +20,14 @@ class _DirectorySettingsState extends State<DirectorySettings> {
final controller = context.read<ContentSearchController>(); final controller = context.read<ContentSearchController>();
_searchDirectoryController = TextEditingController(text: controller.searchDirectory); _searchDirectoryController = TextEditingController(text: controller.searchDirectory);
_fileTypeController = TextEditingController(text: controller.fileType); _fileTypeController = TextEditingController(text: controller.fileType);
_jumpFilesController = TextEditingController(text: controller.jumpFiles);
} }
@override @override
void dispose() { void dispose() {
_searchDirectoryController.dispose(); _searchDirectoryController.dispose();
_fileTypeController.dispose(); _fileTypeController.dispose();
_jumpFilesController.dispose();
super.dispose(); super.dispose();
} }
@ -56,6 +59,17 @@ class _DirectorySettingsState extends State<DirectorySettings> {
onChanged: (value) => controller.searchDirectory = value, 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), const SizedBox(width: 8),
SizedBox( SizedBox(
width: 100, width: 100,
@ -68,14 +82,18 @@ class _DirectorySettingsState extends State<DirectorySettings> {
onChanged: (value) => controller.fileType = value, onChanged: (value) => controller.fileType = value,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton( SizedBox(
icon: const Icon(Icons.folder_open), width: 200,
onPressed: () async { child: TextField(
await controller.pickDirectory(); controller: _jumpFilesController,
// _searchDirectoryController.text decoration: const InputDecoration(
// Consumer labelText: '跳过文件(列表)',
}, border: OutlineInputBorder(),
),
onChanged: (value) => controller.jumpFiles = value,
),
), ),
], ],
), ),

2
win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.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'; import 'package:win_text_editor/modules/memory_table/models/memory_table.dart';
// //

1
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'; import 'package:win_text_editor/shared/models/std_filed.dart';
class Index implements SelectableItem { class Index implements SelectableItem {

2
win_text_editor/lib/modules/uft_component/controllers/component_source.dart

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.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/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<UftComponent> { class ComponentSource extends SelectableDataSource<UftComponent> {
ComponentSource( ComponentSource(

1
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/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'; import 'package:win_text_editor/shared/models/std_filed.dart';
class UftComponent implements SelectableItem { class UftComponent implements SelectableItem {

140
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<T extends SelectableItem> extends StatelessWidget {
final MySfDataSource<T> dataSource;
final Function(int index, bool isSelected)? onSelectionChanged;
final List<GridColumn> columns;
const MySfDataGrid({
super.key,
required this.dataSource,
required this.columns,
this.onSelectionChanged,
});
Widget _buildCheckboxHeader(BuildContext context, SelectableDataSource<T> 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<bool>(
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('已复制到剪贴板')));
}
});
},
),
);
},
),
),
],
),
);
}
}

2
win_text_editor/lib/shared/base/base_data_source.dart → win_text_editor/lib/shared/base/selectable_data_source.dart

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.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<T extends SelectableItem> extends DataGridSource { abstract class SelectableDataSource<T extends SelectableItem> extends DataGridSource {
SelectableDataSource(this.items, {this.onSelectionChanged}) { SelectableDataSource(this.items, {this.onSelectionChanged}) {

3
win_text_editor/lib/shared/base/selectable_item.dart

@ -0,0 +1,3 @@
abstract class SelectableItem {
bool isSelected = false;
}

2
win_text_editor/lib/shared/components/my_grid_column.dart

@ -7,7 +7,7 @@ class MyGridColumn extends GridColumn {
double minimumWidth = 100, double minimumWidth = 100,
double maximumWidth = double.infinity, double maximumWidth = double.infinity,
required String label, required String label,
bool allowEditing = false, bool allowEditing = true,
}) : super( }) : super(
columnName: columnName, columnName: columnName,
minimumWidth: minimumWidth, minimumWidth: minimumWidth,

43
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<T extends SelectableItem> extends SelectableDataSource<T> {
MySfDataSource(
List<T> items, {
required Null Function(dynamic index, dynamic isSelected) onSelectionChanged,
}) : super(items, onSelectionChanged: onSelectionChanged);
@override
List<DataGridRow> get rows;
@override
DataGridRowAdapter buildRow(DataGridRow row);
Future<void> 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<void> copyRowValues(DataGridRow row) async {
final values = row.getCells().map((cell) => cell.value.toString()).join('\t');
await Clipboard.setData(ClipboardData(text: values));
}
Future<void> copyColumnValues(List<DataGridRow> 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));
}
}

5
win_text_editor/lib/shared/models/std_filed.dart

@ -1,4 +1,5 @@
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:win_text_editor/shared/base/selectable_item.dart';
@HiveType(typeId: 7) // typeId一致 @HiveType(typeId: 7) // typeId一致
class StdField { class StdField {
@ -14,10 +15,6 @@ class StdField {
StdField({required this.name, required this.chineseName, required this.dateType}); StdField({required this.name, required this.chineseName, required this.dateType});
} }
abstract class SelectableItem {
bool isSelected = false;
}
class Field implements SelectableItem { class Field implements SelectableItem {
Field(this.id, this.name, this.chineseName, this.type, [this.isSelected = false]); Field(this.id, this.name, this.chineseName, this.type, [this.isSelected = false]);

6
win_text_editor/lib/shared/uft_std_fields/field_data_source.dart

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.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'; import 'package:win_text_editor/shared/models/std_filed.dart';
class FieldsDataSource extends SelectableDataSource<Field> { class FieldsDataSource extends MySfDataSource<Field> {
FieldsDataSource( FieldsDataSource(
List<Field> fields, { List<Field> fields, {
required Null Function(dynamic index, dynamic isSelected) onSelectionChanged, required Null Function(dynamic index, dynamic isSelected) onSelectionChanged,
@ -36,7 +36,7 @@ class FieldsDataSource extends SelectableDataSource<Field> {
if (cell.columnName == 'select') { if (cell.columnName == 'select') {
return Center( return Center(
child: Transform.scale( child: Transform.scale(
scale: 0.75, // scale: 0.75,
child: Checkbox( child: Checkbox(
value: items[rowIndex].isSelected, value: items[rowIndex].isSelected,
onChanged: (value) => toggleRowSelection(rowIndex, value), onChanged: (value) => toggleRowSelection(rowIndex, value),

90
win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:win_text_editor/shared/base/my_sf_data_grid.dart';
import 'package:win_text_editor/shared/base/base_data_source.dart';
import 'package:win_text_editor/shared/components/my_grid_column.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/models/std_filed.dart';
import 'package:win_text_editor/shared/uft_std_fields/field_data_source.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}); const FieldsDataGrid({super.key, required this.fieldsSource, this.onSelectionChanged});
Widget _buildCheckboxHeader<T extends SelectableItem>(
BuildContext context,
SelectableDataSource<T> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return MySfDataGrid<Field>(
child: Column( dataSource: fieldsSource,
crossAxisAlignment: CrossAxisAlignment.start, onSelectionChanged: onSelectionChanged,
children: [ columns: [
Expanded( MyGridColumn(columnName: 'id', label: '序号', minimumWidth: 80),
child: LayoutBuilder( MyGridColumn(columnName: 'name', label: '名称', minimumWidth: 120),
builder: (context, constraints) { MyGridColumn(columnName: 'chineseName', label: '中文名', minimumWidth: 120),
return SizedBox( MyGridColumn(columnName: 'type', label: '类型', minimumWidth: 120),
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<bool>(
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,
);
}
}
},
),
);
},
),
),
],
),
); );
} }
} }

30
win_text_editor/test/widget_test.dart

@ -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);
});
}
Loading…
Cancel
Save