Initial commit

This commit is contained in:
luckyrat 2021-07-07 10:14:27 +01:00
commit 6d8b63618b
26 changed files with 4424 additions and 0 deletions

75
.gitignore vendored Normal file
View file

@ -0,0 +1,75 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

10
.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 91f8d3da32cdbec4780bd9a449550f7c03465a47
channel: master
project_type: package

144
CHANGELOG.md Normal file
View file

@ -0,0 +1,144 @@
## [1.0.3+2]
* Removed unnecessary asserts
* Removed null safety check on TreeView scroll physics property
## [1.0.3+1]
* Null check bug fixes
## [1.0.2+1]
* Bug fixes
### Updated
* Fixes: Null check operator used on a null value
* Fixes: Node Data not being written when TreeViewController is converted back to a Map
Create issue
## [1.0.1+1]
* Updated internal references
## [1.0.0+1]
### Updated
* Updated project to null-safety
## [0.10.0+1]
### Updated
* Fixed NodeExpander Size issue
* Updated code to use proper hover color between web and mobile
## [0.9.0+1]
### Added
* Added expandSpeed property to TreeViewTheme to control the speed in which nodes are animated.
* Added node builder to TreeView to allow custom display of node data. Builder function accepts build context and Node as parameters.
* Added expandAll, collapseAll, withExpandAll and withCollapseAll to TreeViewController
### Updated
* Refactored Node class to use IconData for the icon property.
### Removed
* Removed NodeIcon class.
## [0.8.0+1]
### Added
* Added support for labelOverflow and parentLabelOverflow. Thanks to Long Ti.
## [0.7.1+1]
### Updated
* Refactored logic to prevent getter 'key' called on null error when calling expandToNode and collapseToNode functions.
## [0.7.0+1]
### Added
* Added support for vertical and horizontal spacing. Thanks to Long Ti.
* Added support for padding node icons.
* Added bool parent property to Node class to force node to act as parent.
### Updated
* Updated expander theme to not default to black but instead use the color of the current theme.
* Removed background color from tree nodes that aren't selected
## [0.6.0+1]
### Added
* Added support for importing data property during JSON and Map load
## [0.5.0+1]
### Added
* Added support for using shrinkWrap, primary, and physics property on TreeView
## [0.4.2+1]
### Added
* Added support for using external font packages
## [0.4.1+1]
### Updated
* Updated TreeView widget so that it inherits the ThemeData from context
## [0.4.0+1]
### Added
* Added expandToNode method to TreeViewController to support expanding all nodes down to specified node. Returns List<Node>.
* Added collapseToNode method to TreeViewController to support collapsing all nodes down to specified node. Returns List<Node>.
* Added withExpandToNode method to TreeViewController to support expanding all nodes down to specified node. Returns TreeViewController.
* Added withCollapseToNode method to TreeViewController to support expanding all nodes down to specified node. Returns TreeViewController.
## [0.3.0+1]
### Added
* Added generic data property to Node class to support the use of custom data
## [0.2.0+1]
### Updated
* Added animation controller dispose to TreeNode to prevent memory leaks
### Added
* Added new dense property to TreeViewTheme
* Added new loadJSON and loadMap convenience methods to TreeViewController for data loading
* Added new convenience methods to TreeViewController: toggleNode, withToggleNode, selectedNode
## [0.1.0+2]
### Updated
* Updated links to repository documentation
* Cleaned up warnings
## [0.0.4+1]
### Updated
* Added logic to update TreeNode when expanded programmatically
* Fixed issue with adding new node to a TreeNode with new children
## [0.0.3+7]
### Added
* Added api documentation
### Updated
* Added parentLabelStyle to TreeViewTheme to support separate styling for parent node
## [0.0.2+1]
### Added
* Added ExpanderModifier
### Updated
* Updated open source license
* Simplified ExpanderType
* Refactored TreeNodeExpander class and added animation to icon
* Updated default expander size
### Removed
* Removed custom TreeView font
## [0.0.1]
* Initial package release

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Romain Rastel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

172
README.md Normal file
View file

