Initial commit
This commit is contained in:
commit
6d8b63618b
26 changed files with 4424 additions and 0 deletions
75
.gitignore
vendored
Normal file
75
.gitignore
vendored
Normal 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
10
.metadata
Normal 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
144
CHANGELOG.md
Normal 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
21
LICENSE
Normal 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
172
README.md
Normal 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
1
builddocs.sh
Executable file
|
@ -0,0 +1 @@
|
|||
dartdoc --include flutter_treeview ./
|
12
example/README.md
Normal file
12
example/README.md
Normal 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
|
||||

|
||||
|
||||

|
9
lib/flutter_treeview.dart
Normal file
9
lib/flutter_treeview.dart
Normal 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';
|
133
lib/src/expander_theme_data.dart
Normal file
133
lib/src/expander_theme_data.dart
Normal 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
179
lib/src/models/node.dart
Normal 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
479
lib/src/tree_node.dart
Normal 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
142
lib/src/tree_view.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
487
lib/src/tree_view_controller.dart
Normal file
487
lib/src/tree_view_controller.dart
Normal 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);
|
||||
}
|
||||
}
|
191
lib/src/tree_view_theme.dart
Normal file
191
lib/src/tree_view_theme.dart
Normal 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
681
lib/src/utilities.dart
Normal 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
146
pubspec.lock
Normal 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
23
pubspec.yaml
Normal 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
|
1
res/values/strings_en.arb
Normal file
1
res/values/strings_en.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
BIN
screenshots/ss1.gif
Normal file
BIN
screenshots/ss1.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 386 KiB |
BIN
screenshots/ss2.png
Normal file
BIN
screenshots/ss2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 528 KiB |
44
test/expander_theme_data_test.dart
Normal file
44
test/expander_theme_data_test.dart
Normal 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
132
test/node_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
95
test/tree_view_controller_test.dart
Normal file
95
test/tree_view_controller_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
51
test/tree_view_theme_test.dart
Normal file
51
test/tree_view_theme_test.dart
Normal 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);
|
||||
});
|
||||
}
|
1152
test/utilities_color_test.dart
Normal file
1152
test/utilities_color_test.dart
Normal file
File diff suppressed because it is too large
Load diff
44
test/utilities_test.dart
Normal file
44
test/utilities_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue