16 changed files with 247 additions and 130 deletions
@ -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 @@ |
|||||||
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}) { |
@ -0,0 +1,3 @@ |
|||||||
|
abstract class SelectableItem { |
||||||
|
bool isSelected = false; |
||||||
|
} |
@ -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 @@ |
|||||||
// 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