Compare commits

...

2 Commits

  1. 2
      win_text_editor/assets/config/uft_macro_list.yaml
  2. 5
      win_text_editor/lib/menus/app_menu.dart
  3. 5
      win_text_editor/lib/menus/menu_actions.dart
  4. 1
      win_text_editor/lib/menus/menu_constants.dart
  5. 10
      win_text_editor/lib/modules/content_search/models/count_result.dart
  6. 6
      win_text_editor/lib/modules/content_search/models/search_result.dart
  7. 131
      win_text_editor/lib/modules/content_search/widgets/results_view.dart
  8. 4
      win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart
  9. 24
      win_text_editor/lib/modules/data_extract/models/search_result.dart
  10. 3
      win_text_editor/lib/modules/data_extract/models/xml_rule.dart
  11. 86
      win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart
  12. 38
      win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart
  13. 9
      win_text_editor/lib/modules/data_extract/widgets/results_view.dart
  14. 5
      win_text_editor/lib/modules/module_router.dart
  15. 13
      win_text_editor/lib/modules/outline/controllers/outline_controller.dart
  16. 226
      win_text_editor/lib/modules/outline/controllers/outline_provider.dart
  17. 137
      win_text_editor/lib/modules/outline/models/outline_node.dart
  18. 128
      win_text_editor/lib/modules/outline/services/outline_service.dart
  19. 85
      win_text_editor/lib/modules/outline/widgets/outline_explorer.dart
  20. 73
      win_text_editor/lib/modules/outline/widgets/outline_view.dart
  21. 52
      win_text_editor/lib/modules/template_parser/widgets/grid_view.dart
  22. 4
      win_text_editor/lib/modules/uft_component/controllers/component_source.dart
  23. 87
      win_text_editor/lib/shared/base/my_sf_data_grid.dart
  24. 37
      win_text_editor/lib/shared/base/my_sf_data_source.dart
  25. 4
      win_text_editor/lib/shared/components/file_explorer.dart
  26. 2
      win_text_editor/lib/shared/components/my_grid_column.dart
  27. 2
      win_text_editor/lib/shared/uft_std_fields/field_data_source.dart
  28. 5
      win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart

2
win_text_editor/assets/config/uft_macro_list.yaml

