Browse Source

滚动条错误已修复

master
hejl 2 months ago
parent
commit
8a31ad033d
  1. 135
      win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart
  2. 73
      win_text_editor/lib/modules/template_parser/models/template_node.dart
  3. 128
      win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart
  4. 37
      win_text_editor/lib/shared/components/file_explorer.dart
  5. 90
      win_text_editor/lib/shared/components/tree_view.dart

135
win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart

@ -1,5 +1,6 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/template_parser/models/template_node.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
import 'dart:io'; import 'dart:io';
@ -9,106 +10,128 @@ class TemplateParserController extends BaseContentController {
List<TemplateNode> _treeNodes = []; List<TemplateNode> _treeNodes = [];
List<TemplateItem> _templateItems = []; List<TemplateItem> _templateItems = [];
String? _errorMessage; String? _errorMessage;
TemplateNode? _selectedNode;
// Getters // Getters
String get filePath => _filePath; String get filePath => _filePath;
List<TemplateNode> get treeNodes => _treeNodes; List<TemplateNode> get treeNodes => _treeNodes;
List<TemplateItem> get templateItems => _templateItems; List<TemplateItem> get templateItems => _templateItems;
String? get errorMessage => _errorMessage; String? get errorMessage => _errorMessage;
TemplateNode? get selectedNode => _selectedNode;
Future<void> pickFile() async { Future<void> pickFile() async {
final result = await FilePicker.platform.pickFiles(); final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['xml'],
);
if (result != null) { if (result != null) {
_filePath = result.files.single.path!; _filePath = result.files.single.path!;
await _loadTemplateData(); await _loadTemplateData();
notifyListeners();
} }
} }
void setFilePath(String path) { Future<void> setFilePath(String path) async {
_filePath = path; _filePath = path;
_loadTemplateData(); await _loadTemplateData();
notifyListeners();
} }
void selectTreeNode(TemplateNode node) { void selectTreeNode(TemplateNode node) {
_templateItems = List.generate( _selectedNode = node;
10, _templateItems = _generateTemplateItems(node);
(index) => TemplateItem(id: index + 1, content: 'Content for ${node.name} item ${index + 1}'),
);
notifyListeners(); notifyListeners();
} }
List<TemplateItem> _generateTemplateItems(TemplateNode node) {
// XML解析真实数据
return [
if (node.attributes != null)
...node.attributes!.entries.map(
(e) => TemplateItem(
id: e.key.hashCode,
content: '${node.name}.${e.key}',
xPath: '${node.name}@${e.key}',
value: e.value,
),
),
if (node.text != null && node.text!.trim().isNotEmpty)
TemplateItem(
id: node.text.hashCode,
content: node.text!,
xPath: '${node.name}/text()',
value: node.text!,
),
];
}
Future<void> _loadTemplateData() async { Future<void> _loadTemplateData() async {
_errorMessage = null; try {
_treeNodes = []; _errorMessage = null;
_templateItems = []; _treeNodes = [];
_templateItems = [];
_selectedNode = null;
if (_filePath.isEmpty) return; if (_filePath.isEmpty) return;
try {
final file = File(_filePath); final file = File(_filePath);
final content = await file.readAsString(); final content = await file.readAsString();
await _parseXmlContent(content); await _parseXmlContent(content);
} catch (e) { } catch (e) {
_errorMessage = '格式错误: 不是有效的XML文档'; _errorMessage = 'Failed to load XML: ${e.toString()}';
Logger().error('Failed to parse XML: $e'); Logger().error('XML加载错误$_errorMessage');
} finally {
notifyListeners();
} }
notifyListeners();
} }
Future<void> _parseXmlContent(String xmlContent) async { Future<void> _parseXmlContent(String xmlContent) async {
try { final document = xml.XmlDocument.parse(xmlContent);
final document = xml.XmlDocument.parse(xmlContent); _treeNodes = _buildTreeNodes(document.rootElement, depth: 0);
_treeNodes = _buildTreeNodes(document.rootElement);
} on xml.XmlParserException catch (e) {
throw Exception('XML解析错误: ${e.message}');
}
} }
List<TemplateNode> _buildTreeNodes(xml.XmlElement element) { List<TemplateNode> _buildTreeNodes(xml.XmlElement element, {required int depth}) {
return [ final node = TemplateNode(
TemplateNode( name: element.name.local,
element.name.local, children: [],
element.children attributes:
.whereType<xml.XmlElement>() element.attributes.isNotEmpty
.map((e) => _buildTreeNodes(e)) ? {for (var attr in element.attributes) attr.name.local: attr.value}
.expand((nodes) => nodes) : null,
.toList(), text: element.text.trim().isNotEmpty ? element.text : null,
attributes: element.attributes.fold( depth: depth,
{}, isExpanded: depth < 1, //
(map, attr) => map!..[attr.name.local] = attr.value, );
),
text: element.text, //
node.children.addAll(
element.children.whereType<xml.XmlElement>().map(
(e) => _buildTreeNodes(e, depth: depth + 1).first,
), ),
]; );
//
if (element.attributes.isNotEmpty) {
node.children.addAll(
element.attributes.map((attr) => TemplateNode.attribute(attr.name.local, attr.value)),
);
}
return [node];
} }
@override @override
void onOpenFile(String filePath) { void onOpenFile(String filePath) {
Logger().info('File selected: $filePath');
setFilePath(filePath); setFilePath(filePath);
} }
@override
void dispose() {
_treeNodes.clear();
_templateItems.clear();
super.dispose();
}
@override @override
void onOpenFolder(String folderPath) { void onOpenFolder(String folderPath) {
// TODO: implement onOpenFolder // TODO: implement onOpenFolder
} }
} }
class TemplateNode {
final String name;
final List<TemplateNode> children;
final Map<String, String>? attributes;
final String? text;
TemplateNode(this.name, this.children, {this.attributes, this.text});
}
class TemplateItem {
final int id;
final String content;
TemplateItem({required this.id, required this.content});
}