@ -0,0 +1,172 @@
# flutter_treeview
A hierarchical data widget for your flutter apps.
It offers a number of options for customizing the appearance and handling user interaction.
It also offers some convenience methods for importing data into the tree.
## Features
* Separately customize child and parent labels
* Add any icon to a node
* Choose from four different expander icons and several modifiers for adjusting shape, outline, and fill.
* Import data from a Map
* Includes ability to handle expandChange, tap, and double tap user interactions
* Includes convenience methods for adding, updating and deleting nodes
## Sample Code
### Creating a TreeView
```dart
List<Node> nodes = [
Node(
label: 'Documents',
key: 'docs',
expanded: true,
icon: docsOpen ? Icons.folder_open : Icons.folder,
children: [
Node(
label: 'Job Search',
key: 'd3',
icon: Icons.input,
children: [
Node(
label: 'Resume.docx',
key: 'pd1',
icon: Icons.insert_drive_file,
),
Node(
label: 'Cover Letter.docx',
key: 'pd2',
icon: Icons.insert_drive_file),
]),
Node(
label: 'Inspection.docx',
key: 'd1',
),
Node(
label: 'Invoice.docx',
key: 'd2',
icon: Icons.insert_drive_file),
],
),
Node(
label: 'MeetingReport.xls',
key: 'mrxls',
icon: Icons.insert_drive_file),
Node(
label: 'MeetingReport.pdf',
key: 'mrpdf',
icon: Icons.insert_drive_file),
Node(
label: 'Demo.zip',
key: 'demo',
icon: Icons.archive),
];
TreeViewController _treeViewController = TreeViewController(children: nodes);
TreeView(
controller: _treeViewController,
allowParentSelect: false,
supportParentDoubleTap: false,
onExpansionChanged: _expandNodeHandler,
onNodeTap: (key) {
setState(() {
_treeViewController = _treeViewController.copyWith(selectedKey: key);
});
},
theme: treeViewTheme
),
```
_The TreeView requires that the onExpansionChange property updates the expanded
node so that the tree is rendered properly.
### Creating a theme
```dart
TreeViewTheme _treeViewTheme = TreeViewTheme(
expanderTheme: ExpanderThemeData(
type: ExpanderType.caret,
modifier: ExpanderModifier.none,
position: ExpanderPosition.start,
color: Colors.red.shade800,
size: 20,
),
labelStyle: TextStyle(
fontSize: 16,
letterSpacing: 0.3,
),
parentLabelStyle: TextStyle(
fontSize: 16,
letterSpacing: 0.1,
fontWeight: FontWeight.w800,
color: Colors.red.shade600,
),
iconTheme: IconThemeData(
size: 18,
color: Colors.grey.shade800,
),
colorScheme: ColorScheme.light(),
)
```
### Using custom data
The Node class supports the use of custom data. You can use the data property on the Node instance to store data that you want to easily retrieve.
```dart
class Person {
final String name;
final List<Animal> pets;
Person({this.name, this.pets});
}
class Animal {
final String name;
Animal({this.name});
}
Animal otis = Animal(name: 'Otis');
Animal zorro = Animal(name: 'Zorro');
Person lukas = Person(name: 'Lukas', pets: [otis, zorro]);
List<Node> nodes = [
Node<Person>(
label: 'Lukas',
key: 'lukas',
data: lukas,
children: [
Node<Animal>(
label: 'Otis',
key: 'otis',
data: otis,
),
//<T> is optional but recommended. If not specified, code can return Node<dynamic> instead of Node<Animal>
Node(
label: 'Zorro',
key: 'zorro',
data: zorro,
),
]
),
];
TreeViewController _treeViewController = TreeViewController(children: nodes);
TreeView(
controller: _treeViewController,
onNodeTap: (key) {
Node selectedNode = _treeViewController.getNode(key);
Person selectedModel = selectedNode.data;
},
),
```
## Getting Started
For help getting started with this widget, view our
[online documentation](https://bitbucket.org/kevinandre/flutter_treeview/wiki/Home) or view the
full [API reference](https://pub.dev/documentation/flutter_treeview/latest/).
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

1
builddocs.sh Executable file
View file

@ -0,0 +1 @@
dartdoc --include flutter_treeview ./

12
example/README.md Normal file
View file

@ -0,0 +1,12 @@
# Example
## Example Project
View example code in the [TreeView Test Example](https://bitbucket.org/kevinandre/flutter_treeview_example/src/master/)
## Example Code
There are also some examples in the [unit tests](https://bitbucket.org/kevinandre/flutter_treeview/src/master/test/).
## Screenshots
![Overview](https://bitbucket.org/kevinandre/flutter_treeview/raw/03707dec7b46e18b6d7a68867697bb7251906b75/screenshots/ss1.gif)
![Overview](https://bitbucket.org/kevinandre/flutter_treeview/raw/03707dec7b46e18b6d7a68867697bb7251906b75/screenshots/ss2.png)

View file

@ -0,0 +1,9 @@
library flutter_treeview;
export 'src/expander_theme_data.dart';
export 'src/models/node.dart';
export 'src/tree_node.dart';
export 'src/tree_view.dart';
export 'src/tree_view_controller.dart';
export 'src/tree_view_theme.dart';
export 'src/utilities.dart';

View file

@ -0,0 +1,133 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_treeview/src/tree_view_theme.dart';
const double _kDefaultExpanderSize = 30.0;
/// Defines whether expander icon is shown on the
/// left or right side of the parent node label.
enum ExpanderPosition {
start,
end,
}
/// Defines the type expander icon displayed. All
/// types except the plus-minus type will be animated
enum ExpanderType {
caret,
arrow,
chevron,
plusMinus,
}
/// Defines whether expander icon has a circle or square shape
/// and whether it is outlined or filled.
enum ExpanderModifier {
none,
circleFilled,
circleOutlined,
squareFilled,
squareOutlined,
}
/// Defines the appearance of the expander icons.
///
/// Used by [TreeViewTheme] to control the appearance of the expander icons for a
/// parent tree node in the [TreeView] widget.
class ExpanderThemeData {
/// The [ExpanderPosition] for expander icon.
final ExpanderPosition position;
/// The [ExpanderType] for expander icon.
final ExpanderType type;
/// The size for expander icon.
final double size;
/// The color for expander icon.
final Color? color;
/// The [ExpanderModifier] for expander icon.
final ExpanderModifier modifier;
/// The animation state for expander icon. It determines whether
/// the icon animates when changing states
final bool animated;
const ExpanderThemeData({
this.color,
this.position: ExpanderPosition.start,
this.type: ExpanderType.caret,
this.size: _kDefaultExpanderSize,
this.modifier: ExpanderModifier.none,
this.animated: true,
});
/// Creates an expander icon theme with some reasonable default values.
///
/// The [color] is black,
/// the [position] is [ExpanderPosition.start],
/// the [type] is [ExpanderType.caret],
/// the [modifier] is [ExpanderModifier.none],
/// the [animated] property is true,
/// and the [size] is 30.0.
const ExpanderThemeData.fallback()
: color = const Color(0xFF000000),
position = ExpanderPosition.start,
type = ExpanderType.caret,
modifier = ExpanderModifier.none,
animated = true,
size = _kDefaultExpanderSize;
/// Creates a copy of this theme but with the given fields replaced with
/// the new values.
ExpanderThemeData copyWith({
Color? color,
ExpanderType? type,
ExpanderPosition? position,
ExpanderModifier? modifier,
bool? animated,
double? size,
}) {
return ExpanderThemeData(
color: color ?? this.color,
type: type ?? this.type,
position: position ?? this.position,
modifier: modifier ?? this.modifier,
size: size ?? this.size,
animated: animated ?? this.animated,
);
}
/// Returns a new theme that matches this expander theme but with some values
/// replaced by the non-null parameters of the given icon theme. If the given
/// expander theme is null, simply returns this theme.
ExpanderThemeData merge(ExpanderThemeData? other) {
if (other == null) return this;
return copyWith(
color: other.color,
type: other.type,
position: other.position,
modifier: other.modifier,
animated: other.animated,
size: other.size,
);
}
ExpanderThemeData resolve(BuildContext context) => this;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is ExpanderThemeData &&
other.color == color &&
other.position == position &&
other.type == type &&
other.modifier == modifier &&
other.animated == animated &&
other.size == size;
}
@override
int get hashCode =>
hashValues(color, position, type, size, modifier, animated);
}

179
lib/src/models/node.dart Normal file
View file

@ -0,0 +1,179 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/widgets.dart';
import '../tree_node.dart';
import '../utilities.dart';
/// Defines the data used to display a [TreeNode].
///
/// Used by [TreeView] to display a [TreeNode].
///
/// This object allows the creation of key, label and icon to display
/// a node on the [TreeView] widget. The key and label properties are
/// required. The key is needed for events that occur on the generated
/// [TreeNode]. It should always be unique.
class Node<T> {
/// The unique string that identifies this object.
final String key;
/// The string value that is displayed on the [TreeNode].
final String label;
/// An optional icon that is displayed on the [TreeNode].
final IconData? icon;
/// The open or closed state of the [TreeNode]. Applicable only if the
/// node is a parent
final bool expanded;
/// Generic data model that can be assigned to the [TreeNode]. This makes
/// it useful to assign and retrieve data associated with the [TreeNode]
final T? data;
/// The sub [Node]s of this object.
final List<Node> children;
/// Force the node to be a parent so that node can show expander without
/// having children node.
final bool parent;
const Node({
required this.key,
required this.label,
this.children: const [],
this.expanded: false,
this.parent: false,
this.icon,
this.data,
});
/// Creates a [Node] from a string value. It generates a unique key.
factory Node.fromLabel(String label) {
String _key = Utilities.generateRandom();
return Node(
key: '${_key}_$label',
label: label,
);
}
/// Creates a [Node] from a Map<String, dynamic> map. The map
/// should contain a "label" value. If the key value is
/// missing, it generates a unique key.
/// If the expanded value, if present, can be any 'truthful'
/// value. Excepted values include: 1, yes, true and their
/// associated string values.
factory Node.fromMap(Map<String, dynamic> map) {
String? _key = map['key'];
String _label = map['label'];
var _data = map['data'];
List<Node> _children = [];
if (_key == null) {
_key = Utilities.generateRandom();
}
// if (map['icon'] != null) {
// int _iconData = int.parse(map['icon']);
// if (map['icon'].runtimeType == String) {
// _iconData = int.parse(map['icon']);
// } else if (map['icon'].runtimeType == double) {
// _iconData = (map['icon'] as double).toInt();
// } else {
// _iconData = map['icon'];
// }
// _icon = const IconData(_iconData);
// }
if (map['children'] != null) {
List<Map<String, dynamic>> _childrenMap = List.from(map['children']);
_children = _childrenMap
.map((Map<String, dynamic> child) => Node.fromMap(child))
.toList();
}
return Node(
key: '$_key',
label: _label,
data: _data,
expanded: Utilities.truthful(map['expanded']),
parent: Utilities.truthful(map['parent']),
children: _children,
);
}
/// Creates a copy of this object but with the given fields
/// replaced with the new values.
Node copyWith({
String? key,
String? label,
List<Node>? children,
bool? expanded,
bool? parent,
IconData? icon,
T? data,
}) =>
Node(
key: key ?? this.key,
label: label ?? this.label,
icon: icon ?? this.icon,
expanded: expanded ?? this.expanded,
parent: parent ?? this.parent,
children: children ?? this.children,
data: data ?? this.data,
);
/// Whether this object has children [Node].
bool get isParent => children.isNotEmpty || parent;
/// Whether this object has a non-null icon.
bool get hasIcon => icon != null && icon != null;
/// Whether this object has data associated with it.
bool get hasData => data != null;
/// Map representation of this object
Map<String, dynamic> get asMap {
Map<String, dynamic> _map = {
"key": key,
"label": label,
"icon": icon == null ? null : icon!.codePoint,
"expanded": expanded,
"parent": parent,
"children": children.map((Node child) => child.asMap).toList(),
};
if (data != null) {
_map['data'] = data;
}
//TODO: figure out a means to check for getter or method on generic to include map from generic
return _map;
}
@override
String toString() {
return JsonEncoder().convert(asMap);
}
@override
int get hashCode {
return hashValues(
key,
label,
icon,
expanded,
parent,
children,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is Node &&
other.key == key &&
other.label == label &&
other.icon == icon &&
other.expanded == expanded &&
other.parent == parent &&
other.data.runtimeType == T &&
other.children.length == children.length;
}
}

479
lib/src/tree_node.dart Normal file
View file

@ -0,0 +1,479 @@
import 'dart:math' show pi;
import 'package:flutter/material.dart';
import 'tree_view.dart';
import 'tree_view_theme.dart';
import 'expander_theme_data.dart';
import 'models/node.dart';
const double _kBorderWidth = 0.75;
/// Defines the [TreeNode] widget.
///
/// This widget is used to display a tree node and its children. It requires
/// a single [Node] value. It uses this node to display the state of the
/// widget. It uses the [TreeViewTheme] to handle the appearance and the
/// [TreeView] properties to handle to user actions.
///
/// __This class should not be used directly!__
/// The [TreeView] and [TreeViewController] handlers the data and rendering
/// of the nodes.
class TreeNode extends StatefulWidget {
/// The node object used to display the widget state
final Node node;
const TreeNode({Key? key, required this.node}) : super(key: key);
@override
_TreeNodeState createState() => _TreeNodeState();
}
class _TreeNodeState extends State<TreeNode>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
late AnimationController _controller;
late Animation<double> _heightFactor;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_controller =
AnimationController(duration: Duration(milliseconds: 200), vsync: this);
_heightFactor = _controller.drive(_easeInTween);
_isExpanded = widget.node.expanded;
if (_isExpanded) _controller.value = 1.0;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
TreeView? _treeView = TreeView.of(context);
_controller.duration = _treeView!.theme.expandSpeed;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(TreeNode oldWidget) {
if (widget.node.expanded != oldWidget.node.expanded) {
setState(() {
_isExpanded = widget.node.expanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) return;
setState(() {});
});
}
});
} else if (widget.node != oldWidget.node) {
setState(() {});
}
super.didUpdateWidget(oldWidget);
}
void _handleExpand() {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) return;
setState(() {});
});
}
});
if (_treeView!.onExpansionChanged != null)
_treeView.onExpansionChanged!(widget.node.key, _isExpanded);
}
void _handleTap() {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
if (_treeView!.onNodeTap != null) {
_treeView.onNodeTap!(widget.node.key);
}
}
void _handleDoubleTap() {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
if (_treeView!.onNodeDoubleTap != null) {
_treeView.onNodeDoubleTap!(widget.node.key);
}
}
Widget _buildNodeExpander() {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
TreeViewTheme _theme = _treeView!.theme;
return widget.node.isParent
? GestureDetector(
onTap: () => _handleExpand(),
child: _TreeNodeExpander(
speed: _controller.duration!,
expanded: widget.node.expanded,
themeData: _theme.expanderTheme,
),
)
: Container(width: _theme.expanderTheme.size);
}
Widget _buildNodeIcon() {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
TreeViewTheme _theme = _treeView!.theme;
bool isSelected = _treeView.controller.selectedKey != null &&
_treeView.controller.selectedKey == widget.node.key;
return Container(
alignment: Alignment.center,
width:
widget.node.hasIcon ? _theme.iconTheme.size! + _theme.iconPadding : 0,
child: widget.node.hasIcon
? Icon(
widget.node.icon,
size: _theme.iconTheme.size,
color: isSelected
? _theme.colorScheme.onPrimary
: _theme.iconTheme.color,
)
: null,
);
}
Widget _buildNodeLabel() {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
TreeViewTheme _theme = _treeView!.theme;
bool isSelected = _treeView.controller.selectedKey != null &&
_treeView.controller.selectedKey == widget.node.key;
final icon = _buildNodeIcon();
return Container(
padding: EdgeInsets.symmetric(
vertical: _theme.verticalSpacing ?? (_theme.dense ? 10 : 15),
horizontal: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
icon,
Expanded(
child: Text(
widget.node.label,
softWrap: widget.node.isParent
? _theme.parentLabelOverflow == null
: _theme.labelOverflow == null,
overflow: widget.node.isParent
? _theme.parentLabelOverflow
: _theme.labelOverflow,
style: widget.node.isParent
? _theme.parentLabelStyle.copyWith(
fontWeight: _theme.parentLabelStyle.fontWeight,
color: isSelected
? _theme.colorScheme.onPrimary
: _theme.parentLabelStyle.color,
)
: _theme.labelStyle.copyWith(
fontWeight: _theme.labelStyle.fontWeight,
color: isSelected ? _theme.colorScheme.onPrimary : null,
),
),
),
],
),
);
}
Widget _buildNodeWidget() {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
TreeViewTheme _theme = _treeView!.theme;
bool isSelected = _treeView.controller.selectedKey != null &&
_treeView.controller.selectedKey == widget.node.key;
bool canSelectParent = _treeView.allowParentSelect;
final arrowContainer = _buildNodeExpander();
final labelContainer = _treeView.nodeBuilder != null
? _treeView.nodeBuilder!(context, widget.node)
: _buildNodeLabel();
Widget _tappable = _treeView.onNodeDoubleTap != null
? InkWell(
onTap: _handleTap,
onDoubleTap: _handleDoubleTap,
child: labelContainer,
)
: InkWell(
onTap: _handleTap,
child: labelContainer,
);
if (widget.node.isParent) {
if (_treeView.supportParentDoubleTap && canSelectParent) {
_tappable = InkWell(
onTap: canSelectParent ? _handleTap : _handleExpand,
onDoubleTap: () {
_handleExpand();
_handleDoubleTap();
},
child: labelContainer,
);
} else if (_treeView.supportParentDoubleTap) {
_tappable = InkWell(
onTap: _handleExpand,
onDoubleTap: _handleDoubleTap,
child: labelContainer,
);
} else {
_tappable = InkWell(
onTap: canSelectParent ? _handleTap : _handleExpand,
child: labelContainer,
);
}
}
return Container(
color: isSelected ? _theme.colorScheme.primary : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: _theme.expanderTheme.position == ExpanderPosition.end
? <Widget>[
Expanded(
child: _tappable,
),
arrowContainer,
]
: <Widget>[
arrowContainer,
Expanded(
child: _tappable,
),
],
),
);
}
@override
Widget build(BuildContext context) {
TreeView? _treeView = TreeView.of(context);
assert(_treeView != null, 'TreeView must exist in context');
final bool closed =
(!_isExpanded || !widget.node.expanded) && _controller.isDismissed;
final nodeWidget = _buildNodeWidget();
return widget.node.isParent
? AnimatedBuilder(
animation: _controller.view,
builder: (BuildContext context, Widget? child) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
nodeWidget,
ClipRect(
child: Align(
heightFactor: _heightFactor.value,
child: child,
),
),
],
);
},
child: closed
? null
: Container(
margin: EdgeInsets.only(
left: _treeView!.theme.horizontalSpacing ??
_treeView.theme.iconTheme.size!),
child: Column(
mainAxisSize: MainAxisSize.min,
children: widget.node.children.map((Node node) {
return TreeNode(node: node);
}).toList()),
),
)
: Container(
child: nodeWidget,
);
}
}
class _TreeNodeExpander extends StatefulWidget {
final ExpanderThemeData themeData;
final bool expanded;
final Duration _expandSpeed;
const _TreeNodeExpander({
required Duration speed,
required this.themeData,
required this.expanded,
}) : _expandSpeed = speed;
@override
_TreeNodeExpanderState createState() => _TreeNodeExpanderState();
}
class _TreeNodeExpanderState extends State<_TreeNodeExpander>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
bool isEnd = widget.themeData.position == ExpanderPosition.end;
if (widget.themeData.type != ExpanderType.plusMinus) {
controller = AnimationController(
duration: widget.themeData.animated
? isEnd
? widget._expandSpeed * 0.625
: widget._expandSpeed
: Duration(milliseconds: 0),
vsync: this,
);
animation = Tween<double>(
begin: 0,
end: isEnd ? 180 : 90,
).animate(controller);
} else {
controller =
AnimationController(duration: Duration(milliseconds: 0), vsync: this);
animation = Tween<double>(begin: 0, end: 0).animate(controller);
}
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(_TreeNodeExpander oldWidget) {
if (widget.themeData != oldWidget.themeData ||
widget.expanded != oldWidget.expanded) {
bool isEnd = widget.themeData.position == ExpanderPosition.end;
setState(() {
if (widget.themeData.type != ExpanderType.plusMinus) {
controller.duration = widget.themeData.animated
? isEnd
? widget._expandSpeed * 0.625
: widget._expandSpeed
: Duration(milliseconds: 0);
animation = Tween<double>(
begin: 0,
end: isEnd ? 180 : 90,
).animate(controller);
} else {
controller.duration = Duration(milliseconds: 0);
animation = Tween<double>(begin: 0, end: 0).animate(controller);
}
});
}
super.didUpdateWidget(oldWidget);
}
Color? _onColor(Color? color) {
if (color != null) {
if (color.computeLuminance() > 0.6) {
return Colors.black;
} else {
return Colors.white;
}
}
return null;
}
@override
Widget build(BuildContext context) {
IconData _arrow;
double _iconSize = widget.themeData.size;
double _borderWidth = 0;
BoxShape _shapeBorder = BoxShape.rectangle;
Color _backColor = Colors.transparent;
Color? _iconColor =
widget.themeData.color ?? Theme.of(context).iconTheme.color;
switch (widget.themeData.modifier) {
case ExpanderModifier.none:
break;
case ExpanderModifier.circleFilled:
_shapeBorder = BoxShape.circle;
_backColor = widget.themeData.color ?? Colors.black;
_iconColor = _onColor(_backColor);
break;
case ExpanderModifier.circleOutlined:
_borderWidth = _kBorderWidth;
_shapeBorder = BoxShape.circle;
break;
case ExpanderModifier.squareFilled:
_backColor = widget.themeData.color ?? Colors.black;
_iconColor = _onColor(_backColor);
break;
case ExpanderModifier.squareOutlined:
_borderWidth = _kBorderWidth;
break;
}
switch (widget.themeData.type) {
case ExpanderType.chevron:
_arrow = Icons.expand_more;
break;
case ExpanderType.arrow:
_arrow = Icons.arrow_downward;
_iconSize = widget.themeData.size > 20
? widget.themeData.size - 8
: widget.themeData.size;
break;
case ExpanderType.caret:
_arrow = Icons.arrow_drop_down;
break;
case ExpanderType.plusMinus:
_arrow = widget.expanded ? Icons.remove : Icons.add;
break;
}
Icon _icon = Icon(
_arrow,
size: _iconSize,
color: _iconColor,
);
if (widget.expanded) {
controller.reverse();
} else {
controller.forward();
}
return Container(
width: widget.themeData.size + 2,
height: widget.themeData.size + 2,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: _shapeBorder,
border: _borderWidth == 0
? null
: Border.all(
width: _borderWidth,
color: widget.themeData.color ?? Colors.black,
),
color: _backColor,
),
child: AnimatedBuilder(
animation: controller,
child: _icon,
builder: (context, child) {
return Transform.rotate(
angle: animation.value * (-pi / 180),
child: child,
);
},
),
);
}
}

