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 @@ @@ -1,5 +1,6 @@
import 'package:file_picker/file_picker.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:xml/xml.dart' as xml;
import 'dart:io';
@ -9,106 +10,128 @@ class TemplateParserController extends BaseContentController { @@ -9,106 +10,128 @@ class TemplateParserController extends BaseContentController {
List<TemplateNode> _treeNodes = [];
List<TemplateItem> _templateItems = [];
String? _errorMessage;
TemplateNode? _selectedNode;
// Getters
String get filePath => _filePath;
List<TemplateNode> get treeNodes => _treeNodes;
List<TemplateItem> get templateItems => _templateItems;
String? get errorMessage => _errorMessage;
TemplateNode? get selectedNode => _selectedNode;
Future<void> pickFile() async {
final result = await FilePicker.platform.pickFiles();
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['xml'],
);
if (result != null) {
_filePath = result.files.single.path!;
await _loadTemplateData();
notifyListeners();
}
}
void setFilePath(String path) {
Future<void> setFilePath(String path) async {
_filePath = path;
_loadTemplateData();
notifyListeners();
await _loadTemplateData();
}
void selectTreeNode(TemplateNode node) {
_templateItems = List.generate(
10,
(index) => TemplateItem(id: index + 1, content: 'Content for ${node.name} item ${index + 1}'),
);
_selectedNode = node;
_templateItems = _generateTemplateItems(node);
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 {
_errorMessage = null;
_treeNodes = [];
_templateItems = [];
try {
_errorMessage = null;
_treeNodes = [];
_templateItems = [];
_selectedNode = null;
if (_filePath.isEmpty) return;
if (_filePath.isEmpty) return;
try {
final file = File(_filePath);
final content = await file.readAsString();
await _parseXmlContent(content);
} catch (e) {
_errorMessage = '格式错误: 不是有效的XML文档';
Logger().error('Failed to parse XML: $e');
_errorMessage = 'Failed to load XML: ${e.toString()}';
Logger().error('XML加载错误$_errorMessage');
} finally {
notifyListeners();
}
notifyListeners();
}
Future<void> _parseXmlContent(String xmlContent) async {
try {
final document = xml.XmlDocument.parse(xmlContent);
_treeNodes = _buildTreeNodes(document.rootElement);
} on xml.XmlParserException catch (e) {
throw Exception('XML解析错误: ${e.message}');
}
final document = xml.XmlDocument.parse(xmlContent);
_treeNodes = _buildTreeNodes(document.rootElement, depth: 0);
}
List<TemplateNode> _buildTreeNodes(xml.XmlElement element) {
return [
TemplateNode(
element.name.local,
element.children
.whereType<xml.XmlElement>()
.map((e) => _buildTreeNodes(e))
.expand((nodes) => nodes)
.toList(),
attributes: element.attributes.fold(
{},
(map, attr) => map!..[attr.name.local] = attr.value,
),
text: element.text,
List<TemplateNode> _buildTreeNodes(xml.XmlElement element, {required int depth}) {
final node = TemplateNode(
name: element.name.local,
children: [],
attributes:
element.attributes.isNotEmpty
? {for (var attr in element.attributes) attr.name.local: attr.value}
: null,
text: element.text.trim().isNotEmpty ? element.text : null,
depth: depth,
isExpanded: depth < 1, //
);
//
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
void onOpenFile(String filePath) {
Logger().info('File selected: $filePath');
setFilePath(filePath);
}
@override
void dispose() {
_treeNodes.clear();
_templateItems.clear();
super.dispose();
}
@override
void onOpenFolder(String folderPath) {
// 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 @@ @@ -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'; @@ -2,6 +2,8 @@ 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/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 {
final String tabId;
@ -92,58 +94,100 @@ class _TemplateParserViewState extends State<TemplateParserView> { @@ -92,58 +94,100 @@ class _TemplateParserViewState extends State<TemplateParserView> {
return const Center(child: Text('No XML data available'));
}
return ListView.builder(
itemCount: nodes.length,
itemBuilder: (context, index) {
return _buildTreeNode(nodes[index]);
return TreeView(
nodes: _processXmlNodes(nodes),
config: const TreeViewConfig(
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) {
return ExpansionTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(node.name),
if (node.attributes != null && node.attributes!.isNotEmpty)
Text(
node.attributes!.entries.map((e) => '${e.key}="${e.value}"').join(' '),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
if (node.text != null && node.text!.trim().isNotEmpty)
Text(
'Text: ${node.text!.trim()}',
style: const TextStyle(fontSize: 12, color: Colors.blue),
),
],
List<TreeNode> _processXmlNodes(List<TemplateNode> nodes) {
final uniqueNodes = <String, TemplateNode>{};
final result = <TreeNode>[];
for (final node in nodes) {
// +
final signature = '${node.name}:${node.attributes?.keys.join(',') ?? ''}';
if (!uniqueNodes.containsKey(signature)) {
uniqueNodes[signature] = node;
result.add(node);
} else {
//
uniqueNodes[signature]!.isRepeated = true;
}
}
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) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 5,
),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text('ID: ${item.id}'), Text('Content: ${item.content}')],
),
return Consumer<TemplateParserController>(
builder: (context, controller, _) {
// /
final filteredItems =
controller.selectedNode != null
? items.where((item) => item.matches(controller.selectedNode!))
: items;
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 5,
),
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 { @@ -19,15 +19,7 @@ class FileExplorer extends StatefulWidget {
}
class _FileExplorerState extends State<FileExplorer> {
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
@override
void dispose() {
_verticalScrollController.dispose();
_horizontalScrollController.dispose();
super.dispose();
}
// ScrollController
Future<void> _promptForDirectory(BuildContext context) async {
final fileProvider = Provider.of<FileProvider>(context, listen: false);
@ -79,26 +71,13 @@ class _FileExplorerState extends State<FileExplorer> { @@ -79,26 +71,13 @@ class _FileExplorerState extends State<FileExplorer> {
? const Center(child: CircularProgressIndicator())
: fileProvider.fileNodes.isEmpty
? Center(child: _buildEmptyPrompt(context))
: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _horizontalScrollController,
child: Scrollbar(
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),
),
),
),
: 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 { @@ -13,12 +13,13 @@ abstract class TreeNode {
///
class TreeViewConfig {
final bool lazyLoad; //
final bool singleSelect; //
final bool showCheckboxes; //
final bool showIcons; //
final Color? selectedColor; //
final double indentWidth; //
final bool lazyLoad;
final bool singleSelect;
final bool showCheckboxes;
final bool showIcons;
final Color? selectedColor;
final double indentWidth;
final Map<String, IconData> icons; // final
const TreeViewConfig({
this.lazyLoad = false,
@ -27,6 +28,7 @@ class TreeViewConfig { @@ -27,6 +28,7 @@ class TreeViewConfig {
this.showIcons = true,
this.selectedColor,
this.indentWidth = 24.0,
this.icons = const {}, // Map作为默认值
});
}
@ -37,6 +39,8 @@ class TreeView extends StatefulWidget { @@ -37,6 +39,8 @@ class TreeView extends StatefulWidget {
final Function(TreeNode)? onNodeTap;
final Function(TreeNode)? onNodeDoubleTap;
final Function(TreeNode, bool?)? onNodeCheckChanged;
final Widget Function(BuildContext, TreeNode, bool, VoidCallback)? nodeBuilder; //
final ScrollController? scrollController;
const TreeView({
super.key,
@ -45,6 +49,8 @@ class TreeView extends StatefulWidget { @@ -45,6 +49,8 @@ class TreeView extends StatefulWidget {
this.onNodeTap,
this.onNodeDoubleTap,
this.onNodeCheckChanged,
this.nodeBuilder,
this.scrollController,
});
@override
@ -54,25 +60,61 @@ class TreeView extends StatefulWidget { @@ -54,25 +60,61 @@ class TreeView extends StatefulWidget {
class _TreeViewState extends State<TreeView> {
final Set<String> _selectedIds = {};
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
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: _countVisibleNodes(widget.nodes),
itemBuilder: (context, index) {
final node = _getVisibleNode(widget.nodes, index);
return _TreeNodeWidget(
node: node,
config: widget.config,
isSelected: _selectedIds.contains(node.id),
isChecked: _checkedIds.contains(node.id),
onTap: () => _handleNodeTap(node),
onDoubleTap: () => widget.onNodeDoubleTap?.call(node),
onCheckChanged: (value) => _handleNodeCheckChanged(node, value),
);
},
return Scrollbar(
controller: _effectiveController,
thumbVisibility: true,
notificationPredicate: (_) => true, //
child: ListView.builder(
controller: _effectiveController,
physics: const ClampingScrollPhysics(),
itemCount: _countVisibleNodes(widget.nodes),
itemBuilder: (context, index) {
final node = _getVisibleNode(widget.nodes, index);
final isSelected = _selectedIds.contains(node.id);
// 使
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> { @@ -127,7 +169,7 @@ class _TreeViewState extends State<TreeView> {
}
}
class _TreeNodeWidget extends StatelessWidget {
class TreeNodeWidget extends StatelessWidget {
final TreeNode node;
final TreeViewConfig config;
final bool isSelected;
@ -136,7 +178,7 @@ class _TreeNodeWidget extends StatelessWidget { @@ -136,7 +178,7 @@ class _TreeNodeWidget extends StatelessWidget {
final VoidCallback onDoubleTap;
final Function(bool?)? onCheckChanged;
const _TreeNodeWidget({
const TreeNodeWidget({
required this.node,
required this.config,
required this.isSelected,

Loading…
Cancel
Save