@ -91,7 +91,7 @@ templates: @@ -91,7 +91,7 @@ templates:
遍历记录:
body: |
<M>[遍历记录开始][{{tableName}}({{selectIndexOrKey.name}})][
[遍历记录开始][{{tableName}}({{selectIndexOrKey.name}})][
{{#selectIndexOrKey.fields}}
{{name}} = @{{name}} {{^isLast}}, {{/isLast}}
{{/selectIndexOrKey.fields}}

5
win_text_editor/lib/menus/app_menu.dart

@ -72,6 +72,11 @@ class AppMenu extends StatelessWidget { @@ -72,6 +72,11 @@ class AppMenu extends StatelessWidget {
value: MenuConstants.callFunction,
child: ListTile(leading: Icon(Icons.functions), title: Text('功能号调用')),
),
const PopupMenuDivider(),
const PopupMenuItem<String>(
value: MenuConstants.outline,
child: ListTile(leading: Icon(Icons.outlined_flag_rounded), title: Text('大纲')),
),
];
}

5
win_text_editor/lib/menus/menu_actions.dart

@ -21,6 +21,7 @@ class MenuActions { @@ -21,6 +21,7 @@ class MenuActions {
MenuConstants.uftComponent: _uftComponent,
MenuConstants.callFunction: _callFunction,
MenuConstants.demo: _demo,
MenuConstants.outline: _outline,
MenuConstants.exit: _exitApplication,
};
@ -83,6 +84,10 @@ class MenuActions { @@ -83,6 +84,10 @@ class MenuActions {
await _openOrActivateTab(context, "Demo", RouterKey.demo, Icons.code);
}
static Future<void> _outline(BuildContext context) async {
await _openOrActivateTab(context, "大纲", RouterKey.outline, Icons.outlined_flag_rounded);
}
static Future<void> _openOrActivateTab(
BuildContext context,
String title,

1
win_text_editor/lib/menus/menu_constants.dart

@ -29,6 +29,7 @@ class MenuConstants { @@ -29,6 +29,7 @@ class MenuConstants {
static const String uftComponent = 'uft_component';
static const String callFunction = 'call_function';
static const String uftTools = 'uft_tools';
static const String outline = 'outline';
//
static const String undo = 'undo';

10
win_text_editor/lib/modules/content_search/models/count_result.dart

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
import 'package:win_text_editor/shared/base/selectable_item.dart';
class CountResult implements SelectableItem {
final String keyword;
final int matchCount;
@override
bool isSelected;
CountResult({required this.keyword, required this.matchCount, this.isSelected = false});
}

6
win_text_editor/lib/modules/content_search/models/search_result.dart

@ -1,11 +1,14 @@ @@ -1,11 +1,14 @@
import 'package:win_text_editor/modules/content_search/models/match_result.dart';
import 'package:win_text_editor/shared/base/selectable_item.dart';
class SearchResult {
class SearchResult implements SelectableItem {
final String filePath;
final int lineNumber;
final String lineContent;
final List<MatchResult> matches;
final String queryTerm; //
@override
bool isSelected;
SearchResult({
required this.filePath,
@ -13,5 +16,6 @@ class SearchResult { @@ -13,5 +16,6 @@ class SearchResult {
required this.lineContent,
required this.matches,
required this.queryTerm,
this.isSelected = false,
});
}

131
win_text_editor/lib/modules/content_search/widgets/results_view.dart

@ -6,10 +6,13 @@ import 'package:path/path.dart' as path; @@ -6,10 +6,13 @@ import 'package:path/path.dart' as path;
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart';
import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/modules/content_search/models/count_result.dart';
import 'dart:io';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart';
import 'package:win_text_editor/shared/base/my_sf_data_grid.dart';
import 'package:win_text_editor/shared/base/my_sf_data_source.dart';
import 'package:win_text_editor/shared/components/my_grid_column.dart';
// import 'package:recycle_bin/recycle_bin.dart';
@ -29,88 +32,15 @@ class ResultsView extends StatelessWidget { @@ -29,88 +32,15 @@ class ResultsView extends StatelessWidget {
}
return Card(
child: GestureDetector(
onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition, controller);
},
child: _buildResultsGrid(controller, context),
),
child:
controller.searchMode == SearchMode.locate
? _buildLocateGrid(controller, context)
: _buildCountGrid(controller, context),
);
}
Future<void> _showContextMenu(
BuildContext context,
Offset position,
ContentSearchController controller,
) async {
//
final renderBox = context.findRenderObject() as RenderBox;
final localPosition = renderBox.globalToLocal(position);
//
final result = await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(
position.dx,
position.dy,
position.dx + renderBox.size.width - localPosition.dx,
position.dy + renderBox.size.height - localPosition.dy,
),
items: [const PopupMenuItem<String>(value: 'export', child: Text('导出(csv)'))],
);
//
if (result == 'export' && context.mounted) {
try {
await _exportToCsv(controller);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}')));
}
}
}
}
Future<void> _exportToCsv(ContentSearchController controller) async {
String csvData = '';
if (controller.searchMode == SearchMode.locate) {
csvData = '文件\t行号\t内容\n';
for (var result in controller.results) {
csvData +=
'${path.basename(result.filePath)}\t${result.lineNumber}\t${result.lineContent}\n';
}
} else {
csvData = '关键词\t匹配数量\n';
for (var result in controller.results) {
csvData += '${result.filePath}\t${result.lineNumber}\n';
}
}
final filePath = await FilePicker.platform.saveFile(
dialogTitle: '保存导出结果',
fileName: 'search_results.csv',
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (filePath != null) {
final file = File(filePath);
await file.writeAsString(csvData);
}
}
Widget _buildResultsGrid(ContentSearchController controller, BuildContext context) {
return controller.searchMode == SearchMode.locate
? _buildLocateGrid(controller, context)
: _buildCountGrid(controller);
}
Widget _buildLocateGrid(ContentSearchController controller, BuildContext context) {
return SfDataGrid(
rowHeight: 32,
headerRowHeight: 32,
return MySfDataGrid<SearchResult>(
source: LocateDataSource(controller, context),
columns: [
ShortGridColumn(columnName: 'index', label: '序号'),
@ -118,45 +48,29 @@ class ResultsView extends StatelessWidget { @@ -118,45 +48,29 @@ class ResultsView extends StatelessWidget {
MyGridColumn(columnName: 'content', label: '内容'),
ShortGridColumn(columnName: 'action', label: '操作', width: 90),
],
selectionMode: SelectionMode.multiple,
navigationMode: GridNavigationMode.cell,
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
allowSorting: false,
allowFiltering: false,
columnWidthMode: ColumnWidthMode.fill,
isScrollbarAlwaysShown: true,
allowColumnsResizing: true, //
columnResizeMode: ColumnResizeMode.onResizeEnd,
selectable: false,
);
}
Widget _buildCountGrid(ContentSearchController controller) {
return SfDataGrid(
rowHeight: 32,
headerRowHeight: 32,
source: CountDataSource(controller),
Widget _buildCountGrid(ContentSearchController controller, BuildContext context) {
return MySfDataGrid<CountResult>(
source: CountDataSource(controller, context),
columns: [
ShortGridColumn(columnName: 'index', label: '序号'),
MyGridColumn(columnName: 'keyword', label: '关键词', minimumWidth: 300),
MyGridColumn(columnName: 'count', label: '匹配数量'),
MyGridColumn(columnName: 'matchCount', label: '匹配数量'),
],
selectionMode: SelectionMode.multiple,
navigationMode: GridNavigationMode.cell,
allowColumnsResizing: true,
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
columnWidthMode: ColumnWidthMode.fill,
isScrollbarAlwaysShown: true,
selectable: false,
);
}
}
class LocateDataSource extends DataGridSource {
class LocateDataSource extends MySfDataSource<SearchResult> {
final ContentSearchController controller;
final BuildContext context;
LocateDataSource(this.controller, this.context);
LocateDataSource(this.controller, this.context)
: super(controller.results, onSelectionChanged: (index, isSelected) {});
@override
List<DataGridRow> get rows =>
@ -182,7 +96,7 @@ class LocateDataSource extends DataGridSource { @@ -182,7 +96,7 @@ class LocateDataSource extends DataGridSource {
}).toList();
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
DataGridRowAdapter buildRow(DataGridRow row) {
final cells = row.getCells();
final result = cells[2].value as SearchResult;
@ -357,11 +271,13 @@ class LocateDataSource extends DataGridSource { @@ -357,11 +271,13 @@ class LocateDataSource extends DataGridSource {
}
}
class CountDataSource extends DataGridSource {
class CountDataSource extends MySfDataSource<CountResult> {
final ContentSearchController controller;
final BuildContext context;
late final List<MapEntry<String, int>> _counts;
CountDataSource(this.controller) {
CountDataSource(this.controller, this.context)
: super([], onSelectionChanged: (index, isSelected) {}) {
final counts = <String, int>{};
for (var result in controller.results) {
counts[result.filePath] = (counts[result.filePath] ?? 0) + result.lineNumber;
@ -383,7 +299,7 @@ class CountDataSource extends DataGridSource { @@ -383,7 +299,7 @@ class CountDataSource extends DataGridSource {
}).toList();
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
DataGridRowAdapter buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells:
row.getCells().map((cell) {
@ -397,7 +313,6 @@ class CountDataSource extends DataGridSource { @@ -397,7 +313,6 @@ class CountDataSource extends DataGridSource {
}
}
// Action标识类
class _ConfirmAction extends Intent {
const _ConfirmAction();
}

4
win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart

@ -42,7 +42,9 @@ class DataExtractController extends BaseContentController { @@ -42,7 +42,9 @@ class DataExtractController extends BaseContentController {
_results.addAll(newResults);
} catch (e) {
Logger().error("提取目录出错:$e");
_results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Extraction failed: $e'));
_results.add(
SearchResult(rowNum: 1, filePath: 'Error', content: 'Extraction failed: $e', matchCount: 0),
);
} finally {
_isExtracting = false;
notifyListeners();

24
win_text_editor/lib/modules/data_extract/models/search_result.dart

@ -3,6 +3,26 @@ class SearchResult { @@ -3,6 +3,26 @@ class SearchResult {
final int rowNum;
final String filePath;
final String content;
final int matchCount;
SearchResult({required this.rowNum, required this.filePath, required this.content});
}
SearchResult({
required this.rowNum,
required this.filePath,
required this.content,
required this.matchCount,
});
SearchResult copyWith({
int? rowNum,
String? filePath,
String? content,
int? matchCount,
}) {
return SearchResult(
rowNum: rowNum ?? this.rowNum,
filePath: filePath ?? this.filePath,
content: content ?? this.content,
matchCount: matchCount ?? this.matchCount,
);
}
}

3
win_text_editor/lib/modules/data_extract/models/xml_rule.dart

@ -5,11 +5,14 @@ class XmlRule { @@ -5,11 +5,14 @@ class XmlRule {
final bool isFirstOccurrence;
final String? namespacePrefix;
final bool isDuplicatesOnly;
XmlRule({
required this.nodePath,
required this.attributeName,
this.isFirstOccurrence = false,
this.namespacePrefix,
required this.isDuplicatesOnly,
});
String toxPath() {

86
win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart

@ -21,32 +21,100 @@ class XmlExtractService { @@ -21,32 +21,100 @@ class XmlExtractService {
try {
final fileContent = await entity.readAsString();
final document = xml.XmlDocument.parse(fileContent);
final values = _extractWithRule(document, rule);
for (var value in values) {
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: value));
final fileResults = _extractWithRule(document, rule, entity.path);
// "仅提取重复项"matchCount>1
if (rule.isDuplicatesOnly) {
results.addAll(fileResults.where((r) => r.matchCount > 1));
} else {
results.addAll(fileResults);
}
} catch (e) {
Logger().error('XmlExtractService.extractFromDirectory方法执行出错: $e');
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $e'));
results.add(
SearchResult(
rowNum: rowNum++,
filePath: entity.path,
content: 'Error: $e',
matchCount: 1,
),
);
}
}
}
//
for (int i = 0; i < results.length; i++) {
results[i] = results[i].copyWith(rowNum: i + 1);
}
return results;
}
List<String> _extractWithRule(xml.XmlDocument document, XmlRule rule) {
final nodes = document.findAllElements(rule.nodePath);
//final nodes = SimpleXPath.query(document, rule.toxPath());
List<SearchResult> _extractWithRule(xml.XmlDocument document, XmlRule rule, String filePath) {
final List<xml.XmlElement> nodes = document.findAllElements(rule.nodePath).toList();
if (rule.isFirstOccurrence && nodes.isNotEmpty) {
final attr = nodes.first.getAttribute(rule.attributeName);
return attr != null ? [attr] : [];
return attr != null
? [SearchResult(rowNum: 0, filePath: filePath, content: attr, matchCount: 1)]
: [];
} else if (rule.isDuplicatesOnly) {
return _findDuplicateAttributes(nodes, rule.attributeName, filePath);
} else {
final attributeCounts = <String, int>{};
// Count occurrences of each attribute value
for (final node in nodes) {
final attr = node.getAttribute(rule.attributeName);
if (attr != null) {
attributeCounts[attr] = (attributeCounts[attr] ?? 0) + 1;
}
}
return nodes
.map((node) => node.getAttribute(rule.attributeName))
.where((attr) => attr != null)
.cast<String>()
.map(
(attr) => SearchResult(
rowNum: 0, // Will be updated later
filePath: filePath,
content: attr!,
matchCount: attributeCounts[attr]!,
),
)
.toList();
}
}
List<SearchResult> _findDuplicateAttributes(
List<xml.XmlElement> nodes,
String attributeName,
String filePath,
) {
final attributeCounts = <String, int>{};
final attributeFirstOccurrence = <String, xml.XmlElement>{};
//
for (final node in nodes) {
final attr = node.getAttribute(attributeName);
if (attr != null) {
attributeCounts[attr] = (attributeCounts[attr] ?? 0) + 1;
attributeFirstOccurrence.putIfAbsent(attr, () => node);
}
}
// >1
final duplicateAttributes =
attributeCounts.entries.where((entry) => entry.value > 1).map((entry) {
final attr = entry.key;
return SearchResult(
rowNum: 0, //
filePath: filePath,
content: attr,
matchCount: entry.value,
);
}).toList();
return duplicateAttributes;
}
}

38
win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart

@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; @@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart';
import 'package:win_text_editor/modules/data_extract/models/xml_rule.dart';
import 'package:win_text_editor/shared/components/my_checkbox.dart';
class ConditionSetting extends StatefulWidget {
const ConditionSetting({super.key});
@ -15,7 +14,7 @@ class _ConditionSettingState extends State<ConditionSetting> { @@ -15,7 +14,7 @@ class _ConditionSettingState extends State<ConditionSetting> {
final _nodePathController = TextEditingController();
final _attributeNameController = TextEditingController();
final _namespacePrefixController = TextEditingController();
bool _isFirstOccurrence = false;
int _extractionOption = 0; // 0: all, 1: first occurrence, 2: duplicates only
bool _isExtracting = false;
@override
@ -60,11 +59,35 @@ class _ConditionSettingState extends State<ConditionSetting> { @@ -60,11 +59,35 @@ class _ConditionSettingState extends State<ConditionSetting> {
const SizedBox(height: 12),
MyCheckbox(
title: '仅提取第一个匹配项',
value: _isFirstOccurrence,
onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false),
// Radio buttons for extraction options
Row(
children: [
Radio<int>(
value: 1,
groupValue: _extractionOption,
onChanged: (value) {
setState(() {
_extractionOption = value ?? 0;
});
},
),
const Text('仅提取第一个匹配项'),
const SizedBox(width: 16),
Radio<int>(
value: 2,
groupValue: _extractionOption,
onChanged: (value) {
setState(() {
_extractionOption = value ?? 0;
});
},
),
const Text('仅提取重复项'),
],
),
const SizedBox(height: 16),
//
Row(
@ -102,7 +125,8 @@ class _ConditionSettingState extends State<ConditionSetting> { @@ -102,7 +125,8 @@ class _ConditionSettingState extends State<ConditionSetting> {
XmlRule(
nodePath: nodePath,
attributeName: attributeName,
isFirstOccurrence: _isFirstOccurrence,
isFirstOccurrence: _extractionOption == 1,
isDuplicatesOnly: _extractionOption == 2,
namespacePrefix:
_namespacePrefixController.text.trim().isNotEmpty
? _namespacePrefixController.text.trim()

9
win_text_editor/lib/modules/data_extract/widgets/results_view.dart

@ -106,6 +106,7 @@ class ResultsView extends StatelessWidget { @@ -106,6 +106,7 @@ class ResultsView extends StatelessWidget {
ShortGridColumn(columnName: 'rowNum', label: '序号'),
MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300),
MyGridColumn(columnName: 'content', label: '内容'),
ShortGridColumn(columnName: 'matchCount', label: '匹配次数'),
],
selectionMode: SelectionMode.multiple,
navigationMode: GridNavigationMode.cell,
@ -134,6 +135,7 @@ class LocateDataSource extends DataGridSource { @@ -134,6 +135,7 @@ class LocateDataSource extends DataGridSource {
DataGridCell(columnName: 'rowNum', value: result.rowNum),
DataGridCell(columnName: 'file', value: path.basename(result.filePath)),
DataGridCell(columnName: 'content', value: result.content),
DataGridCell(columnName: 'matchCount', value: result.matchCount),
],
);
}).toList();
@ -141,10 +143,11 @@ class LocateDataSource extends DataGridSource { @@ -141,10 +143,11 @@ class LocateDataSource extends DataGridSource {
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells:
row.getCells().map<Widget>((cell) {
cells: row.getCells().map<Widget>((cell) {
return Container(
alignment: Alignment.centerLeft,
alignment: cell.columnName == 'matchCount'
? Alignment.center
: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis),
);

5
win_text_editor/lib/modules/module_router.dart

@ -13,6 +13,8 @@ import 'package:win_text_editor/modules/demo/controllers/demo_controller.dart'; @@ -13,6 +13,8 @@ import 'package:win_text_editor/modules/demo/controllers/demo_controller.dart';
import 'package:win_text_editor/modules/demo/widgets/demo_view.dart';
import 'package:win_text_editor/modules/memory_table/controllers/memory_table_controller.dart';
import 'package:win_text_editor/modules/memory_table/widgets/memory_table_view.dart';
import 'package:win_text_editor/modules/outline/controllers/outline_controller.dart';
import 'package:win_text_editor/modules/outline/widgets/outline_view.dart';
import 'package:win_text_editor/modules/uft_component/controllers/uft_component_controller.dart';
import 'package:win_text_editor/modules/uft_component/widgets/uft_component_view.dart';
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart';
@ -34,6 +36,7 @@ class RouterKey { @@ -34,6 +36,7 @@ class RouterKey {
static const String memoryTable = 'memory_table';
static const String uftComponent = 'uft_component';
static const String callFunction = 'call_function';
static const String outline = 'outline';
static const String demo = 'demo';
}
@ -49,6 +52,7 @@ class ModuleRouter { @@ -49,6 +52,7 @@ class ModuleRouter {
RouterKey.memoryTable: (tab) => MemoryTableController(),
RouterKey.uftComponent: (tab) => UftComponentController(),
RouterKey.callFunction: (tab) => CallFunctionController(),
RouterKey.outline: (tab) => OutlineController(),
RouterKey.demo: (tab) => DemoController(),
};
@ -63,6 +67,7 @@ class ModuleRouter { @@ -63,6 +67,7 @@ class ModuleRouter {
RouterKey.memoryTable: (tab, controller) => MemoryTableView(tabId: tab.id),
RouterKey.uftComponent: (tab, controller) => UftComponentView(tabId: tab.id),
RouterKey.callFunction: (tab, controller) => CallFunctionView(tabId: tab.id),
RouterKey.outline: (tab, controller) => OutlineView(tabId: tab.id),
RouterKey.demo: (tab, controller) => DemoView(tabId: tab.id),
};

13
win_text_editor/lib/modules/outline/controllers/outline_controller.dart

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
import 'package:win_text_editor/shared/base/base_content_controller.dart';
class OutlineController extends BaseContentController {
@override
void onOpenFile(String filePath) {
// TODO: implement onOpenFile
}
@override
void onOpenFolder(String folderPath) {
// TODO: implement onOpenFolder
}
}

226
win_text_editor/lib/modules/outline/controllers/outline_provider.dart

@ -0,0 +1,226 @@ @@ -0,0 +1,226 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/modules/outline/services/outline_service.dart';
class OutlineProvider with ChangeNotifier {
List<OutlineNode> _fileNodes = [];
bool _isLoading = false;
String _searchQuery = '';
String? _currentRootPath; //
bool get isLoading => _isLoading;
bool get hasRoot => _fileNodes.isNotEmpty && _fileNodes[0].isRoot;
// _initOutlineTree调用
OutlineProvider();
String? get rootPath => _currentRootPath;
//
Future<void> setRootPath(String path) async {
_currentRootPath = path;
await _loadRootDirectory();
}
List<OutlineNode> get fileNodes =>
_searchQuery.isEmpty ? _fileNodes : _fileNodes.where((node) => _filterNode(node)).toList();
bool _filterNode(OutlineNode node) {
if (node.name.toLowerCase().contains(_searchQuery.toLowerCase())) {
return true;
}
return node.children.any(_filterNode);
}
void searchOutlines(String query) {
_searchQuery = query;
notifyListeners();
}
void toggleExpand(OutlineNode node) {
node.isExpanded = !node.isExpanded;
notifyListeners();
}
Future<void> pickAndOpenOutline() async {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
//
Logger().info('Outline selected: ${result.files.single.path}');
}
}
Future<void> loadDirectory(String path) async {
_isLoading = true;
notifyListeners();
try {
final directory = Directory(path);
final displayName = await OutlineService.getModuleDisplayName(directory.path);
final rootNode = OutlineNode(
name: displayName ?? directory.path.split(Platform.pathSeparator).last,
path: directory.path,
isDirectory: true,
isRoot: true,
children: await OutlineService.buildOutlineTree(directory.path),
);
_fileNodes = [rootNode];
} catch (e) {
Logger().error('Error loading directory: $e');
_fileNodes = [];
}
_isLoading = false;
notifyListeners();
}
Future<void> _loadRootDirectory() async {
if (_currentRootPath == null) return;
_isLoading = true;
notifyListeners();
try {
final displayName = await OutlineService.getModuleDisplayName(_currentRootPath!);
_fileNodes = [
OutlineNode(
name: displayName ?? _currentRootPath!.split(Platform.pathSeparator).last,
path: _currentRootPath!,
isDirectory: true,
isRoot: true,
depth: 0, // 0
children: [], //
),
];
} catch (e) {
Logger().error('Error loading root directory: $e');
_fileNodes = [];
}
_isLoading = false;
notifyListeners();
}
Future<void> loadRootDirectory(String path) async {
_isLoading = true;
notifyListeners();
try {
final displayName = await OutlineService.getModuleDisplayName(path);
_fileNodes = [
OutlineNode(
name: displayName ?? path.split(Platform.pathSeparator).last,
path: path,
isDirectory: true,
isRoot: true,
children: [], //
),
];
} catch (e) {
Logger().error('Error loading root: $e');
_fileNodes = [];
}
_isLoading = false;
notifyListeners();
}
Future<void> toggleDirectory(OutlineNode dirNode) async {
if (dirNode.children.isEmpty) {
//
_isLoading = true;
notifyListeners();
try {
dirNode.children = await OutlineService.listDirectory(dirNode.path);
dirNode.isExpanded = true;
} catch (e) {
Logger().error('Error loading directory: $e');
dirNode.children = [];
}
_isLoading = false;
notifyListeners();
} else {
//
dirNode.isExpanded = !dirNode.isExpanded;
notifyListeners();
}
}
Future<void> loadDirectoryContents(OutlineNode dirNode) async {
if (dirNode.children.isNotEmpty && dirNode.isExpanded) {
//
dirNode.isExpanded = !dirNode.isExpanded;
notifyListeners();
return;
}
_isLoading = true;
notifyListeners();
try {
final contents = await OutlineService.listDirectory(dirNode.path, parentDepth: dirNode.depth);
final updatedNode = dirNode.copyWith(children: contents, isExpanded: true);
_replaceNodeInTree(dirNode, updatedNode);
} catch (e) {
Logger().error('Error loading directory contents: $e');
final updatedNode = dirNode.copyWith(children: []);
_replaceNodeInTree(dirNode, updatedNode);
}
_isLoading = false;
notifyListeners();
}
void _replaceNodeInTree(OutlineNode oldNode, OutlineNode newNode) {
for (int i = 0; i < _fileNodes.length; i++) {
if (_fileNodes[i] == oldNode) {
_fileNodes[i] = newNode;
return;
}
_replaceNodeInChildren(_fileNodes[i], oldNode, newNode);
}
}
void _replaceNodeInChildren(OutlineNode parent, OutlineNode oldNode, OutlineNode newNode) {
for (int i = 0; i < parent.children.length; i++) {
if (parent.children[i] == oldNode) {
parent.children[i] = newNode;
return;
}
_replaceNodeInChildren(parent.children[i], oldNode, newNode);
}
}
Future<void> refreshOutlineTree({bool loadContent = false}) async {
_isLoading = true;
notifyListeners();
try {
final rootDir = await getApplicationDocumentsDirectory();
_fileNodes = [
OutlineNode(
name: rootDir.path.split(Platform.pathSeparator).last,
path: rootDir.path,
isDirectory: true,
isRoot: true,
//
children: loadContent ? await OutlineService.listDirectory(rootDir.path) : [],
),
];
} catch (e) {
Logger().error('Error refreshing file tree: $e');
_fileNodes = [];
}
_isLoading = false;
notifyListeners();
}
}

137
win_text_editor/lib/modules/outline/models/outline_node.dart

@ -0,0 +1,137 @@ @@ -0,0 +1,137 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:win_text_editor/shared/components/tree_view.dart';
class OutlineNode implements TreeNode {
@override
final String name;
final String path;
@override
final bool isDirectory;
final bool isRoot;
@override
final int depth;
@override
List<OutlineNode> children;
@override
bool isExpanded;
OutlineNode({
required this.name,
required this.path,
required this.isDirectory,
this.isRoot = false,
this.depth = 0,
this.isExpanded = false,
List<OutlineNode>? children,
}) : children = children ?? [];
@override
String get id => path;
//
@override
IconData get iconData {
if (isDirectory) {
return Icons.folder;
}
final ext = name.split('.').last.toLowerCase();
//
switch (ext) {
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.article;
case 'xls':
case 'xlsx':
return Icons.table_chart;
case 'ppt':
case 'pptx':
return Icons.slideshow;
case 'txt':
return Icons.text_snippet;
case 'dart':
return Icons.code;
case 'js':
return Icons.javascript;
case 'java':
return Icons.coffee;
case 'py':
return Icons.data_object;
case 'html':
return Icons.html;
case 'css':
return Icons.css;
case 'json':
return Icons.data_array;
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
return Icons.image;
case 'mp3':
case 'wav':
return Icons.audiotrack;
case 'mp4':
case 'avi':
case 'mov':
return Icons.videocam;
case 'zip':
case 'rar':
case '7z':
return Icons.archive;
default:
return Icons.insert_drive_file;
}
}
//
Widget get icon {
return Icon(iconData, color: isDirectory ? Colors.amber[700] : Colors.blue);
}
OutlineNode copyWith({
String? name,
String? path,
bool? isDirectory,
bool? isExpanded,
bool? isRoot,
List<OutlineNode>? children,
int? depth,
}) {
return OutlineNode(
name: name ?? this.name,
path: path ?? this.path,
isDirectory: isDirectory ?? this.isDirectory,
isExpanded: isExpanded ?? this.isExpanded,
isRoot: isRoot ?? this.isRoot,
children: children ?? this.children,
depth: depth ?? this.depth,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is OutlineNode &&
other.name == name &&
other.path == path &&
other.isDirectory == isDirectory &&
other.isRoot == isRoot &&
listEquals(other.children, children) &&
other.isExpanded == isExpanded;
}
@override
int get hashCode {
return name.hashCode ^
path.hashCode ^
isDirectory.hashCode ^
isRoot.hashCode ^
children.hashCode ^
isExpanded.hashCode;
}
}

128
win_text_editor/lib/modules/outline/services/outline_service.dart

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
import 'dart:io';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/services/fast_xml_parser.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:xml/xml.dart';
class OutlineService {
static const _specialExtensions = [
'.uftfunction',
'.uftservice',
'.uftatomfunction',
'.uftatomservice',
'.uftfactorfunction',
'.uftfactorservice',
];
static const Map<String, String> _uftFloders = {
'.settings': '项目设置',
'metadata': '元数据',
'tools': '工具资源',
'uftatom': 'UFT原子',
'uftbusiness': 'UFT业务逻辑',
'uftfactor': 'UFT因子',
'uftstructure': 'UFT对象',
};
static const _hiddenFiles = ['.classpath', '.project', '.respath', 'project.xml', 'module.xml'];
static Future<String?> getSpecialFileName(String filePath) async {
final extension = filePath.substring(filePath.lastIndexOf('.'));
if (!_specialExtensions.contains(extension)) {
return null;
}
try {
final result = await FastXmlParser.parse(filePath);
return ('[${result['objectId']}]${result['chineseName']}');
} catch (e) {
Logger().debug('Error reading special file: $e');
}
return null;
}
///
static Future<List<OutlineNode>> listDirectory(String path, {int parentDepth = 0}) async {
final dir = Directory(path);
final List<FileSystemEntity> entities = await dir.list().toList();
final List<OutlineNode> nodes = [];
// final stopwatch = Stopwatch()..start();
for (final entity in entities) {
final pathName = entity.path.split(Platform.pathSeparator).last;
if (_hiddenFiles.contains(pathName)) continue;
final isDirectory = await FileSystemEntity.isDirectory(entity.path);
final displayName =
isDirectory
? await getModuleDisplayName(entity.path)
: await getSpecialFileName(entity.path);
nodes.add(
OutlineNode(
name: displayName ?? pathName,
path: entity.path,
isDirectory: isDirectory,
depth: parentDepth + 1,
),
);
}
// stopwatch.stop();
// Logger().debug('执行耗时: ${stopwatch.elapsedMilliseconds} 毫秒 (ms)');
return nodes;
}
static Future<String?> getModuleDisplayName(String dirPath) async {
try {
final floderName = dirPath.split(Platform.pathSeparator).last;
if (_uftFloders.containsKey(floderName)) return _uftFloders[floderName];
final moduleFile = File('$dirPath${Platform.pathSeparator}module.xml');
if (await moduleFile.exists()) {
final content = await moduleFile.readAsString();
final xmlDoc = XmlDocument.parse(content);
final infoNode = xmlDoc.findAllElements('info').firstOrNull;
return infoNode?.getAttribute('cname');
}
} catch (e) {
Logger().debug('Error reading module.xml: $e');
}
return null;
}
///
static Future<List<OutlineNode>> buildOutlineTree(String rootPath) async {
final rootDirectory = Directory(rootPath);
final List<OutlineNode> nodes = [];
if (await rootDirectory.exists()) {
final entities = rootDirectory.listSync();
for (final entity in entities) {
final pathName = entity.path.split(Platform.pathSeparator).last;
if (_hiddenFiles.contains(pathName)) continue;
final node = OutlineNode(
name: pathName,
path: entity.path,
isDirectory: entity is Directory,
);
if (entity is Directory) {
node.children.addAll(await buildOutlineTree(entity.path));
}
nodes.add(node);
}
}
return nodes;
}
static Future<String> readFile(String filePath) async {
return await File(filePath).readAsString();
}
static Future<void> writeFile(String filePath, String content) async {
await File(filePath).writeAsString(content);
}
}

85
win_text_editor/lib/modules/outline/widgets/outline_explorer.dart

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/modules/outline/controllers/outline_provider.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/components/tree_view.dart';
class OutlineExplorer extends StatefulWidget {
final Function(String)? onFileDoubleTap;
final Function(String)? onFolderDoubleTap;
const OutlineExplorer({super.key, this.onFileDoubleTap, this.onFolderDoubleTap});
@override
State<OutlineExplorer> createState() => _OutlineExplorerState();
}
class _OutlineExplorerState extends State<OutlineExplorer> {
final ScrollController _scrollController = ScrollController(); // ScrollController
@override
void dispose() {
_scrollController.dispose(); // controller
super.dispose();
}
//
double calculateTotalWidth(BuildContext context, OutlineProvider fileProvider) {
final maxDepth = _getMaxDepth(fileProvider.fileNodes);
return maxDepth * 60 + MediaQuery.of(context).size.width * 0.2;
}
int _getMaxDepth(List<OutlineNode> nodes) {
int maxDepth = 0;
for (final node in nodes) {
if (node.isDirectory && node.isExpanded) {
maxDepth = max(maxDepth, _getMaxDepth(node.children) + 1);
}
}
return maxDepth;
}
@override
Widget build(BuildContext context) {
final fileProvider = Provider.of<OutlineProvider>(context);
return Scrollbar(
controller: _scrollController, // controller
thumbVisibility: true,
child: SingleChildScrollView(
controller: _scrollController, // 使controller
scrollDirection: Axis.horizontal,
child: Container(
color: Colors.white,
child: SizedBox(
width: calculateTotalWidth(context, fileProvider),
child: TreeView(
nodes: fileProvider.fileNodes,
config: const TreeViewConfig(showIcons: true, lazyLoad: true),
onNodeTap: (node) => _handleNodeTap(context, node as OutlineNode),
onNodeDoubleTap: (node) => _handleNodeDoubleTap(node as OutlineNode),
),
),
),
),
);
}
Future<void> _handleNodeTap(BuildContext context, OutlineNode node) async {
final fileProvider = Provider.of<OutlineProvider>(context, listen: false);
if (node.isDirectory) {
await fileProvider.loadDirectoryContents(node);
}
}
void _handleNodeDoubleTap(TreeNode node) {
final fileNode = node as OutlineNode;
if (fileNode.isDirectory && widget.onFolderDoubleTap != null) {
widget.onFolderDoubleTap!(fileNode.path);
} else if (!fileNode.isDirectory && widget.onFileDoubleTap != null) {
widget.onFileDoubleTap!(fileNode.path);
}
}
}

73
win_text_editor/lib/modules/outline/widgets/outline_view.dart

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/modules/outline/controllers/outline_controller.dart';
import 'package:win_text_editor/modules/outline/controllers/outline_provider.dart'; //
import 'package:win_text_editor/modules/outline/widgets/outline_explorer.dart';
class OutlineView extends StatefulWidget {
final String tabId;
const OutlineView({super.key, required this.tabId});
@override
State<OutlineView> createState() => _OutlineViewState();
}
class _OutlineViewState extends State<OutlineView> {
late final OutlineController _controller;
late final OutlineProvider _outlineProvider; // OutlineProvider实例
bool _isControllerFromTabManager = false;
get tabManager => Provider.of<TabItemsController>(context, listen: false);
@override
void initState() {
super.initState();
_outlineProvider = OutlineProvider(); // OutlineProvider
final controllerFromManager = tabManager.getController(widget.tabId);
if (controllerFromManager != null) {
_controller = controllerFromManager;
_isControllerFromTabManager = true;
} else {
_controller = OutlineController();
_isControllerFromTabManager = false;
tabManager.registerController(widget.tabId, _controller);
}
}
@override
void dispose() {
if (!_isControllerFromTabManager) {
_controller.dispose();
}
_outlineProvider.dispose(); // provider
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<OutlineProvider>.value(
value: _outlineProvider, // OutlineProvider
child: Row(
children: [
const VerticalDivider(width: 1),
SizedBox(
width: 300,
child: OutlineExplorer(
onFileDoubleTap: (path) {
//
},
onFolderDoubleTap: (path) {
//
},
),
),
const VerticalDivider(width: 1),
const Expanded(child: Center(child: Text('demo'))),
],
),
);
}
}

52
win_text_editor/lib/modules/template_parser/widgets/grid_view.dart

@ -124,7 +124,6 @@ class TemplateGridView extends StatelessWidget { @@ -124,7 +124,6 @@ class TemplateGridView extends StatelessWidget {
return MyGridColumn(
columnName: node.path,
label: node.isAttribute ? node.name.substring(1) : node.name,
allowEditing: true, //
);
}).toList(),
];
@ -136,7 +135,6 @@ class TemplateGridView extends StatelessWidget { @@ -136,7 +135,6 @@ class TemplateGridView extends StatelessWidget {
columns: columns,
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
columnWidthMode: ColumnWidthMode.fill,
allowColumnsResizing: true,
allowEditing: true, //
editingGestureType: EditingGestureType.tap, //
@ -214,54 +212,4 @@ class _TemplateItemDataSource extends DataGridSource { @@ -214,54 +212,4 @@ class _TemplateItemDataSource extends DataGridSource {
}).toList(),
);
}
// 1:
@override
Future<void> onCellSubmit(
DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex,
GridColumn column,
) async {
final dynamic newValue = dataGridRow.getCells()[rowColumnIndex.columnIndex].value;
final int dataRowIndex = _rows.indexWhere(
(row) => row['_index'] == dataGridRow.getCells()[0].value,
);
if (dataRowIndex >= 0) {
final node = selectedNodes[rowColumnIndex.columnIndex - 1]; //
_rows[dataRowIndex][node.path] = newValue;
if (onCellValueChanged != null) {
onCellValueChanged!(node, column.columnName, newValue);
}
notifyListeners();
}
}
// 2:
@override
Widget? buildEditWidget(
DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex,
GridColumn column,
CellSubmit submitCell,
) {
//
if (column.columnName == 'index') return null;
final String value = dataGridRow.getCells()[rowColumnIndex.columnIndex].value.toString();
return Container(
padding: const EdgeInsets.all(8.0),
child: TextField(
autofocus: true,
controller: TextEditingController(text: value),
onSubmitted: (String newValue) {
// submitCell调用方式
submitCell();
},
),
);
}
}

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

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
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/selectable_data_source.dart';
import 'package:win_text_editor/shared/base/my_sf_data_source.dart';
class ComponentSource extends SelectableDataSource<UftComponent> {
class ComponentSource extends MySfDataSource<UftComponent> {
ComponentSource(
List<UftComponent> uftComponents, {
required Null Function(dynamic index, dynamic isSelected) onSelectionChanged,

87
win_text_editor/lib/shared/base/my_sf_data_grid.dart

@ -2,18 +2,23 @@ import 'package:flutter/material.dart'; @@ -2,18 +2,23 @@ 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';
import 'package:win_text_editor/shared/base/my_sf_data_source.dart';
// ignore: must_be_immutable
class MySfDataGrid<T extends SelectableItem> extends StatelessWidget {
final MySfDataSource<T> dataSource;
final MySfDataSource<T> source;
final Function(int index, bool isSelected)? onSelectionChanged;
final List<GridColumn> columns;
final DataGridController? controller;
bool selectable = false;
const MySfDataGrid({
MySfDataGrid({
super.key,
required this.dataSource,
required this.source,
required this.columns,
this.onSelectionChanged,
this.controller,
this.selectable = false,
});
Widget _buildCheckboxHeader(BuildContext context, SelectableDataSource<T> dataSource) {
@ -52,35 +57,37 @@ class MySfDataGrid<T extends SelectableItem> extends StatelessWidget { @@ -52,35 +57,37 @@ class MySfDataGrid<T extends SelectableItem> extends StatelessWidget {
child: SfDataGrid(
rowHeight: 32,
headerRowHeight: 32,
source: dataSource,
source: source,
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
columnWidthMode: ColumnWidthMode.fitByCellValue,
selectionMode: SelectionMode.none,
allowEditing: true,
allowColumnsResizing: true, //
columnResizeMode: ColumnResizeMode.onResizeEnd, //
controller: controller,
columns: [
GridColumn(
columnName: 'select',
label: ValueListenableBuilder<bool>(
valueListenable: dataSource.selectionNotifier,
builder: (context, _, __) => _buildCheckboxHeader(context, dataSource),
if (selectable)
GridColumn(
columnName: 'select',
label: ValueListenableBuilder<bool>(
valueListenable: source.selectionNotifier,
builder: (context, _, __) => _buildCheckboxHeader(context, source),
),
width: 60,
),
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);
if (rowIndex >= 0 && rowIndex < source.items.length) {
source.toggleRowSelection(rowIndex, !source.items[rowIndex].isSelected);
onSelectionChanged?.call(rowIndex, source.items[rowIndex].isSelected);
}
}
},
onColumnResizeUpdate: (ColumnResizeUpdateDetails details) {
print('${details.column.columnName} 宽度变为 ${details.width}');
return true; // true接受调整
},
onCellSecondaryTap: (details) {
final rowIndex = details.rowColumnIndex.rowIndex - 1;
final columnName = details.column.columnName;
@ -98,33 +105,43 @@ class MySfDataGrid<T extends SelectableItem> extends StatelessWidget { @@ -98,33 +105,43 @@ class MySfDataGrid<T extends SelectableItem> extends StatelessWidget {
position.dy + renderBox.size.height - position.dy,
),
items: [
const PopupMenuItem<String>(value: 'export', child: Text('导出表格(csv)')),
if (!isHeader)
PopupMenuItem(value: 'copyCell', child: Text('复制单元格 ($columnName)')),
PopupMenuItem<String>(
value: 'copyCell',
child: Text('复制单元格 ($columnName)'),
),
if (!isHeader)
const PopupMenuItem(value: 'copyRow', child: Text('复制当前行')),
PopupMenuItem(value: 'copyColumn', child: Text('复制整列 ($columnName)')),
const PopupMenuItem<String>(value: 'copyRow', child: Text('复制当前行')),
PopupMenuItem<String>(
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);
final row = source.effectiveRows[rowIndex];
await source.copyCellValue(row, columnName);
break;
case 'copyRow':
final row = dataSource.effectiveRows[rowIndex];
await dataSource.copyRowValues(row);
final row = source.effectiveRows[rowIndex];
await source.copyRowValues(row);
break;
case 'copyColumn':
await dataSource.copyColumnValues(
dataSource.effectiveRows,
columnName,
);
await source.copyColumnValues(source.effectiveRows, columnName);
break;
case 'export':
await source.exportToCsv(source.effectiveRows);
break;
}
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已复制到剪贴板')));
if (value != 'export') {
ScaffoldMessenger.of(
// ignore: use_build_context_synchronously
context,
).showSnackBar(const SnackBar(content: Text('已复制到剪贴板')));
}
}
});
},

37
win_text_editor/lib/shared/components/my_sf_data_source.dart → win_text_editor/lib/shared/base/my_sf_data_source.dart

@ -1,3 +1,6 @@ @@ -1,3 +1,6 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/shared/base/selectable_data_source.dart';
@ -40,4 +43,38 @@ abstract class MySfDataSource<T extends SelectableItem> extends SelectableDataSo @@ -40,4 +43,38 @@ abstract class MySfDataSource<T extends SelectableItem> extends SelectableDataSo
.join('\n');
await Clipboard.setData(ClipboardData(text: values));
}
Future<void> exportToCsv(List<DataGridRow> rows) async {
if (rows.length <= 1) return;
//
String csvData = "";
for (int i = 1; i < rows[0].getCells().length; i++) {
csvData += rows[0].getCells()[i].columnName.toString();
if (i < rows[0].getCells().length - 1) csvData += '\t';
}
csvData += '\n';
//
for (final row in rows) {
// 10
for (int i = 1; i < row.getCells().length; i++) {
csvData += row.getCells()[i].value.toString();
if (i < row.getCells().length - 1) csvData += '\t';
}
csvData += '\n';
}
//
final filePath = await FilePicker.platform.saveFile(
dialogTitle: '保存导出结果',
fileName: 'template_results.csv',
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (filePath != null) {
await File(filePath).writeAsString(csvData);
}
}
}

4
win_text_editor/lib/shared/components/file_explorer.dart

@ -2,9 +2,9 @@ import 'dart:math'; @@ -2,9 +2,9 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/file_provider.dart';
import 'package:win_text_editor/framework/models/file_node.dart';
import '../../framework/models/file_node.dart';
import '../../framework/controllers/file_provider.dart';
import 'tree_view.dart';
class FileExplorer extends StatefulWidget {

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

@ -7,7 +7,6 @@ class MyGridColumn extends GridColumn { @@ -7,7 +7,6 @@ class MyGridColumn extends GridColumn {
double minimumWidth = 100,
double maximumWidth = double.infinity,
required String label,
bool allowEditing = true,
}) : super(
columnName: columnName,
minimumWidth: minimumWidth,
@ -18,7 +17,6 @@ class MyGridColumn extends GridColumn { @@ -18,7 +17,6 @@ class MyGridColumn extends GridColumn {
padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
),
allowEditing: allowEditing,
);
}

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

@ -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/components/my_sf_data_source.dart';
import 'package:win_text_editor/shared/base/my_sf_data_source.dart';
import 'package:win_text_editor/shared/models/std_filed.dart';
class FieldsDataSource extends MySfDataSource<Field> {

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

@ -13,10 +13,11 @@ class FieldsDataGrid extends StatelessWidget { @@ -13,10 +13,11 @@ class FieldsDataGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MySfDataGrid<Field>(
dataSource: fieldsSource,
source: fieldsSource,
onSelectionChanged: onSelectionChanged,
selectable: true,
columns: [
MyGridColumn(columnName: 'id', label: '序号', minimumWidth: 80),
ShortGridColumn(columnName: 'id', label: '序号'),
MyGridColumn(columnName: 'name', label: '名称', minimumWidth: 120),
MyGridColumn(columnName: 'chineseName', label: '中文名', minimumWidth: 120),
MyGridColumn(columnName: 'type', label: '类型', minimumWidth: 120),

Loading…
Cancel
Save