73
win_text_editor/lib/modules/template_parser/models/template_node.dart

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/shared/components/tree_view.dart';
class TemplateNode implements TreeNode {
@override
final String id; // 使ID
@override
final String name;
@override
final List<TemplateNode> children;
final Map<String, String>? attributes;
final String? text;
@override
final int depth; //
@override
bool isExpanded; //
bool isRepeated; //
@override
bool get isDirectory => children.isNotEmpty; //
@override
IconData? get iconData {
if (name.startsWith('@')) return Icons.code; //
return isDirectory ? Icons.folder : Icons.insert_drive_file; //
}
TemplateNode({
required this.name,
required this.children,
this.attributes,
this.text,
this.depth = 0,
this.isExpanded = false,
this.isRepeated = false,
String? id,
}) : id = id ?? '${depth}_${name}'; // ID生成逻辑
//
TemplateNode.attribute(String name, String value)
: this(name: '@$name', children: const [], attributes: {name: value}, depth: 1);
//
TemplateNode copyWith({int? depth, bool? isExpanded}) {
return TemplateNode(
name: name,
children: children,
attributes: attributes,
text: text,
depth: depth ?? this.depth,
isExpanded: isExpanded ?? this.isExpanded,
id: id,
);
}
}
class TemplateItem {
final int id;
final String content;
final String xPath;
final String value;
TemplateItem({required this.id, required this.content, required this.xPath, required this.value});
bool matches(TemplateNode node) {
if (node.name.startsWith('@')) {
final attrName = node.name.substring(1);
return xPath.contains('@$attrName');
}
return xPath.contains(node.name);
}
}

