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