142
lib/src/tree_view.dart Normal file
View file

@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'tree_view_controller.dart';
import 'tree_view_theme.dart';
import 'tree_node.dart';
import 'models/node.dart';
/// Defines the [TreeView] widget.
///
/// This is the main widget for the package. It requires a controller
/// and allows you to specify other optional properties that manages
/// the appearance and handle events.
///
/// ```dart
/// TreeView(
/// controller: _treeViewController,
/// allowParentSelect: false,
/// supportParentDoubleTap: false,
/// onExpansionChanged: _expandNodeHandler,
/// onNodeTap: (key) {
/// setState(() {
/// _treeViewController = _treeViewController.copyWith(selectedKey: key);
/// });
/// },
/// theme: treeViewTheme
/// ),
/// ```
class TreeView extends InheritedWidget {
/// The controller for the [TreeView]. It manages the data and selected key.
final TreeViewController controller;
/// The tap handler for a node. Passes the node key.
final Function(String)? onNodeTap;
/// Custom builder for nodes. Parameters are the build context and tree node.
final Widget Function(BuildContext, Node)? nodeBuilder;
/// The double tap handler for a node. Passes the node key.
final Function(String)? onNodeDoubleTap;
/// The expand/collapse handler for a node. Passes the node key and the
/// expansion state.
final Function(String, bool)? onExpansionChanged;
/// The theme for [TreeView].
final TreeViewTheme theme;
/// Determines whether the user can select a parent node. If false,
/// tapping the parent will expand or collapse the node. If true, the node
/// will be selected and the use has to use the expander to expand or
/// collapse the node.
final bool allowParentSelect;
/// How the [TreeView] should respond to user input.
final ScrollPhysics? physics;
/// Whether the extent of the [TreeView] should be determined by the contents
/// being viewed.
///
/// Defaults to false.
final bool shrinkWrap;
/// Whether the [TreeView] is the primary scroll widget associated with the
/// parent PrimaryScrollController..
///
/// Defaults to true.
final bool primary;
/// Determines whether the parent node can receive a double tap. This is
/// useful if [allowParentSelect] is true. This allows the user to double tap
/// the parent node to expand or collapse the parent when [allowParentSelect]
/// is true.
/// ___IMPORTANT___
/// _When true, the tap handler is delayed. This is because the double tap
/// action requires a short delay to determine whether the user is attempting
/// a single or double tap._
final bool supportParentDoubleTap;
TreeView({
Key? key,
required this.controller,
this.onNodeTap,
this.onNodeDoubleTap,
this.physics,
this.onExpansionChanged,
this.allowParentSelect: false,
this.supportParentDoubleTap: false,
this.shrinkWrap: false,
this.primary: true,
this.nodeBuilder,
TreeViewTheme? theme,
}) : this.theme = theme ?? const TreeViewTheme(),
super(
key: key,
child: _TreeViewData(
controller,
shrinkWrap: shrinkWrap,
primary: primary,
physics: physics,
),
);
static TreeView? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType(aspect: TreeView);
@override
bool updateShouldNotify(TreeView oldWidget) {
return oldWidget.controller.children != this.controller.children ||
oldWidget.onNodeTap != this.onNodeTap ||
oldWidget.onExpansionChanged != this.onExpansionChanged ||
oldWidget.theme != this.theme ||
oldWidget.supportParentDoubleTap != this.supportParentDoubleTap ||
oldWidget.allowParentSelect != this.allowParentSelect;
}
}
class _TreeViewData extends StatelessWidget {
final TreeViewController _controller;
final bool? shrinkWrap;
final bool? primary;
final ScrollPhysics? physics;
const _TreeViewData(this._controller,
{this.shrinkWrap, this.primary, this.physics});
@override
Widget build(BuildContext context) {
ThemeData _parentTheme = Theme.of(context);
return Theme(
data: _parentTheme.copyWith(hoverColor: Colors.grey.shade100),
child: ListView(
shrinkWrap: shrinkWrap!,
primary: primary,
physics: physics,
padding: EdgeInsets.zero,
children: _controller.children.map((Node node) {
return TreeNode(node: node);
}).toList(),
),
);
}
}