128
win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.dart'; import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.dart';
import 'package:win_text_editor/modules/template_parser/models/template_node.dart';
import 'package:win_text_editor/shared/components/tree_view.dart';
class TemplateParserView extends StatefulWidget { class TemplateParserView extends StatefulWidget {
final String tabId; final String tabId;
@ -92,58 +94,100 @@ class _TemplateParserViewState extends State<TemplateParserView> {
return const Center(child: Text('No XML data available')); return const Center(child: Text('No XML data available'));
} }
return ListView.builder( return TreeView(
itemCount: nodes.length, nodes: _processXmlNodes(nodes),
itemBuilder: (context, index) { config: const TreeViewConfig(
return _buildTreeNode(nodes[index]); showIcons: true,
singleSelect: true,
selectedColor: Colors.lightBlueAccent,
icons: {'element': Icons.label_outline, 'attribute': Icons.code},
),
onNodeTap: (node) {
final templateNode = node as TemplateNode;
Provider.of<TemplateParserController>(context, listen: false).selectTreeNode(templateNode);
}, },
nodeBuilder: (context, node, isSelected, onTap) => _buildCustomNode(node),
); );
} }
Widget _buildTreeNode(TemplateNode node) { List<TreeNode> _processXmlNodes(List<TemplateNode> nodes) {
return ExpansionTile( final uniqueNodes = <String, TemplateNode>{};
title: Column( final result = <TreeNode>[];
crossAxisAlignment: CrossAxisAlignment.start,
children: [ for (final node in nodes) {
Text(node.name), // +
if (node.attributes != null && node.attributes!.isNotEmpty) final signature = '${node.name}:${node.attributes?.keys.join(',') ?? ''}';
Text(
node.attributes!.entries.map((e) => '${e.key}="${e.value}"').join(' '), if (!uniqueNodes.containsKey(signature)) {
style: const TextStyle(fontSize: 12, color: Colors.grey), uniqueNodes[signature] = node;
), result.add(node);
if (node.text != null && node.text!.trim().isNotEmpty) } else {
Text( //
'Text: ${node.text!.trim()}', uniqueNodes[signature]!.isRepeated = true;
style: const TextStyle(fontSize: 12, color: Colors.blue), }
), }
], return result.cast<TreeNode>();
}
Widget _buildCustomNode(TreeNode node) {
final templateNode = node as TemplateNode;
final isAttribute = node.depth > 0 && node.name.startsWith('@');
return Padding(
padding: EdgeInsets.only(left: 12.0 * node.depth), //
child: ListTile(
dense: true,
leading:
isAttribute
? const Icon(Icons.code, size: 16, color: Colors.grey)
: const Icon(Icons.label_outline, size: 18, color: Colors.blue),
title: Text(
isAttribute ? templateNode.name.substring(1) : templateNode.name,
style: TextStyle(
color: isAttribute ? Colors.grey[600] : Colors.black,
fontWeight: isAttribute ? FontWeight.normal : FontWeight.w500,
),
),
trailing:
templateNode.isRepeated
? const Text("(repeated)", style: TextStyle(color: Colors.grey))
: null,
onTap:
() => Provider.of<TemplateParserController>(
context,
listen: false,
).selectTreeNode(templateNode),
), ),
children: node.children.map((child) => _buildTreeNode(child)).toList(),
onExpansionChanged: (expanded) {
if (expanded) {
Provider.of<TemplateParserController>(context, listen: false).selectTreeNode(node);
}
},
); );
} }
Widget _buildGridView(List<TemplateItem> items) { Widget _buildGridView(List<TemplateItem> items) {
return GridView.builder( return Consumer<TemplateParserController>(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( builder: (context, controller, _) {
crossAxisCount: 2, // /
childAspectRatio: 5, final filteredItems =
), controller.selectedNode != null
itemCount: items.length, ? items.where((item) => item.matches(controller.selectedNode!))
itemBuilder: (context, index) { : items;
final item = items[index];
return Card( return GridView.builder(
child: Padding( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
padding: const EdgeInsets.all(8.0), crossAxisCount: 2,
child: Column( childAspectRatio: 5,
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text('ID: ${item.id}'), Text('Content: ${item.content}')],
),
), ),
itemCount: filteredItems.length,
itemBuilder: (context, index) {
final item = filteredItems.elementAt(index);
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text('Path: ${item.xPath}'), Text('Value: ${item.value}')],
),
),
);
},
); );
}, },
); );

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

