diff --git a/win_text_editor/assets/config/uft_macro_list.yaml b/win_text_editor/assets/config/uft_macro_list.yaml index 14409ea..318a404 100644 --- a/win_text_editor/assets/config/uft_macro_list.yaml +++ b/win_text_editor/assets/config/uft_macro_list.yaml @@ -91,7 +91,7 @@ templates: 遍历记录: body: | - [遍历记录开始][{{tableName}}({{selectIndexOrKey.name}})][ + [遍历记录开始][{{tableName}}({{selectIndexOrKey.name}})][ {{#selectIndexOrKey.fields}} {{name}} = @{{name}} {{^isLast}}, {{/isLast}} {{/selectIndexOrKey.fields}} diff --git a/win_text_editor/lib/menus/app_menu.dart b/win_text_editor/lib/menus/app_menu.dart index 9417afd..42f76e7 100644 --- a/win_text_editor/lib/menus/app_menu.dart +++ b/win_text_editor/lib/menus/app_menu.dart @@ -72,6 +72,11 @@ class AppMenu extends StatelessWidget { value: MenuConstants.callFunction, child: ListTile(leading: Icon(Icons.functions), title: Text('功能号调用')), ), + const PopupMenuDivider(), + const PopupMenuItem( + value: MenuConstants.outline, + child: ListTile(leading: Icon(Icons.outlined_flag_rounded), title: Text('大纲')), + ), ]; } diff --git a/win_text_editor/lib/menus/menu_actions.dart b/win_text_editor/lib/menus/menu_actions.dart index eb68df3..1d511f1 100644 --- a/win_text_editor/lib/menus/menu_actions.dart +++ b/win_text_editor/lib/menus/menu_actions.dart @@ -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 { await _openOrActivateTab(context, "Demo", RouterKey.demo, Icons.code); } + static Future _outline(BuildContext context) async { + await _openOrActivateTab(context, "大纲", RouterKey.outline, Icons.outlined_flag_rounded); + } + static Future _openOrActivateTab( BuildContext context, String title, diff --git a/win_text_editor/lib/menus/menu_constants.dart b/win_text_editor/lib/menus/menu_constants.dart index 7745db9..3df8a8d 100644 --- a/win_text_editor/lib/menus/menu_constants.dart +++ b/win_text_editor/lib/menus/menu_constants.dart @@ -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'; diff --git a/win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart b/win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart index c4044e6..1f423b2 100644 --- a/win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart +++ b/win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart @@ -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(); diff --git a/win_text_editor/lib/modules/data_extract/models/search_result.dart b/win_text_editor/lib/modules/data_extract/models/search_result.dart index b6a6ea9..2d8302a 100644 --- a/win_text_editor/lib/modules/data_extract/models/search_result.dart +++ b/win_text_editor/lib/modules/data_extract/models/search_result.dart @@ -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, + ); + } +} \ No newline at end of file diff --git a/win_text_editor/lib/modules/data_extract/models/xml_rule.dart b/win_text_editor/lib/modules/data_extract/models/xml_rule.dart index 106e221..2788f7f 100644 --- a/win_text_editor/lib/modules/data_extract/models/xml_rule.dart +++ b/win_text_editor/lib/modules/data_extract/models/xml_rule.dart @@ -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() { diff --git a/win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart b/win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart index 36938d3..6cfcd61 100644 --- a/win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart +++ b/win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart @@ -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 _extractWithRule(xml.XmlDocument document, XmlRule rule) { - final nodes = document.findAllElements(rule.nodePath); - //final nodes = SimpleXPath.query(document, rule.toxPath()); + List _extractWithRule(xml.XmlDocument document, XmlRule rule, String filePath) { + final List 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 = {}; + // 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() + .map( + (attr) => SearchResult( + rowNum: 0, // Will be updated later + filePath: filePath, + content: attr!, + matchCount: attributeCounts[attr]!, + ), + ) .toList(); } } + + List _findDuplicateAttributes( + List nodes, + String attributeName, + String filePath, + ) { + final attributeCounts = {}; + final attributeFirstOccurrence = {}; + + // 第一次遍历:统计每个属性值的出现次数,并记录第一次出现的节点 + 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; + } } diff --git a/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart b/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart index ed29241..315c63e 100644 --- a/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart +++ b/win_text_editor/lib/modules/data_extract/widgets/condition_setting.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 { 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 { const SizedBox(height: 12), - MyCheckbox( - title: '仅提取第一个匹配项', - value: _isFirstOccurrence, - onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false), + // Radio buttons for extraction options + Row( + children: [ + Radio( + value: 1, + groupValue: _extractionOption, + onChanged: (value) { + setState(() { + _extractionOption = value ?? 0; + }); + }, + ), + const Text('仅提取第一个匹配项'), + + const SizedBox(width: 16), + + Radio( + 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 { XmlRule( nodePath: nodePath, attributeName: attributeName, - isFirstOccurrence: _isFirstOccurrence, + isFirstOccurrence: _extractionOption == 1, + isDuplicatesOnly: _extractionOption == 2, namespacePrefix: _namespacePrefixController.text.trim().isNotEmpty ? _namespacePrefixController.text.trim() diff --git a/win_text_editor/lib/modules/data_extract/widgets/results_view.dart b/win_text_editor/lib/modules/data_extract/widgets/results_view.dart index f7e63a5..776ed74 100644 --- a/win_text_editor/lib/modules/data_extract/widgets/results_view.dart +++ b/win_text_editor/lib/modules/data_extract/widgets/results_view.dart @@ -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 { 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 { @override DataGridRowAdapter? buildRow(DataGridRow row) { return DataGridRowAdapter( - cells: - row.getCells().map((cell) { + cells: row.getCells().map((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), ); diff --git a/win_text_editor/lib/modules/module_router.dart b/win_text_editor/lib/modules/module_router.dart index b0c986e..1762b5e 100644 --- a/win_text_editor/lib/modules/module_router.dart +++ b/win_text_editor/lib/modules/module_router.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 { 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 { 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 { 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), }; diff --git a/win_text_editor/lib/modules/outline/controllers/outline_controller.dart b/win_text_editor/lib/modules/outline/controllers/outline_controller.dart new file mode 100644 index 0000000..a31a98e --- /dev/null +++ b/win_text_editor/lib/modules/outline/controllers/outline_controller.dart @@ -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 + } +} diff --git a/win_text_editor/lib/modules/outline/controllers/outline_provider.dart b/win_text_editor/lib/modules/outline/controllers/outline_provider.dart new file mode 100644 index 0000000..edbfd24 --- /dev/null +++ b/win_text_editor/lib/modules/outline/controllers/outline_provider.dart @@ -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 _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 setRootPath(String path) async { + _currentRootPath = path; + await _loadRootDirectory(); + } + + List 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 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 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 _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 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 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 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 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(); + } +} diff --git a/win_text_editor/lib/modules/outline/models/outline_node.dart b/win_text_editor/lib/modules/outline/models/outline_node.dart new file mode 100644 index 0000000..0a3215f --- /dev/null +++ b/win_text_editor/lib/modules/outline/models/outline_node.dart @@ -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 children; + @override + bool isExpanded; + + OutlineNode({ + required this.name, + required this.path, + required this.isDirectory, + this.isRoot = false, + this.depth = 0, + this.isExpanded = false, + List? 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? 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; + } +} diff --git a/win_text_editor/lib/modules/outline/services/outline_service.dart b/win_text_editor/lib/modules/outline/services/outline_service.dart new file mode 100644 index 0000000..a0d9728 --- /dev/null +++ b/win_text_editor/lib/modules/outline/services/outline_service.dart @@ -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 _uftFloders = { + '.settings': '项目设置', + 'metadata': '元数据', + 'tools': '工具资源', + 'uftatom': 'UFT原子', + 'uftbusiness': 'UFT业务逻辑', + 'uftfactor': 'UFT因子', + 'uftstructure': 'UFT对象', + }; + static const _hiddenFiles = ['.classpath', '.project', '.respath', 'project.xml', 'module.xml']; + + static Future 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> listDirectory(String path, {int parentDepth = 0}) async { + final dir = Directory(path); + final List entities = await dir.list().toList(); + final List 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 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> buildOutlineTree(String rootPath) async { + final rootDirectory = Directory(rootPath); + final List 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 readFile(String filePath) async { + return await File(filePath).readAsString(); + } + + static Future writeFile(String filePath, String content) async { + await File(filePath).writeAsString(content); + } +} diff --git a/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart b/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart new file mode 100644 index 0000000..7458106 --- /dev/null +++ b/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart @@ -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 createState() => _OutlineExplorerState(); +} + +class _OutlineExplorerState extends State { + 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 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(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 _handleNodeTap(BuildContext context, OutlineNode node) async { + final fileProvider = Provider.of(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); + } + } +} diff --git a/win_text_editor/lib/modules/outline/widgets/outline_view.dart b/win_text_editor/lib/modules/outline/widgets/outline_view.dart new file mode 100644 index 0000000..8f03509 --- /dev/null +++ b/win_text_editor/lib/modules/outline/widgets/outline_view.dart @@ -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 createState() => _OutlineViewState(); +} + +class _OutlineViewState extends State { + late final OutlineController _controller; + late final OutlineProvider _outlineProvider; // 新增OutlineProvider实例 + bool _isControllerFromTabManager = false; + + get tabManager => Provider.of(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.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'))), + ], + ), + ); + } +} diff --git a/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart b/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart index 36f31e6..16c84ee 100644 --- a/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart +++ b/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart @@ -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 { columns: columns, gridLinesVisibility: GridLinesVisibility.both, headerGridLinesVisibility: GridLinesVisibility.both, - columnWidthMode: ColumnWidthMode.fill, allowColumnsResizing: true, allowEditing: true, // 启用编辑 editingGestureType: EditingGestureType.tap, // 点击编辑 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 index 3e703c9..1bb1bd7 100644 --- a/win_text_editor/lib/shared/base/my_sf_data_grid.dart +++ b/win_text_editor/lib/shared/base/my_sf_data_grid.dart @@ -60,8 +60,8 @@ class MySfDataGrid extends StatelessWidget { source: source, gridLinesVisibility: GridLinesVisibility.both, headerGridLinesVisibility: GridLinesVisibility.both, - columnWidthMode: ColumnWidthMode.fitByCellValue, - selectionMode: SelectionMode.none, + allowColumnsResizing: true, // 启用列宽调整 + columnResizeMode: ColumnResizeMode.onResizeEnd, // 推荐模式 controller: controller, columns: [ if (selectable) @@ -84,6 +84,10 @@ class MySfDataGrid extends StatelessWidget { } } }, + 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; diff --git a/win_text_editor/lib/shared/components/file_explorer.dart b/win_text_editor/lib/shared/components/file_explorer.dart index 08ee963..2bcbf6a 100644 --- a/win_text_editor/lib/shared/components/file_explorer.dart +++ b/win_text_editor/lib/shared/components/file_explorer.dart @@ -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 { 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 8b23338..7760827 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,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 { padding: const EdgeInsets.all(2.0), child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), ), - allowEditing: allowEditing, ); } 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 8906f2e..eaee3c9 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 @@ -17,7 +17,7 @@ class FieldsDataGrid extends StatelessWidget { 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),