16 changed files with 247 additions and 130 deletions
@ -0,0 +1,140 @@
@@ -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('已复制到剪贴板'))); |
||||
} |
||||
}); |
||||
}, |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
@ -1,6 +1,6 @@
@@ -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<T extends SelectableItem> extends DataGridSource { |
||||
SelectableDataSource(this.items, {this.onSelectionChanged}) { |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
abstract class SelectableItem { |
||||
bool isSelected = false; |
||||
} |
@ -0,0 +1,43 @@
@@ -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)); |
||||
} |
||||
} |
@ -1,30 +0,0 @@
@@ -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…
Reference in new issue