@ -19,15 +19,7 @@ class FileExplorer extends StatefulWidget {
} }
class _FileExplorerState extends State<FileExplorer> { class _FileExplorerState extends State<FileExplorer> {
final ScrollController _verticalScrollController = ScrollController(); // ScrollController
final ScrollController _horizontalScrollController = ScrollController();
@override
void dispose() {
_verticalScrollController.dispose();
_horizontalScrollController.dispose();
super.dispose();
}
Future<void> _promptForDirectory(BuildContext context) async { Future<void> _promptForDirectory(BuildContext context) async {
final fileProvider = Provider.of<FileProvider>(context, listen: false); final fileProvider = Provider.of<FileProvider>(context, listen: false);
@ -79,26 +71,13 @@ class _FileExplorerState extends State<FileExplorer> {
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: fileProvider.fileNodes.isEmpty : fileProvider.fileNodes.isEmpty
? Center(child: _buildEmptyPrompt(context)) ? Center(child: _buildEmptyPrompt(context))
: Scrollbar( : SizedBox(
controller: _verticalScrollController, width: calculateTotalWidth(context, fileProvider),
thumbVisibility: true, child: TreeView(
child: SingleChildScrollView( nodes: fileProvider.fileNodes,
scrollDirection: Axis.horizontal, config: const TreeViewConfig(showIcons: true, lazyLoad: true),
controller: _horizontalScrollController, onNodeTap: (node) => _handleNodeTap(context, node as FileNode),
child: Scrollbar( onNodeDoubleTap: (node) => _handleNodeDoubleTap(node as FileNode),
controller: _horizontalScrollController,
thumbVisibility: true,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SizedBox(
width: calculateTotalWidth(context, fileProvider),
child: TreeView(
nodes: fileProvider.fileNodes,
config: const TreeViewConfig(showIcons: true, lazyLoad: true),
onNodeTap: (node) => _handleNodeTap(context, node as FileNode),
onNodeDoubleTap: (node) => _handleNodeDoubleTap(node as FileNode),
),
),
),
), ),
), ),
), ),

90
win_text_editor/lib/shared/components/tree_view.dart

@ -13,12 +13,13 @@ abstract class TreeNode {
/// ///
class TreeViewConfig { class TreeViewConfig {
final bool lazyLoad; // final bool lazyLoad;
final bool singleSelect; // final bool singleSelect;
final bool showCheckboxes; // final bool showCheckboxes;
final bool showIcons; // final bool showIcons;
final Color? selectedColor; // final Color? selectedColor;
final double indentWidth; // final double indentWidth;
final Map<String, IconData> icons; // final
const TreeViewConfig({ const TreeViewConfig({
this.lazyLoad = false, this.lazyLoad = false,
@ -27,6 +28,7 @@ class TreeViewConfig {
this.showIcons = true, this.showIcons = true,
this.selectedColor, this.selectedColor,
this.indentWidth = 24.0, this.indentWidth = 24.0,
this.icons = const {}, // Map作为默认值
}); });
} }
@ -37,6 +39,8 @@ class TreeView extends StatefulWidget {
final Function(TreeNode)? onNodeTap; final Function(TreeNode)? onNodeTap;
final Function(TreeNode)? onNodeDoubleTap; final Function(TreeNode)? onNodeDoubleTap;
final Function(TreeNode, bool?)? onNodeCheckChanged; final Function(TreeNode, bool?)? onNodeCheckChanged;
final Widget Function(BuildContext, TreeNode, bool, VoidCallback)? nodeBuilder; //
final ScrollController? scrollController;
const TreeView({ const TreeView({
super.key, super.key,
@ -45,6 +49,8 @@ class TreeView extends StatefulWidget {
this.onNodeTap, this.onNodeTap,
this.onNodeDoubleTap, this.onNodeDoubleTap,
this.onNodeCheckChanged, this.onNodeCheckChanged,
this.nodeBuilder,
this.scrollController,
}); });
@override @override
@ -54,25 +60,61 @@ class TreeView extends StatefulWidget {
class _TreeViewState extends State<TreeView> { class _TreeViewState extends State<TreeView> {
final Set<String> _selectedIds = {}; final Set<String> _selectedIds = {};
final Set<String> _checkedIds = {}; final Set<String> _checkedIds = {};
late ScrollController _effectiveController;
@override
void initState() {
super.initState();
_effectiveController = widget.scrollController ?? ScrollController();
}
@override
void didUpdateWidget(TreeView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.scrollController != oldWidget.scrollController) {
if (oldWidget.scrollController == null) {
_effectiveController.dispose();
}
_effectiveController = widget.scrollController ?? ScrollController();
}
}
@override
void dispose() {
if (widget.scrollController == null) {
_effectiveController.dispose();
}
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.builder( return Scrollbar(
shrinkWrap: true, controller: _effectiveController,
physics: const ClampingScrollPhysics(), thumbVisibility: true,
itemCount: _countVisibleNodes(widget.nodes), notificationPredicate: (_) => true, //
itemBuilder: (context, index) { child: ListView.builder(
final node = _getVisibleNode(widget.nodes, index); controller: _effectiveController,
return _TreeNodeWidget( physics: const ClampingScrollPhysics(),
node: node, itemCount: _countVisibleNodes(widget.nodes),
config: widget.config, itemBuilder: (context, index) {
isSelected: _selectedIds.contains(node.id), final node = _getVisibleNode(widget.nodes, index);
isChecked: _checkedIds.contains(node.id), final isSelected = _selectedIds.contains(node.id);
onTap: () => _handleNodeTap(node),
onDoubleTap: () => widget.onNodeDoubleTap?.call(node), // 使
onCheckChanged: (value) => _handleNodeCheckChanged(node, value), return widget.nodeBuilder != null
); ? widget.nodeBuilder!(context, node, isSelected, () => _handleNodeTap(node))
}, : TreeNodeWidget(
node: node,
config: widget.config,
isSelected: isSelected,
isChecked: _checkedIds.contains(node.id),
onTap: () => _handleNodeTap(node),
onDoubleTap: () => widget.onNodeDoubleTap?.call(node),
onCheckChanged: (value) => _handleNodeCheckChanged(node, value),
);
},
),
); );
} }
@ -127,7 +169,7 @@ class _TreeViewState extends State<TreeView> {
} }
} }
class _TreeNodeWidget extends StatelessWidget { class TreeNodeWidget extends StatelessWidget {
final TreeNode node; final TreeNode node;
final TreeViewConfig config; final TreeViewConfig config;
final bool isSelected; final bool isSelected;
@ -136,7 +178,7 @@ class _TreeNodeWidget extends StatelessWidget {
final VoidCallback onDoubleTap; final VoidCallback onDoubleTap;
final Function(bool?)? onCheckChanged; final Function(bool?)? onCheckChanged;
const _TreeNodeWidget({ const TreeNodeWidget({
required this.node, required this.node,
required this.config, required this.config,
required this.isSelected, required this.isSelected,

Loading…
Cancel
Save