View file

@ -0,0 +1,487 @@
import 'dart:convert' show jsonDecode, jsonEncode;
import 'models/node.dart';
/// Defines the insertion mode adding a new [Node] to the [TreeView].
enum InsertMode {
prepend,
append,
insert,
}
/// Defines the controller needed to display the [TreeView].
///
/// Used by [TreeView] to display the nodes and selected node.
///
/// This class also defines methods used to manipulate data in
/// the [TreeView]. The methods ([addNode], [updateNode],
/// and [deleteNode]) are non-mutilating, meaning they will not
/// modify the tree but instead they will return a mutilated
/// copy of the data. You can then use your own logic to appropriately
/// update the [TreeView]. e.g.
///
/// ```dart
/// TreeViewController controller = TreeViewController(children: nodes);
/// Node node = controller.getNode('unique_key')!;
/// Node updatedNode = node.copyWith(
/// key: 'another_unique_key',
/// label: 'Another Node',
/// );
/// List<Node> newChildren = controller.updateNode(node.key, updatedNode);
/// controller = TreeViewController(children: newChildren);
/// ```
class TreeViewController {
/// The data for the [TreeView].
final List<Node> children;
/// The key of the select node in the [TreeView].
final String? selectedKey;
TreeViewController({
this.children: const [],
this.selectedKey,
});
/// Creates a copy of this controller but with the given fields
/// replaced with the new values.
TreeViewController copyWith({List<Node>? children, String? selectedKey}) {
return TreeViewController(
children: children ?? this.children,
selectedKey: selectedKey ?? this.selectedKey,
);
}
/// Loads this controller with data from a JSON String
/// This method expects the user to properly update the state
///
/// ```dart
/// setState((){
/// controller = controller.loadJSON(json: jsonString);
/// });
/// ```
TreeViewController loadJSON({String json: '[]'}) {
List jsonList = jsonDecode(json);
List<Map<String, dynamic>> list = List<Map<String, dynamic>>.from(jsonList);
return loadMap(list: list);
}
/// Loads this controller with data from a Map.
/// This method expects the user to properly update the state
///
/// ```dart
/// setState((){
/// controller = controller.loadMap(map: dataMap);
/// });
/// ```
TreeViewController loadMap({List<Map<String, dynamic>> list: const []}) {
List<Node> treeData =
list.map((Map<String, dynamic> item) => Node.fromMap(item)).toList();
return TreeViewController(
children: treeData,
selectedKey: this.selectedKey,
);
}
/// Adds a new node to an existing node identified by specified key.
/// It returns a new controller with the new node added. This method
/// expects the user to properly place this call so that the state is
/// updated.
///
/// See [TreeViewController.addNode] for info on optional parameters.
///
/// ```dart
/// setState((){
/// controller = controller.withAddNode(key, newNode);
/// });
/// ```
TreeViewController withAddNode(
String key,
Node newNode, {
Node? parent,
int? index,
InsertMode mode: InsertMode.append,
}) {
List<Node> _data =
addNode(key, newNode, parent: parent, mode: mode, index: index);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Replaces an existing node identified by specified key with a new node.
/// It returns a new controller with the updated node replaced. This method
/// expects the user to properly place this call so that the state is
/// updated.
///
/// See [TreeViewController.updateNode] for info on optional parameters.
///
/// ```dart
/// setState((){
/// controller = controller.withUpdateNode(key, newNode);
/// });
/// ```
TreeViewController withUpdateNode(String key, Node newNode, {Node? parent}) {
List<Node> _data = updateNode(key, newNode, parent: parent);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Removes an existing node identified by specified key.
/// It returns a new controller with the node removed. This method
/// expects the user to properly place this call so that the state is
/// updated.
///
/// See [TreeViewController.deleteNode] for info on optional parameters.
///
/// ```dart
/// setState((){
/// controller = controller.withDeleteNode(key);
/// });
/// ```
TreeViewController withDeleteNode(String key, {Node? parent}) {
List<Node> _data = deleteNode(key, parent: parent);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Toggles the expanded property of an existing node identified by
/// specified key. It returns a new controller with the node toggled.
/// This method expects the user to properly place this call so
/// that the state is updated.
///
/// See [TreeViewController.toggleNode] for info on optional parameters.
///
/// ```dart
/// setState((){
/// controller = controller.withToggleNode(key, newNode);
/// });
/// ```
TreeViewController withToggleNode(String key, {Node? parent}) {
List<Node> _data = toggleNode(key, parent: parent);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Expands all nodes down to Node identified by specified key.
/// It returns a new controller with the nodes expanded.
/// This method expects the user to properly place this call so
/// that the state is updated.
///
/// Internally uses [TreeViewController.expandToNode].
///
/// ```dart
/// setState((){
/// controller = controller.withExpandToNode(key, newNode);
/// });
/// ```
TreeViewController withExpandToNode(String key) {
List<Node> _data = expandToNode(key);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Collapses all nodes down to Node identified by specified key.
/// It returns a new controller with the nodes collapsed.
/// This method expects the user to properly place this call so
/// that the state is updated.
///
/// Internally uses [TreeViewController.collapseToNode].
///
/// ```dart
/// setState((){
/// controller = controller.withCollapseToNode(key, newNode);
/// });
/// ```
TreeViewController withCollapseToNode(String key) {
List<Node> _data = collapseToNode(key);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Expands all nodes down to parent Node.
/// It returns a new controller with the nodes expanded.
/// This method expects the user to properly place this call so
/// that the state is updated.
///
/// Internally uses [TreeViewController.expandAll].
///
/// ```dart
/// setState((){
/// controller = controller.withExpandAll();
/// });
/// ```
TreeViewController withExpandAll({Node? parent}) {
List<Node> _data = expandAll(parent: parent);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Collapses all nodes down to parent Node.
/// It returns a new controller with the nodes collapsed.
/// This method expects the user to properly place this call so
/// that the state is updated.
///
/// Internally uses [TreeViewController.collapseAll].
///
/// ```dart
/// setState((){
/// controller = controller.withCollapseAll();
/// });
/// ```
TreeViewController withCollapseAll({Node? parent}) {
List<Node> _data = collapseAll(parent: parent);
return TreeViewController(
children: _data,
selectedKey: this.selectedKey,
);
}
/// Gets the node that has a key value equal to the specified key.
Node? getNode(String key, {Node? parent}) {
Node? _found;
List<Node> _children = parent == null ? this.children : parent.children;
Iterator iter = _children.iterator;
while (iter.moveNext()) {
Node child = iter.current;
if (child.key == key) {
_found = child;
break;
} else {
if (child.isParent) {
_found = this.getNode(key, parent: child);
if (_found != null) {
break;
}
}
}
}
return _found;
}
/// Expands all node that are children of the parent node parameter. If no parent is passed, uses the root node as the parent.
List<Node> expandAll({Node? parent}) {
List<Node> _children = [];
Iterator iter =
parent == null ? this.children.iterator : parent.children.iterator;
while (iter.moveNext()) {
Node child = iter.current;
if (child.isParent) {
_children.add(child.copyWith(
expanded: true,
children: this.expandAll(parent: child),
));
} else {
_children.add(child);
}
}
return _children;
}
/// Collapses all node that are children of the parent node parameter. If no parent is passed, uses the root node as the parent.
List<Node> collapseAll({Node? parent}) {
List<Node> _children = [];
Iterator iter =
parent == null ? this.children.iterator : parent.children.iterator;
while (iter.moveNext()) {
Node child = iter.current;
if (child.isParent) {
_children.add(child.copyWith(
expanded: false,
children: this.expandAll(parent: child),
));
} else {
_children.add(child);
}
}
return _children;
}
/// Gets the parent of the node identified by specified key.
Node? getParent(String key, {Node? parent}) {
Node? _found;
List<Node> _children = parent == null ? this.children : parent.children;
Iterator iter = _children.iterator;
while (iter.moveNext()) {
Node child = iter.current;
if (child.key == key) {
_found = parent ?? child;
break;
} else {
if (child.isParent) {
_found = this.getParent(key, parent: child);
if (_found != null) {
break;
}
}
}
}
return _found;
}
/// Expands a node and all of the node's ancestors so that the node is
/// visible without the need to manually expand each node.
List<Node> expandToNode(String key) {
List<String> _ancestors = [];
String _currentKey = key;
_ancestors.add(_currentKey);
Node? _parent = this.getParent(_currentKey);
if (_parent != null) {
while (_parent!.key != _currentKey) {
_currentKey = _parent.key;
_ancestors.add(_currentKey);
_parent = this.getParent(_currentKey);
}
TreeViewController _this = this;
_ancestors.forEach((String k) {
Node _node = _this.getNode(k)!;
Node _updated = _node.copyWith(expanded: true);
_this = _this.withUpdateNode(k, _updated);
});
return _this.children;
}
return this.children;
}
/// Collapses a node and all of the node's ancestors without the need to
/// manually collapse each node.
List<Node> collapseToNode(String key) {
List<String> _ancestors = [];
String _currentKey = key;
_ancestors.add(_currentKey);
Node? _parent = this.getParent(_currentKey);
if (_parent != null) {
while (_parent!.key != _currentKey) {
_currentKey = _parent.key;
_ancestors.add(_currentKey);
_parent = this.getParent(_currentKey);
}
TreeViewController _this = this;
_ancestors.forEach((String k) {
Node _node = _this.getNode(k)!;
Node _updated = _node.copyWith(expanded: false);
_this = _this.withUpdateNode(k, _updated);
});
return _this.children;
}
return this.children;
}
/// Adds a new node to an existing node identified by specified key. It optionally
/// accepts an [InsertMode] and index. If no [InsertMode] is specified,
/// it appends the new node as a child at the end. This method returns
/// a new list with the added node.
List<Node> addNode(
String key,
Node newNode, {
Node? parent,
int? index,
InsertMode mode: InsertMode.append,
}) {
List<Node> _children = parent == null ? this.children : parent.children;
return _children.map((Node child) {
if (child.key == key) {
List<Node> _children = child.children.toList(growable: true);
if (mode == InsertMode.prepend) {
_children.insert(0, newNode);
} else if (mode == InsertMode.insert) {
_children.insert(index ?? _children.length, newNode);
} else {
_children.add(newNode);
}
return child.copyWith(children: _children);
} else {
return child.copyWith(
children: addNode(
key,
newNode,
parent: child,
mode: mode,
index: index,
),
);
}
}).toList();
}
/// Updates an existing node identified by specified key. This method
/// returns a new list with the updated node.
List<Node> updateNode(String key, Node newNode, {Node? parent}) {
List<Node> _children = parent == null ? this.children : parent.children;
return _children.map((Node child) {
if (child.key == key) {
return newNode;
} else {
if (child.isParent) {
return child.copyWith(
children: updateNode(
key,
newNode,
parent: child,
),
);
}
return child;
}
}).toList();
}
/// Toggles an existing node identified by specified key. This method
/// returns a new list with the specified node toggled.
List<Node> toggleNode(String key, {Node? parent}) {
Node? _node = getNode(key, parent: parent);
return updateNode(key, _node!.copyWith(expanded: !_node.expanded));
}
/// Deletes an existing node identified by specified key. This method
/// returns a new list with the specified node removed.
List<Node> deleteNode(String key, {Node? parent}) {
List<Node> _children = parent == null ? this.children : parent.children;
List<Node> _filteredChildren = [];
Iterator iter = _children.iterator;
while (iter.moveNext()) {
Node child = iter.current;
if (child.key != key) {
if (child.isParent) {
_filteredChildren.add(child.copyWith(
children: deleteNode(key, parent: child),
));
} else {
_filteredChildren.add(child);
}
}
}
return _filteredChildren;
}
/// Get the current selected node. Returns null if there is no selectedKey
Node? get selectedNode {
return this.selectedKey!.isEmpty ? null : getNode(this.selectedKey!);
}
/// Map representation of this object
List<Map<String, dynamic>> get asMap {
return children.map((Node child) => child.asMap).toList();
}
@override
String toString() {
return jsonEncode(asMap);
}
}

View file

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'expander_theme_data.dart';
import 'tree_node.dart';
const double _kDefaultLevelPadding = 20;
const int _kExpandSpeed = 130;
/// Defines the appearance of the [TreeView].
///
/// Used by [TreeView] to control the appearance of the sub-widgets
/// in the [TreeView] widget.
class TreeViewTheme {
/// The [ColorScheme] for [TreeView] widget.
final ColorScheme colorScheme;
/// The horizontal padding for the children of a [TreeNode] parent.
final double levelPadding;
/// Whether the [TreeNode] is vertically dense.
///
/// If this property is null then its value is based on [ListTileTheme.dense].
///
/// A dense [TreeNode] defaults to a smaller height.
final bool dense;
/// Vertical spacing between tabs.
/// If this property is null then [dense] attribute will work and vice versa.
final double? verticalSpacing;
/// Horizontal spacing between tabs.
/// If this property is null then horizontal spacing between tabs is default [_treeView.theme.iconTheme.size + 5]
final double? horizontalSpacing;
/// Horizontal padding for node icons.
final double iconPadding;
/// The default appearance theme for [TreeNode] icons.
final IconThemeData iconTheme;
/// The appearance theme for [TreeNode] expander icons.
final ExpanderThemeData expanderTheme;
/// The text style for child [TreeNode] text.
final TextStyle labelStyle;
/// The text style for parent [TreeNode] text.
final TextStyle parentLabelStyle;
/// The text overflow for child [TreeNode] text.
/// If this property is null then [softWrap] is true;
final TextOverflow? labelOverflow;
/// The text overflow for parent [TreeNode] text.
/// If this property is null then [softWrap] is true;
final TextOverflow? parentLabelOverflow;
/// the speed at which expander icon animates.
final Duration expandSpeed;
const TreeViewTheme({
this.colorScheme: const ColorScheme.light(),
this.iconTheme: const IconThemeData.fallback(),
this.expanderTheme: const ExpanderThemeData.fallback(),
this.labelStyle: const TextStyle(),
this.parentLabelStyle: const TextStyle(fontWeight: FontWeight.bold),
this.labelOverflow,
this.parentLabelOverflow,
this.levelPadding: _kDefaultLevelPadding,
this.dense: true,
this.verticalSpacing,
this.horizontalSpacing,
this.iconPadding: 8,
this.expandSpeed: const Duration(milliseconds: _kExpandSpeed),
});
/// Creates a [TreeView] theme with some reasonable default values.
///
/// The [colorScheme] is [ColorScheme.light],
/// the [iconTheme] is [IconThemeData.fallback],
/// the [expanderTheme] is [ExpanderThemeData.fallback],
/// the [labelStyle] is the default [TextStyle],
/// the [parentLabelStyle] is the default [TextStyle] with bold weight,
/// and the default [levelPadding] is 20.0.
const TreeViewTheme.fallback()
: colorScheme = const ColorScheme.light(),
iconTheme = const IconThemeData.fallback(),
expanderTheme = const ExpanderThemeData.fallback(),
labelStyle = const TextStyle(),
parentLabelStyle = const TextStyle(fontWeight: FontWeight.bold),
labelOverflow = null,
parentLabelOverflow = null,
dense = true,
verticalSpacing = null,
horizontalSpacing = null,
iconPadding = 8,
levelPadding = _kDefaultLevelPadding,
expandSpeed = const Duration(milliseconds: _kExpandSpeed);
/// Creates a copy of this theme but with the given fields replaced with
/// the new values.
TreeViewTheme copyWith({
ColorScheme? colorScheme,
IconThemeData? iconTheme,
ExpanderThemeData? expanderTheme,
TextStyle? labelStyle,
TextStyle? parentLabelStyle,
TextOverflow? labelOverflow,
TextOverflow? parentLabelOverflow,
bool? dense,
double? verticalSpacing,
double? horizontalSpacing,
double? iconPadding,
double? levelPadding,
}) {
return TreeViewTheme(
colorScheme: colorScheme ?? this.colorScheme,
levelPadding: levelPadding ?? this.levelPadding,
iconPadding: iconPadding ?? this.iconPadding,
iconTheme: iconTheme ?? this.iconTheme,
expanderTheme: expanderTheme ?? this.expanderTheme,
labelStyle: labelStyle ?? this.labelStyle,
dense: dense ?? this.dense,
verticalSpacing: verticalSpacing ?? this.verticalSpacing,
horizontalSpacing: horizontalSpacing ?? this.horizontalSpacing,
parentLabelStyle: parentLabelStyle ?? this.parentLabelStyle,
labelOverflow: labelOverflow ?? this.labelOverflow,
parentLabelOverflow: parentLabelOverflow ?? this.parentLabelOverflow);
}
/// Returns a new theme that matches this [TreeView] theme but with some values
/// replaced by the non-null parameters of the given icon theme. If the given
/// [TreeViewTheme] is null, simply returns this theme.
TreeViewTheme merge(TreeViewTheme other) {
if (other == null) return this;
return copyWith(
colorScheme: other.colorScheme,
levelPadding: other.levelPadding,
iconPadding: other.iconPadding,
iconTheme: other.iconTheme,
expanderTheme: other.expanderTheme,
labelStyle: other.labelStyle,
dense: other.dense,
verticalSpacing: other.verticalSpacing,
horizontalSpacing: other.horizontalSpacing,
parentLabelStyle: other.parentLabelStyle,
labelOverflow: other.labelOverflow,
parentLabelOverflow: other.parentLabelOverflow);
}
TreeViewTheme resolve(BuildContext context) => this;
Duration get quickExpandSpeed =>
Duration(milliseconds: (expandSpeed.inMilliseconds * 1.6).toInt());
@override
int get hashCode {
return hashValues(
colorScheme,
levelPadding,
iconPadding,
iconTheme,
expanderTheme,
labelStyle,
dense,
verticalSpacing,
horizontalSpacing,
parentLabelStyle,
labelOverflow,
parentLabelOverflow);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is TreeViewTheme &&
other.colorScheme == colorScheme &&
other.levelPadding == levelPadding &&
other.iconPadding == iconPadding &&
other.iconTheme == iconTheme &&
other.expanderTheme == expanderTheme &&
other.labelStyle == labelStyle &&
other.dense == dense &&
other.verticalSpacing == verticalSpacing &&
other.horizontalSpacing == horizontalSpacing &&
other.parentLabelStyle == parentLabelStyle &&
other.labelOverflow == labelOverflow &&
other.parentLabelOverflow == parentLabelOverflow;
}
}

681
lib/src/utilities.dart Normal file
View file

@ -0,0 +1,681 @@
import 'dart:convert';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
class Utilities {
static final RegExp _hexExp = RegExp(
r'^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',
caseSensitive: false,
);
static final RegExp _rgbExp = RegExp(
r'^rgb\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\)$',
caseSensitive: false,
);
static final RegExp _rgbaExp = RegExp(
r'^rgba\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s?,\s?(0|0?\.\d|1(\.0)?)\)$',
caseSensitive: false,
);
static final RegExp _materialDesignColorExp = RegExp(
r'^((?:red|pink|purple|deepPurple|indigo|blue|lightBlue|cyan|teal|green|lightGreen|lime|yellow|amber|orange|deepOrange|brown|grey|blueGrey)(?:50|100|200|300|400|500|600|700|800|900)?|(?:red|pink|purple|deepPurple|indigo|blue|lightBlue|cyan|teal|green|lightGreen|lime|yellow|amber|orange|deepOrange)(?:Accent|Accent50|Accent100|Accent200|Accent400|Accent700)?|(?:black|white))$',
caseSensitive: false,
);
static const Color BLACK = Color.fromARGB(255, 0, 0, 0);
static const Color WHITE = Color.fromARGB(255, 255, 255, 255);
static Color getColor(String value) {
String colorValue = value;
if (_hexExp.hasMatch(colorValue)) {
final buffer = StringBuffer();
if (colorValue.length == 3 || colorValue.length == 4) {
colorValue = colorValue.replaceFirst('#', '');
List<String> pieces =
colorValue.split('').map((String piece) => '$piece$piece').toList();
colorValue = pieces.join();
}
if (colorValue.length == 6 || colorValue.length == 7) buffer.write('ff');
buffer.write(colorValue.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
} else if (_rgbExp.hasMatch(value)) {
var parts = _rgbExp.allMatches(value);
int r = 0;
int g = 0;
int b = 0;
for (var part in parts) {
r = int.parse(part.group(1)!);
g = int.parse(part.group(2)!);
b = int.parse(part.group(3)!);
}
return Color.fromARGB(255, r, g, b);
} else if (_rgbaExp.hasMatch(value)) {
var parts = _rgbaExp.allMatches(value);
int r = 0;
int g = 0;
int b = 0;
double a = 1;
for (var part in parts) {
r = int.parse(part.group(1)!);
g = int.parse(part.group(2)!);
b = int.parse(part.group(3)!);
a = double.parse(part.group(4)!);
}
return Color.fromARGB((255 * a).toInt(), r, g, b);
} else if (_materialDesignColorExp.hasMatch(value)) {
switch (value) {
case 'black':
return Colors.black;
case 'white':
return Colors.white;
case 'amber':
return Colors.amber;
case 'amber100':
return Colors.amber.shade100;
case 'amber200':
return Colors.amber.shade200;
case 'amber300':
return Colors.amber.shade300;
case 'amber400':
return Colors.amber.shade400;
case 'amber500':
return Colors.amber.shade500;
case 'amber600':
return Colors.amber.shade600;
case 'amber700':
return Colors.amber.shade700;
case 'amber800':
return Colors.amber.shade800;
case 'amber900':
return Colors.amber.shade900;
case 'amberAccent':
return Colors.amberAccent;
case 'amberAccent50':
return Colors.amberAccent.shade50;
case 'amberAccent100':
return Colors.amberAccent.shade100;
case 'amberAccent200':
return Colors.amberAccent.shade200;
case 'amberAccent400':
return Colors.amberAccent.shade400;
case 'amberAccent700':
return Colors.amberAccent.shade700;
case 'blue':
return Colors.blue;
case 'blue100':
return Colors.blue.shade100;
case 'blue200':
return Colors.blue.shade200;
case 'blue300':
return Colors.blue.shade300;
case 'blue400':
return Colors.blue.shade400;
case 'blue500':
return Colors.blue.shade500;
case 'blue600':
return Colors.blue.shade600;
case 'blue700':
return Colors.blue.shade700;
case 'blue800':
return Colors.blue.shade800;
case 'blue900':
return Colors.blue.shade900;
case 'blueAccent':
return Colors.blueAccent;
case 'blueAccent50':
return Colors.blueAccent.shade50;
case 'blueAccent100':
return Colors.blueAccent.shade100;
case 'blueAccent200':
return Colors.blueAccent.shade200;
case 'blueAccent400':
return Colors.blueAccent.shade400;
case 'blueAccent700':
return Colors.blueAccent.shade700;
case 'blueGrey':
return Colors.blueGrey;
case 'blueGrey100':
return Colors.blueGrey.shade100;
case 'blueGrey200':
return Colors.blueGrey.shade200;
case 'blueGrey300':
return Colors.blueGrey.shade300;
case 'blueGrey400':
return Colors.blueGrey.shade400;
case 'blueGrey500':
return Colors.blueGrey.shade500;
case 'blueGrey600':
return Colors.blueGrey.shade600;
case 'blueGrey700':
return Colors.blueGrey.shade700;
case 'blueGrey800':
return Colors.blueGrey.shade800;
case 'blueGrey900':
return Colors.blueGrey.shade900;
case 'brown':
return Colors.brown;
case 'brown100':
return Colors.brown.shade100;
case 'brown200':
return Colors.brown.shade200;
case 'brown300':
return Colors.brown.shade300;
case 'brown400':
return Colors.brown.shade400;
case 'brown500':
return Colors.brown.shade500;
case 'brown600':
return Colors.brown.shade600;
case 'brown700':
return Colors.brown.shade700;
case 'brown800':
return Colors.brown.shade800;
case 'brown900':
return Colors.brown.shade900;
case 'cyan':
return Colors.cyan;
case 'cyan100':
return Colors.cyan.shade100;
case 'cyan200':
return Colors.cyan.shade200;
case 'cyan300':
return Colors.cyan.shade300;
case 'cyan400':
return Colors.cyan.shade400;
case 'cyan500':
return Colors.cyan.shade500;
case 'cyan600':
return Colors.cyan.shade600;
case 'cyan700':
return Colors.cyan.shade700;
case 'cyan800':
return Colors.cyan.shade800;
case 'cyan900':
return Colors.cyan.shade900;
case 'cyanAccent':
return Colors.cyanAccent;
case 'cyanAccent50':
return Colors.cyanAccent.shade50;
case 'cyanAccent100':
return Colors.cyanAccent.shade100;
case 'cyanAccent200':
return Colors.cyanAccent.shade200;
case 'cyanAccent400':
return Colors.cyanAccent.shade400;
case 'cyanAccent700':
return Colors.cyanAccent.shade700;
case 'deepOrange':
return Colors.deepOrange;
case 'deepOrange100':
return Colors.deepOrange.shade100;
case 'deepOrange200':
return Colors.deepOrange.shade200;
case 'deepOrange300':
return Colors.deepOrange.shade300;
case 'deepOrange400':
return Colors.deepOrange.shade400;
case 'deepOrange500':
return Colors.deepOrange.shade500;
case 'deepOrange600':
return Colors.deepOrange.shade600;
case 'deepOrange700':
return Colors.deepOrange.shade700;
case 'deepOrange800':
return Colors.deepOrange.shade800;
case 'deepOrange900':
return Colors.deepOrange.shade900;
case 'deepOrangeAccent':
return Colors.deepOrangeAccent;
case 'deepOrangeAccent50':
return Colors.deepOrangeAccent.shade50;
case 'deepOrangeAccent100':
return Colors.deepOrangeAccent.shade100;
case 'deepOrangeAccent200':
return Colors.deepOrangeAccent.shade200;
case 'deepOrangeAccent400':
return Colors.deepOrangeAccent.shade400;
case 'deepOrangeAccent700':
return Colors.deepOrangeAccent.shade700;
case 'deepPurple':
return Colors.deepPurple;
case 'deepPurple100':
return Colors.deepPurple.shade100;
case 'deepPurple200':
return Colors.deepPurple.shade200;
case 'deepPurple300':
return Colors.deepPurple.shade300;
case 'deepPurple400':
return Colors.deepPurple.shade400;
case 'deepPurple500':
return Colors.deepPurple.shade500;
case 'deepPurple600':
return Colors.deepPurple.shade600;
case 'deepPurple700':
return Colors.deepPurple.shade700;
case 'deepPurple800':
return Colors.deepPurple.shade800;
case 'deepPurple900':
return Colors.deepPurple.shade900;
case 'deepPurpleAccent':
return Colors.deepPurpleAccent;
case 'deepPurpleAccent50':
return Colors.deepPurpleAccent.shade50;
case 'deepPurpleAccent100':
return Colors.deepPurpleAccent.shade100;
case 'deepPurpleAccent200':
return Colors.deepPurpleAccent.shade200;
case 'deepPurpleAccent400':
return Colors.deepPurpleAccent.shade400;
case 'deepPurpleAccent700':
return Colors.deepPurpleAccent.shade700;
case 'green':
return Colors.green;
case 'green100':
return Colors.green.shade100;
case 'green200':
return Colors.green.shade200;
case 'green300':
return Colors.green.shade300;
case 'green400':
return Colors.green.shade400;
case 'green500':
return Colors.green.shade500;
case 'green600':
return Colors.green.shade600;
case 'green700':
return Colors.green.shade700;
case 'green800':
return Colors.green.shade800;
case 'green900':
return Colors.green.shade900;
case 'greenAccent':
return Colors.greenAccent;
case 'greenAccent50':
return Colors.greenAccent.shade50;
case 'greenAccent100':
return Colors.greenAccent.shade100;
case 'greenAccent200':
return Colors.greenAccent.shade200;
case 'greenAccent400':
return Colors.greenAccent.shade400;
case 'greenAccent700':
return Colors.greenAccent.shade700;
case 'grey':
return Colors.grey;
case 'grey100':
return Colors.grey.shade100;
case 'grey200':
return Colors.grey.shade200;
case 'grey300':
return Colors.grey.shade300;
case 'grey400':
return Colors.grey.shade400;
case 'grey500':
return Colors.grey.shade500;
case 'grey600':
return Colors.grey.shade600;
case 'grey700':
return Colors.grey.shade700;
case 'grey800':
return Colors.grey.shade800;
case 'grey900':
return Colors.grey.shade900;
case 'indigo':
return Colors.indigo;
case 'indigo100':
return Colors.indigo.shade100;
case 'indigo200':
return Colors.indigo.shade200;
case 'indigo300':
return Colors.indigo.shade300;
case 'indigo400':
return Colors.indigo.shade400;
case 'indigo500':
return Colors.indigo.shade500;
case 'indigo600':
return Colors.indigo.shade600;
case 'indigo700':
return Colors.indigo.shade700;
case 'indigo800':
return Colors.indigo.shade800;
case 'indigo900':
return Colors.indigo.shade900;
case 'indigoAccent':
return Colors.indigoAccent;
case 'indigoAccent50':
return Colors.indigoAccent.shade50;
case 'indigoAccent100':
return Colors.indigoAccent.shade100;
case 'indigoAccent200':
return Colors.indigoAccent.shade200;
case 'indigoAccent400':
return Colors.indigoAccent.shade400;
case 'indigoAccent700':
return Colors.indigoAccent.shade700;
case 'lightBlue':
return Colors.lightBlue;
case 'lightBlue100':
return Colors.lightBlue.shade100;
case 'lightBlue200':
return Colors.lightBlue.shade200;
case 'lightBlue300':
return Colors.lightBlue.shade300;
case 'lightBlue400':
return Colors.lightBlue.shade400;
case 'lightBlue500':
return Colors.lightBlue.shade500;
case 'lightBlue600':
return Colors.lightBlue.shade600;
case 'lightBlue700':
return Colors.lightBlue.shade700;
case 'lightBlue800':
return Colors.lightBlue.shade800;
case 'lightBlue900':
return Colors.lightBlue.shade900;
case 'lightBlueAccent':
return Colors.lightBlueAccent;
case 'lightBlueAccent50':
return Colors.lightBlueAccent.shade50;
case 'lightBlueAccent100':
return Colors.lightBlueAccent.shade100;
case 'lightBlueAccent200':
return Colors.lightBlueAccent.shade200;
case 'lightBlueAccent400':
return Colors.lightBlueAccent.shade400;
case 'lightBlueAccent700':
return Colors.lightBlueAccent.shade700;
case 'lightGreen':
return Colors.lightGreen;
case 'lightGreen100':
return Colors.lightGreen.shade100;
case 'lightGreen200':
return Colors.lightGreen.shade200;
case 'lightGreen300':
return Colors.lightGreen.shade300;
case 'lightGreen400':
return Colors.lightGreen.shade400;
case 'lightGreen500':
return Colors.lightGreen.shade500;
case 'lightGreen600':
return Colors.lightGreen.shade600;
case 'lightGreen700':
return Colors.lightGreen.shade700;
case 'lightGreen800':
return Colors.lightGreen.shade800;
case 'lightGreen900':
return Colors.lightGreen.shade900;
case 'lightGreenAccent':
return Colors.lightGreenAccent;
case 'lightGreenAccent50':
return Colors.lightGreenAccent.shade50;
case 'lightGreenAccent100':
return Colors.lightGreenAccent.shade100;
case 'lightGreenAccent200':
return Colors.lightGreenAccent.shade200;
case 'lightGreenAccent400':
return Colors.lightGreenAccent.shade400;
case 'lightGreenAccent700':
return Colors.lightGreenAccent.shade700;
case 'lime':
return Colors.lime;
case 'lime100':
return Colors.lime.shade100;
case 'lime200':
return Colors.lime.shade200;
case 'lime300':
return Colors.lime.shade300;
case 'lime400':
return Colors.lime.shade400;
case 'lime500':
return Colors.lime.shade500;
case 'lime600':
return Colors.lime.shade600;
case 'lime700':
return Colors.lime.shade700;
case 'lime800':
return Colors.lime.shade800;
case 'lime900':
return Colors.lime.shade900;
case 'limeAccent':
return Colors.limeAccent;
case 'limeAccent50':
return Colors.limeAccent.shade50;
case 'limeAccent100':
return Colors.limeAccent.shade100;
case 'limeAccent200':
return Colors.limeAccent.shade200;
case 'limeAccent400':
return Colors.limeAccent.shade400;
case 'limeAccent700':
return Colors.limeAccent.shade700;
case 'orange':
return Colors.orange;
case 'orange100':
return Colors.orange.shade100;
case 'orange200':
return Colors.orange.shade200;
case 'orange300':
return Colors.orange.shade300;
case 'orange400':
return Colors.orange.shade400;
case 'orange500':
return Colors.orange.shade500;
case 'orange600':
return Colors.orange.shade600;
case 'orange700':
return Colors.orange.shade700;
case 'orange800':
return Colors.orange.shade800;
case 'orange900':
return Colors.orange.shade900;
case 'orangeAccent':
return Colors.orangeAccent;
case 'orangeAccent50':
return Colors.orangeAccent.shade50;
case 'orangeAccent100':
return Colors.orangeAccent.shade100;
case 'orangeAccent200':
return Colors.orangeAccent.shade200;
case 'orangeAccent400':
return Colors.orangeAccent.shade400;
case 'orangeAccent700':
return Colors.orangeAccent.shade700;
case 'pink':
return Colors.pink;
case 'pink100':
return Colors.pink.shade100;
case 'pink200':
return Colors.pink.shade200;
case 'pink300':
return Colors.pink.shade300;
case 'pink400':
return Colors.pink.shade400;
case 'pink500':
return Colors.pink.shade500;
case 'pink600':
return Colors.pink.shade600;
case 'pink700':
return Colors.pink.shade700;
case 'pink800':
return Colors.pink.shade800;
case 'pink900':
return Colors.pink.shade900;
case 'pinkAccent':
return Colors.pinkAccent;
case 'pinkAccent50':
return Colors.pinkAccent.shade50;
case 'pinkAccent100':
return Colors.pinkAccent.shade100;
case 'pinkAccent200':
return Colors.pinkAccent.shade200;
case 'pinkAccent400':
return Colors.pinkAccent.shade400;
case 'pinkAccent700':
return Colors.pinkAccent.shade700;
case 'purple':
return Colors.purple;
case 'purple100':
return Colors.purple.shade100;
case 'purple200':
return Colors.purple.shade200;
case 'purple300':
return Colors.purple.shade300;
case 'purple400':
return Colors.purple.shade400;
case 'purple500':
return Colors.purple.shade500;
case 'purple600':
return Colors.purple.shade600;
case 'purple700':
return Colors.purple.shade700;
case 'purple800':
return Colors.purple.shade800;
case 'purple900':
return Colors.purple.shade900;
case 'purpleAccent':
return Colors.purpleAccent;
case 'purpleAccent50':
return Colors.purpleAccent.shade50;
case 'purpleAccent100':
return Colors.purpleAccent.shade100;
case 'purpleAccent200':
return Colors.purpleAccent.shade200;
case 'purpleAccent400':
return Colors.purpleAccent.shade400;
case 'purpleAccent700':
return Colors.purpleAccent.shade700;
case 'red':
return Colors.red;
case 'red100':
return Colors.red.shade100;
case 'red200':
return Colors.red.shade200;
case 'red300':
return Colors.red.shade300;
case 'red400':
return Colors.red.shade400;
case 'red500':
return Colors.red.shade500;
case 'red600':
return Colors.red.shade600;
case 'red700':
return Colors.red.shade700;
case 'red800':
return Colors.red.shade800;
case 'red900':
return Colors.red.shade900;
case 'redAccent':
return Colors.redAccent;
case 'redAccent50':
return Colors.redAccent.shade50;
case 'redAccent100':
return Colors.redAccent.shade100;
case 'redAccent200':
return Colors.redAccent.shade200;
case 'redAccent400':
return Colors.redAccent.shade400;
case 'redAccent700':
return Colors.redAccent.shade700;
case 'teal':
return Colors.teal;
case 'teal100':
return Colors.teal.shade100;
case 'teal200':
return Colors.teal.shade200;
case 'teal300':
return Colors.teal.shade300;
case 'teal400':
return Colors.teal.shade400;
case 'teal500':
return Colors.teal.shade500;
case 'teal600':
return Colors.teal.shade600;
case 'teal700':
return Colors.teal.shade700;
case 'teal800':
return Colors.teal.shade800;
case 'teal900':
return Colors.teal.shade900;
case 'tealAccent':
return Colors.tealAccent;
case 'tealAccent50':
return Colors.tealAccent.shade50;
case 'tealAccent100':
return Colors.tealAccent.shade100;
case 'tealAccent200':
return Colors.tealAccent.shade200;
case 'tealAccent400':
return Colors.tealAccent.shade400;
case 'tealAccent700':
return Colors.tealAccent.shade700;
case 'yellow':
return Colors.yellow;
case 'yellow100':
return Colors.yellow.shade100;
case 'yellow200':
return Colors.yellow.shade200;
case 'yellow300':
return Colors.yellow.shade300;
case 'yellow400':
return Colors.yellow.shade400;
case 'yellow500':
return Colors.yellow.shade500;
case 'yellow600':
return Colors.yellow.shade600;
case 'yellow700':
return Colors.yellow.shade700;
case 'yellow800':
return Colors.yellow.shade800;
case 'yellow900':
return Colors.yellow.shade900;
case 'yellowAccent':
return Colors.yellowAccent;
case 'yellowAccent50':
return Colors.yellowAccent.shade50;
case 'yellowAccent100':
return Colors.yellowAccent.shade100;
case 'yellowAccent200':
return Colors.yellowAccent.shade200;
case 'yellowAccent400':
return Colors.yellowAccent.shade400;
case 'yellowAccent700':
return Colors.yellowAccent.shade700;
}
} else {
return Color.fromARGB(255, 0, 0, 0);
}
return Color.fromARGB(255, 0, 0, 0);
}
static String toRGBA(Color color) {
return 'rgba(${color.red},${color.green},${color.blue},${color.alpha / 255})';
}
static Color textColor(Color color) {
if (color.computeLuminance() > 0.6) {
return BLACK;
} else {
return WHITE;
}
}
static String generateRandom([int length = 16]) {
final Random _random = Random.secure();
var values = List<int>.generate(length, (i) => _random.nextInt(256));
return base64Url.encode(values).substring(0, length);
}
static bool truthful(value) {
if (value == null) {
return false;
}
if (value == true ||
value == 'true' ||
value == 1 ||
value == '1' ||
value.toString().toLowerCase() == 'yes') {
return true;
}
return false;
}
}

146
pubspec.lock Normal file
View file

@ -0,0 +1,146 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
sdks:
dart: ">=2.12.0 <3.0.0"

23
pubspec.yaml Normal file
View file

@ -0,0 +1,23 @@
name: flutter_treeview
description: A tree widget for Flutter that can be used to display nested, hierarchical data. It includes a number of features like styling labels, icons, and import and export utilities.
version: 1.0.3+2
homepage: https://bitbucket.org/kevinandre/flutter_treeview/src/master/
repository: https://bitbucket.org/kevinandre/flutter_treeview/src/master/
issue_tracker: https://bitbucket.org/kevinandre/flutter_treeview/issues
documentation: https://bitbucket.org/kevinandre/flutter_treeview/wiki/Home
api: https://pub.dev/documentation/flutter_treeview/latest/
#author: kevinandre.com <kevin@codeninelabs.com>
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true

View file

@ -0,0 +1 @@
{}

BIN
screenshots/ss1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

BIN
screenshots/ss2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

View file

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_treeview/src/expander_theme_data.dart';
void main() {
test('fallback', () {
final ExpanderThemeData theme = ExpanderThemeData.fallback();
expect(theme.type, ExpanderType.caret);
expect(theme.position, ExpanderPosition.start);
expect(theme.size, 30);
expect(theme.color, Color(0xFF000000));
});
test('copyWith', () {
ExpanderThemeData theme = ExpanderThemeData.fallback();
theme = theme.copyWith(
type: ExpanderType.arrow,
position: ExpanderPosition.end,
size: 20,
color: Color(0xFF990000),
);
expect(theme.type, ExpanderType.arrow);
expect(theme.position, ExpanderPosition.end);
expect(theme.size, 20);
expect(theme.color, Color(0xFF990000));
});
test('merge', () {
ExpanderThemeData theme = ExpanderThemeData.fallback();
ExpanderThemeData theme2 = ExpanderThemeData(
type: ExpanderType.arrow,
position: ExpanderPosition.end,
modifier: ExpanderModifier.circleFilled,
size: 20,
animated: false,
color: Color(0xFF990000),
);
theme = theme.merge(theme2);
expect(theme.type, ExpanderType.arrow);
expect(theme.position, ExpanderPosition.end);
expect(theme.modifier, ExpanderModifier.circleFilled);
expect(theme.size, 20);
expect(theme.color, Color(0xFF990000));
expect(theme, theme2);
});
}

132
test/node_test.dart Normal file
View file

@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_treeview/src/models/node.dart';
class Person {
final int age;
Person({required this.age});
}
class Animal {
final int legs;
Animal({required this.legs});
}
void main() {
group('instantiate', () {
test('from string', () {
final Node node = Node.fromLabel('Home');
expect(node.key.isNotEmpty, true);
expect(node.label, 'Home');
expect(node.icon, null);
expect(node.expanded, false);
expect(node.children.length, 0);
});
test('copyWith', () {
final Node node = Node.fromLabel('Home');
expect(node.label, 'Home');
expect(node.icon, null);
expect(node.expanded, false);
expect(node.children.length, 0);
final Node newNode = node.copyWith(label: 'New Home');
expect(newNode.label, 'New Home');
expect(newNode.icon, null);
expect(newNode.expanded, false);
expect(newNode.children.length, 0);
});
test('from map', () {
final map = {
"key": "12345",
"label": "Home",
};
final expectedMap = {
"key": "12345",
"label": "Home",
"icon": null,
"expanded": false,
"parent": false,
"children": [],
};
final Node node = Node.fromMap(map);
expect(node.key, '12345');
expect(node.label, 'Home');
expect(node.icon, null);
expect(node.expanded, false);
expect(node.children.length, 0);
expect(node.isParent, false);
expect(node.asMap, expectedMap);
});
test('from nested map', () {
final map = {
"label": "Home",
"expanded": 1,
"icon": Icons.home.codePoint.toString(),
"children": [
{
"key": "12345b",
"label": "Basement",
},
{
"key": "12345k",
"label": "Kitchen",
}
],
};
final expectedMap = {
"label": "Home",
"icon": null,
"expanded": true,
"parent": false,
"children": [
{
"key": "12345b",
"label": "Basement",
"icon": null,
"expanded": false,
"parent": false,
"children": [],
},
{
"key": "12345k",
"label": "Kitchen",
"icon": null,
"expanded": false,
"parent": false,
"children": [],
}
],
};
final Node node = Node.fromMap(map);
expect(node.key.isNotEmpty, true);
expect(node.label, 'Home');
expect(node.icon, null);
expect(node.expanded, true);
expect(node.children.length, 2);
expect(node.isParent, true);
Map nodeMap = node.asMap;
nodeMap.remove('key');
expect(nodeMap, expectedMap);
});
test('with data', () {
Person lukas = Person(age: 3);
Animal otis = Animal(legs: 4);
final Node node1 =
Node<Person>(label: 'Lukas', key: 'lukas', data: lukas);
Node node2 = Node.fromLabel('Friend');
expect(node1.hasData, true);
expect(node1.data.runtimeType, Person);
expect(node2.hasData, false);
expect(node2.data.runtimeType, Null);
node2 = node2.copyWith(data: otis);
expect(node2.data.runtimeType, Animal);
expect(node2.hasData, true);
final Node node3 =
Node<double>(label: 'Building Height', key: 'bldghgt', data: 100.4);
expect(node3.hasData, true);
expect(node3.data.runtimeType, double);
});
});
}

View file

@ -0,0 +1,95 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_treeview/src/models/node.dart';
import 'package:flutter_treeview/src/tree_view_controller.dart';
void main() {
List<Node> nodes = [
Node(label: 'documents', key: 'docs', children: [
Node(label: 'personal', key: 'd3', children: [
Node(label: 'Resume.docx', key: 'pd1'),
Node(label: 'Cover Letter.docx', key: 'pd2'),
]),
Node(label: 'Inspection.docx', key: 'd1'),
Node(label: 'Invoice.docx', key: 'd2'),
]),
Node(
label: 'MeetingReport.xls',
key: 'mrxls',
),
Node(
label: 'MeetingReport.pdf',
key: 'mrpdf',
),
Node(
label: 'Demo.zip',
key: 'demo',
),
];
group('TreeViewController Tests', () {
test('...get node', () {
TreeViewController controller = TreeViewController(children: nodes);
Node? validNode = controller.getNode('pd1');
Node? invalidNode = controller.getNode('xpd1');
expect(controller.children.length, 4);
expect(validNode.runtimeType, Node);
expect(validNode!.label, 'Resume.docx');
expect(invalidNode.runtimeType, Null);
});
test('...get parent', () {
TreeViewController controller = TreeViewController(children: nodes);
Node? nodeParent = controller.getParent('pd1');
Node? rootParent = controller.getParent('docs');
Node? noParent = controller.getParent('xpd1');
expect(nodeParent.runtimeType, Node);
expect(nodeParent!.key, 'd3');
expect(rootParent.runtimeType, Node);
expect(rootParent!.key, 'docs');
expect(noParent.runtimeType, Null);
});
test('...update node', () {
TreeViewController controller = TreeViewController(children: nodes);
Node? node = controller.getNode('pd1');
Node updatedNode = node!.copyWith(
key: 'pdf1',
label: 'My Resume.pdf',
);
List<Node> newChildren = controller.updateNode(node.key, updatedNode);
controller = TreeViewController(children: newChildren);
Node? validNode = controller.getNode('pdf1');
Node? invalidNode = controller.getNode('pd1');
expect(validNode.runtimeType, Node);
expect(validNode!.key, updatedNode.key);
expect(validNode.label, updatedNode.label);
expect(invalidNode.runtimeType, Null);
});
test('...delete child node', () {
TreeViewController controller = TreeViewController(children: nodes);
List<Node> newChildren = controller.deleteNode('pd1');
controller = TreeViewController(children: newChildren);
Node? invalidNode = controller.getNode('pd1');
expect(invalidNode.runtimeType, Null);
});
test('...delete parent node', () {
TreeViewController controller = TreeViewController(children: nodes);
List<Node> newChildren = controller.deleteNode('docs');
controller = TreeViewController(children: newChildren);
expect(controller.getNode('docs'), null);
expect(controller.getNode('pd1'), null);
expect(controller.getNode('pd2'), null);
expect(controller.getNode('d3'), null);
expect(controller.getNode('d1'), null);
expect(controller.getNode('d2'), null);
});
test('...add child node', () {
TreeViewController controller = TreeViewController(children: nodes);
Node? invalidNode = controller.getNode('pd3');
expect(invalidNode.runtimeType, Null);
Node newNode = Node(label: 'References.docx', key: 'pd3');
List<Node> newChildren = controller.addNode('d3', newNode);
controller = TreeViewController(children: newChildren);
Node? validNode = controller.getNode('pd3');
expect(validNode.runtimeType, Node);
expect(validNode!.label, newNode.label);
});
});
}

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_treeview/src/expander_theme_data.dart';
import 'package:flutter_treeview/src/tree_view_theme.dart';
void main() {
test('fallback', () {
final TreeViewTheme theme = TreeViewTheme.fallback();
expect(theme.colorScheme, ColorScheme.light());
expect(theme.levelPadding, 20);
expect(theme.iconTheme.isConcrete, true);
expect(theme.labelStyle, TextStyle());
});
test('copyWith', () {
TreeViewTheme theme = TreeViewTheme.fallback();
theme = theme.copyWith(
colorScheme: ColorScheme.dark(),
levelPadding: 25,
iconTheme: IconThemeData(),
expanderTheme: ExpanderThemeData(),
labelStyle: TextStyle(fontSize: 35),
);
expect(theme.colorScheme, ColorScheme.dark());
expect(theme.levelPadding, 25);
expect(theme.iconTheme.isConcrete, false);
// expect(theme.expanderTheme.isConcrete, true);
expect(theme.labelStyle.fontSize, 35);
});
test('merge', () {
TreeViewTheme theme = TreeViewTheme.fallback();
TreeViewTheme theme2 = TreeViewTheme(
colorScheme: ColorScheme.dark(),
levelPadding: 25,
iconTheme: IconThemeData(),
expanderTheme: ExpanderThemeData(),
labelStyle: TextStyle(fontSize: 35),
);
theme = theme.merge(theme2);
expect(theme.colorScheme, ColorScheme.dark());
expect(theme.levelPadding, 25);
expect(theme.iconTheme.isConcrete, false);
// expect(theme.expanderTheme.isConcrete, true);
expect(theme.labelStyle.fontSize, 35);
expect(theme, theme2);
});
test('isConcrete', () {
TreeViewTheme theme = TreeViewTheme.fallback();
TreeViewTheme theme2 = TreeViewTheme();
expect(theme == theme2, true);
});
}

File diff suppressed because it is too large Load diff

44
test/utilities_test.dart Normal file
View file

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_treeview/src/utilities.dart';
void main() {
group('random generator', () {
test('generates at least 300 unique strings', () {
List<String> _keys = [];
final int count = 300;
for (int i = 0; i < count; i++) {
_keys.add(Utilities.generateRandom());
}
expect(_keys.length, count);
expect(_keys, _keys.toSet().toList());
});
test('generates at least 200 unique 10 character strings', () {
List<String> _keys = [];
final int count = 200;
for (int i = 0; i < count; i++) {
_keys.add(Utilities.generateRandom(10));
}
expect(_keys.first.length, 10);
expect(_keys.length, count);
expect(_keys, _keys.toSet().toList());
});
});
group('truthful', () {
test('is true', () {
expect(Utilities.truthful(true), true);
expect(Utilities.truthful('true'), true);
expect(Utilities.truthful(1), true);
expect(Utilities.truthful('1'), true);
expect(Utilities.truthful('yes'), true);
});
test('is false', () {
expect(Utilities.truthful(false), false);
expect(Utilities.truthful('false'), false);
expect(Utilities.truthful(0), false);
expect(Utilities.truthful('0'), false);
expect(Utilities.truthful('no'), false);
expect(Utilities.truthful('someothervalue'), false);
});
});
}