intermediate

This commit is contained in:
mr
2024-08-08 08:42:32 +02:00
parent 593f03648b
commit ceeebfc964
74 changed files with 3784 additions and 634 deletions

View File

@@ -5,5 +5,5 @@ export 'src/dashboard.dart';
export 'src/elements/connection_params.dart';
export 'src/elements/flow_element.dart';
export 'src/flow_chart.dart';
export 'src/ui/draw_arrow.dart' show ArrowParams, ArrowStyle;
export 'src/ui/draw_arrow.dart' show ArrowParams, ArrowStyle, ArrowDirection;
export 'src/ui/grid_background.dart' show GridBackgroundParams;

View File

@@ -2,6 +2,7 @@
import 'dart:io';
import 'dart:convert';
import 'package:flutter_flow_chart/src/flow_chart_left_menu.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
@@ -23,10 +24,15 @@ typedef ConnectionListener = void Function(
//
class Dashboard extends ChangeNotifier {
GlobalKey<FlowChartSelectedMenuState> selectedMenuKey = GlobalKey<FlowChartSelectedMenuState>();
GlobalKey<FlowChartLeftMenuState> selectedLeftMenuKey = GlobalKey<FlowChartLeftMenuState>();
GlobalKey<FlowChartMenuState> chartMenuKey = GlobalKey<FlowChartMenuState>();
GlobalKey<ChartWidgetState> chartKey = GlobalKey<ChartWidgetState>();
List<Map<String, dynamic>> tempHistory = [];
List<Map<String, dynamic>> history = [];
Map<String, dynamic> scheduler = {};
Map<String, bool> schedulerState = {};
bool schedulerSave = false;
String? id;
String name;
String defaultName = "";
bool isMenu = true;
@@ -39,11 +45,17 @@ class Dashboard extends ChangeNotifier {
double defaultDashWidth = 0;
double defaultBackWidth = 10;
double defaultForwardWidth = 10;
final void Function()? save;
Future<void> Function(String? id)? save;
List<Widget> Function(FlowData? obj)? infoItemWidget;
List<Widget> Function()? infoWidget;
FlowData? Function(Map<String, dynamic> json)? transformToData;
///
Dashboard({
this.id,
this.transformToData,
required this.name,
this.save,
this.scheduler = const {},
Offset? handlerFeedbackOffset,
this.isMenu = true,
this.defaultDashSpace = 0,
@@ -53,6 +65,8 @@ class Dashboard extends ChangeNotifier {
this.defaultArrowDirection = ArrowDirection.forward,
this.defaultArrowStyle = ArrowStyle.curve,
this.loadedGraph,
this.infoWidget,
this.load,
}) : elements = [],
_dashboardPosition = Offset.zero,
dashboardSize = Size.zero,
@@ -80,25 +94,28 @@ class Dashboard extends ChangeNotifier {
addToHistory();
}
Future<void> Function(String cat)? load;
///
factory Dashboard.fromMap(Map<String, dynamic> map) {
final d = Dashboard(
name: map['name'] as String,
isMenu: map['isMenu'] as bool,
scheduler: map['schedule'] as Map<String, String>? ?? {},
defaultDashSpace: map['defaultDashSpace'] as double? ?? 0,
defaultDashWidth: map['defaultDashWidth'] as double? ?? 0,
defaultArrowDirection: ArrowDirection.values[
map['defaultArrowDirection'] as int? ?? 0],
defaultArrowStyle: ArrowStyle.values[map['arrowStyle'] as int? ?? 0],
)
..arrows = List<ArrowPainter>.from(
);
d..arrows = List<ArrowPainter>.from(
(map['arrows'] as List<dynamic>).map<ArrowPainter>(
(x) => ArrowPainter.fromMap(x as Map<String, dynamic>),
),
)
..elements = List<FlowElement>.from(
(map['elements'] as List<dynamic>).map<FlowElement>(
(x) => FlowElement.fromMap(x as Map<String, dynamic>),
(x) => FlowElement.fromMap(d, x as Map<String, dynamic>),
),
)
..dashboardSize = Size(
@@ -120,6 +137,7 @@ class Dashboard extends ChangeNotifier {
}
void copyFromMap(Map<String, dynamic> map) {
scheduler = map['schedule'] as Map<String, String>? ?? {};
defaultArrowStyle = ArrowStyle.values[map['arrowStyle'] as int? ?? 0];
defaultDashSpace = map['defaultDashSpace'] as double? ?? 0;
defaultDashWidth = map['defaultDashWidth'] as double? ?? 0;
@@ -132,7 +150,7 @@ class Dashboard extends ChangeNotifier {
);
elements = List<FlowElement>.from(
(map['elements'] as List<dynamic>).map<FlowElement>(
(x) => FlowElement.fromMap(x as Map<String, dynamic>),
(x) => FlowElement.fromMap(this, x as Map<String, dynamic>),
),
);
dashboardSize = Size(
@@ -148,7 +166,6 @@ class Dashboard extends ChangeNotifier {
blockDefaultZoomGestures =
(map['blockDefaultZoomGestures'] as bool? ?? false);
minimumZoomFactor = map['minimumZoomFactor'] as double? ?? 0.25;
if (save != null) { save!(); }
}
///
@@ -194,22 +211,34 @@ class Dashboard extends ChangeNotifier {
final List<ConnectionListener> _connectionListeners = [];
Map<String, dynamic> serialize() {
Map<String, dynamic> d = {};
Map<String, dynamic> graph = {};
graph['zoom'] = getZoomFactor();
graph['elements'] = {};
for(var el in elements) {
graph['elements'][el.id] = el.serialize();
}
graph['arrows'] = arrows.map((e) => e.serialize()).toList();
return graph;
d["id"]=id;
d["name"]=name;
d["graph"]=graph;
if (schedulerSave) {
d["schedule"]=scheduler;
}
return d;
}
void deserialize(Map<String, dynamic> graph) {
elements.clear();
arrows.clear();
for(var el in graph['elements'].values) {
elements = [];
arrows = [];
print(graph['schedule']);
scheduler = graph['schedule'] ?? {};
setZoomFactor(graph["graph"]?["zoom"] ?? 1.0);
for(var el in graph['graph']?['elements'] ?? []) {
List<ConnectionParams> nexts = [];
var flow = FlowElement.deserialize(el);
for(var ar in graph['arrows']) {
var flow = FlowElement.deserialize(this, el);
for(var ar in graph['graph']['arrows']) {
if (ar['from']['id'] != flow.id) { continue; }
nexts.add(ConnectionParams(
srcElementId: ar['from']['id'],
destElementId: ar['to']['id'],
@@ -221,7 +250,21 @@ class Dashboard extends ChangeNotifier {
));
}
flow.next = nexts;
List<FlowData> arr = [];
for (var cat in chartKey.currentState?.widget.flowChart.widget.categories ?? []) {
for (var build in chartKey.currentState!.widget.flowChart.widget.draggableItemBuilder(cat)) {
arr.add(build);
}
}
try {
FlowData data = arr.firstWhere((element) => element.getID() == flow.element?.getID());
flow.kind = ElementKind.widget;
flow.widget = chartKey.currentState?.widget.flowChart.widget.itemWidget(data);
} catch (e) { print(e); }
elements.add(flow);
}
selectedMenuKey.currentState?.setState(() { });
chartMenuKey.currentState?.setState(() { });
}
/// add listener called when a new connection is created
@@ -249,7 +292,7 @@ class Dashboard extends ChangeNotifier {
void addToHistory() {
if (tempHistory.length >= 50) { tempHistory.removeAt(0); }
tempHistory.add(toMap());
if (save != null) { save!(); }
if (save != null) { save!(id); }
history = tempHistory.map((e) => e).toList();
chartKey.currentState?.setState(() { });
chartMenuKey.currentState?.setState(() { });
@@ -774,7 +817,7 @@ class Dashboard extends ChangeNotifier {
final loadedElements = List<FlowElement>.from(
(source['elements'] as List<dynamic>).map<FlowElement>(
(x) => FlowElement.fromMap(x as Map<String, dynamic>),
(x) => FlowElement.fromMap(this, x as Map<String, dynamic>),
),
);
elements

View File

@@ -4,7 +4,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
import 'package:flutter_flow_chart/src/elements/connection_params.dart';
import 'package:flutter_flow_chart/src/dashboard.dart';
import 'package:uuid/uuid.dart';
/// Kinf od element
@@ -61,12 +61,16 @@ enum Handler {
}
/// Class to store [ElementWidget]s and notify its changes
class FlowElement extends ChangeNotifier {
class FlowElement<T extends FlowData> extends ChangeNotifier {
Dashboard dashboard;
bool isSelected = false;
T? element;
///
FlowElement({
required this.dashboard,
Offset position = Offset.zero,
String? id,
this.element,
this.size = Size.zero,
this.text = '',
this.textColor = Colors.black,
@@ -103,8 +107,12 @@ class FlowElement extends ChangeNotifier {
}
return false;
}
factory FlowElement.fromMap(Map<String, dynamic> map) {
final e = FlowElement(
factory FlowElement.fromMap(Dashboard dashboard, Map<String, dynamic> map) {
final e = FlowElement<T>(
element: (dashboard.transformToData != null
? dashboard.transformToData!(map['element'] ?? {})
: null) as T?,
dashboard: dashboard,
widget: map['widget'] as Widget?,
size: Size(map['size.width'] as double, map['size.height'] as double),
text: map['text'] as String,
@@ -141,8 +149,8 @@ class FlowElement extends ChangeNotifier {
///
factory FlowElement.fromJson(String source) =>
FlowElement.fromMap(json.decode(source) as Map<String, dynamic>);
factory FlowElement.fromJson(Dashboard dashboard, String source) =>
FlowElement.fromMap(dashboard, json.decode(source) as Map<String, dynamic>);
/// Unique id set when adding a [FlowElement] with [Dashboard.addElement()]
late String id;
@@ -365,14 +373,18 @@ class FlowElement extends ChangeNotifier {
graphElement['y'] = position.dy;
graphElement['width'] = size.width;
graphElement['height'] = size.height;
graphElement['element']=element?.serialize();
return graphElement;
}
static FlowElement deserialize(Map<String, dynamic> map) {
return FlowElement(
id: map['id'],
position: Offset(map['x'], map['y']),
size: Size(map['width'], map['height']),
);
}
static FlowElement deserialize<T extends FlowData>(Dashboard dashboard, Map<String, dynamic> map) {
return FlowElement<T>(
dashboard: dashboard,
id: map['id'],
kind: ElementKind.widget,
position: Offset(double.parse("${map['x']}"), double.parse("${map['y']}")),
size: Size(double.parse("${map['width']}"), double.parse("${map['height']}")),
element: (dashboard.transformToData != null ? dashboard.transformToData!(map['element'] ?? {}) : null) as T?,
);
}
}

View File

@@ -1,10 +1,11 @@
// ignore: directives_ordering
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
import 'package:flutter_flow_chart/src/dashboard.dart';
import 'package:flutter_flow_chart/src/elements/flow_element.dart';
import 'package:flutter_flow_chart/src/flow_chart_left_menu.dart';
import 'package:flutter_flow_chart/src/flow_chart_menu.dart';
import 'package:flutter_flow_chart/src/flow_chart_selected_menu.dart';
import 'package:flutter_flow_chart/src/ui/draw_arrow.dart';
@@ -15,7 +16,14 @@ import 'package:uuid/uuid.dart';
/// Main flow chart Widget.
/// It displays the background grid, all the elements and connection lines
class FlowChart<T extends Object> extends StatefulWidget {
abstract class FlowData {
String getID();
String getName();
Map<String, dynamic> serialize();
FlowData deserialize(Map<String, dynamic> data);
}
class FlowChart<T extends FlowData> extends StatefulWidget {
FlowChart({
required this.dashboard,
required this.itemWidget,
@@ -44,13 +52,15 @@ class FlowChart<T extends Object> extends StatefulWidget {
this.categories = const [],
required this.draggableItemBuilder,
this.onDashboardAlertOpened,
this.menuExtension,
}) {}
final List<String> categories;
final double width;
final double height;
final double innerMenuWidth;
Widget Function()? menuExtension;
double itemWidth = 80;
double zoom = 1;
@@ -161,7 +171,6 @@ class HoverMenuController {
currentState?.hideSubMenu();
}
}
bool isPopUp = false;
class HoverMenu extends StatefulWidget {
final Widget title;
final double? width;
@@ -277,49 +286,8 @@ class HoverMenuState extends State<HoverMenu> {
}
var node = FocusNode();
class FlowChartState<T extends Object> extends State<FlowChart> {
List<Draggable<T>> getDraggable(List<T> items) {
List<Draggable<T>> res = [];
double realSize = widget.itemWidth * widget.zoom;
GlobalKey<HoverMenuState> hoverKey = GlobalKey<HoverMenuState>();
for (var e in items) {
res.add(Draggable<T>(
// Data is the value this Draggable stores.
data: e,
onDragStarted: () => hoverKey.currentState?.hideSubMenu(),
onDragEnd: (d) => node.requestFocus(),
childWhenDragging: Opacity(opacity: .5,
child: Padding( padding: const EdgeInsets.all(10),
child: Container( height: realSize - 20, child: widget.itemWidget(e) ))),
feedback: Container( height: realSize, child: widget.itemWidget(e) ),
child: InkWell( mouseCursor: SystemMouseCursors.grab, child: Padding( padding: const EdgeInsets.all(10),
child: widget.itemWidgetTooltip != null ? HoverMenu( key: hoverKey, width: 400, title: Container(
height: realSize - 20, child: widget.itemWidget(e) ),
items: [
Container(child: widget.itemWidgetTooltip!(e)),
]
) : Container(
height: realSize - 20, child: widget.itemWidget(e)
)
) )));
}
if (!widget.dashboard.isOpened && widget.onDashboardAlertOpened != null && isPopUp == false) {
isPopUp = true;
widget.dashboard.isOpened = true;
Future.delayed(Duration(milliseconds: 1), () => showDialog(
barrierDismissible: false,
context: context, builder: (context) {
return AlertDialog(
titlePadding: EdgeInsets.zero,
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0)),
title: widget.onDashboardAlertOpened!(context, widget.dashboard));
}));
}
widget.dashboard.isOpened = true;
return res;
}
class FlowChartState<T extends FlowData> extends State<FlowChart> {
@override
void initState() {
@@ -342,48 +310,14 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
}
}
});
// disabling default browser context menu on web
if (kIsWeb) BrowserContextMenu.disableContextMenu();
List<Widget> menuItems = [];
for (var cat in widget.categories) {
menuItems.add(
Container( width: widget.dashboard.isMenu ? widget.innerMenuWidth : 0, margin: const EdgeInsets.only(bottom: 0.3),
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey, width: .5))),
child: Stack( children: [
widget.dashboard.isMenu && widget.innerMenuWidth < 200 ? Wrap( alignment: WrapAlignment.start,
children: getDraggable(widget.draggableItemBuilder(cat) as List<T>))
: ExpansionTile(
shape: const ContinuousRectangleBorder(side: BorderSide(color: Colors.transparent)),
initiallyExpanded: true,
title: SizedBox(
child : Row( children: [
Padding(padding: const EdgeInsets.only(right: 10),
child: Icon(cat.toUpperCase().contains("DATA") ? Icons.grid_on : Icons.bookmark, color: Colors.grey)),
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 5),
child: Text(cat.toUpperCase(), overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.black, fontSize: 11, fontWeight: FontWeight.w500))))
])
),
iconColor: Colors.white,
collapsedIconColor: Colors.white,
children: [
Container( width: widget.dashboard.isMenu ? widget.innerMenuWidth : 0, color: Colors.white,
child : Wrap( alignment: WrapAlignment.start,
children: getDraggable(widget.draggableItemBuilder(cat) as List<T>))
)],
)
]
))
);
}
return KeyboardListener(
focusNode: node,
onKeyEvent: (event) {
bool change = false;
if ((event is KeyDownEvent || event is KeyRepeatEvent) && event.logicalKey == LogicalKeyboardKey.arrowUp) {
change = true;
for (var el in widget.dashboard.elements) {
if (el.isSelected) {
el.position = Offset(el.position.dx, el.position.dy - 10);
@@ -391,6 +325,7 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
}
}
if ((event is KeyDownEvent || event is KeyRepeatEvent) && event.logicalKey == LogicalKeyboardKey.arrowDown) {
change = true;
for (var el in widget.dashboard.elements) {
if (el.isSelected) {
el.position = Offset(el.position.dx, el.position.dy + 10);
@@ -398,6 +333,7 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
}
}
if ((event is KeyDownEvent || event is KeyRepeatEvent) && event.logicalKey == LogicalKeyboardKey.arrowLeft) {
change = true;
for (var el in widget.dashboard.elements) {
if (el.isSelected) {
el.position = Offset(el.position.dx - 10, el.position.dy);
@@ -405,6 +341,7 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
}
}
if ((event is KeyDownEvent || event is KeyRepeatEvent) && event.logicalKey == LogicalKeyboardKey.arrowRight) {
change = true;
for (var el in widget.dashboard.elements) {
if (el.isSelected) {
el.position = Offset(el.position.dx + 10, el.position.dy);
@@ -413,8 +350,11 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
}
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.add) {
change = true;
for (var el in widget.dashboard.elementSelected) {
widget.dashboard.elements.add(FlowElement(
widget.dashboard.elements.add(FlowElement<T>(
element: el.element as T,
dashboard: widget.dashboard,
id: const Uuid().v4(),
position: el.position + const Offset(100, 100),
size: el.size,
@@ -427,6 +367,7 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
}
}
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.delete) {
change = true;
widget.dashboard.elements.removeWhere( (el) => el.isSelected );
for (var arrow in widget.dashboard.arrowsSelected) {
for (var el in widget.dashboard.elements.where((element) => element.id == arrow.fromID.split("_")[0])) {
@@ -435,11 +376,13 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
}
widget.dashboard.arrows.removeWhere( (el) => el.isSelected );
}
DrawingArrow.instance.notifyListeners();
widget.dashboard.chartKey.currentState?.setState(() { });
Future.delayed(Duration(milliseconds: 10), () {
node.requestFocus();
});
if (change) {
DrawingArrow.instance.notifyListeners();
widget.dashboard.chartKey.currentState?.setState(() { });
/*Future.delayed(Duration(milliseconds: 10), () {
node.requestFocus();
});*/
}
},
child: ClipRect(
child: Stack(
@@ -456,7 +399,7 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
return SizedBox(
width: widget.width,
height: widget.height,
child: ChartWidget(
child: ChartWidget<T>(
key: widget.dashboard.chartKey,
flowChart: this,
dashboard: widget.dashboard,
@@ -478,8 +421,10 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
onAcceptWithDetails: (DragTargetDetails<T> details) {
var e = details.data;
String newID = const Uuid().v4();
FlowElement el = FlowElement(
FlowElement<T> el = FlowElement<T>(
dashboard: widget.dashboard,
id: newID,
element: e,
position: details.offset,
size: const Size(100, 100),
text: '${widget.dashboard.elements.length}',
@@ -497,16 +442,70 @@ class FlowChartState<T extends Object> extends State<FlowChart> {
},
))]
),
widget.dashboard.isMenu ? Positioned(top: 50, child: Container(
height: widget.height - 50,
constraints: BoxConstraints(minWidth: widget.itemWidth),
width: widget.dashboard.isMenu ? widget.innerMenuWidth : 0,
color: Colors.grey.shade300,
child: SingleChildScrollView( child: Column( children: menuItems ) )
)) : Container(),
widget.dashboard.isMenu ? Positioned(top: 50, child: FlowChartLeftMenu<T>(
key: widget.dashboard.selectedLeftMenuKey,
dashboard: widget.dashboard,
categories: widget.categories,
height: widget.height,
innerMenuWidth: widget.innerMenuWidth,
itemWidth: widget.itemWidth,
menuExtension: widget.menuExtension,
draggableItemBuilder: widget.draggableItemBuilder as List<T> Function(String cat),
getDraggable: getDraggable,
) )
: Container(),
widget.dashboard.isInfo ? Positioned(top: 50, right: 0, child:
FlowChartSelectedMenu(
key: widget.dashboard.selectedMenuKey,
dashboard: widget.dashboard,
height: MediaQuery.of(context).size.height - 100
)
) : Container()
])
));
}
List<Draggable<T>> getDraggable(List<T> items) {
List<Draggable<T>> res = [];
double realSize = widget.itemWidth * widget.zoom;
for (var e in items) {
GlobalKey<HoverMenuState> hoverKey = GlobalKey<HoverMenuState>();
res.add(Draggable<T>(
// Data is the value this Draggable stores.
data: e,
onDragStarted: () => hoverKey.currentState?.hideSubMenu(),
onDragEnd: (d) => node.requestFocus(),
childWhenDragging: Opacity(opacity: .5,
child: Padding( padding: const EdgeInsets.all(10),
child: Container( height: realSize - 20, child: widget.itemWidget(e) ))),
feedback: Container( height: realSize, child: widget.itemWidget(e) ),
child: InkWell( mouseCursor: SystemMouseCursors.grab, child: Padding( padding: const EdgeInsets.all(10),
child: widget.itemWidgetTooltip != null ? HoverMenu( key: hoverKey, width: 400, title: Container(
height: realSize - 20, child: widget.itemWidget(e) ),
items: [
Container(child: widget.itemWidgetTooltip!(e)),
]
) : Container(
height: realSize - 20, child: widget.itemWidget(e)
)
) )));
}
if (!widget.dashboard.isOpened && widget.onDashboardAlertOpened != null ) {
Future.delayed(Duration(milliseconds: 100), () {
showDialog(
barrierDismissible: false,
context: context, builder: (context) {
return AlertDialog(
titlePadding: EdgeInsets.zero,
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0)),
title: widget.onDashboardAlertOpened!(context, widget.dashboard));
}); });
}
return res;
}
}
/// Widget to draw interactive connection when the user tap on handlers
@@ -553,8 +552,7 @@ class _DrawingArrowWidgetState extends State<DrawingArrowWidget> {
}
}
class ChartWidget extends StatefulWidget {
class ChartWidget<T extends FlowData> extends StatefulWidget {
ChartWidget ({ Key? key,
required this.flowChart,
this.onElementPressed,
@@ -667,9 +665,9 @@ class ChartWidget extends StatefulWidget {
final Dashboard dashboard;
@override ChartWidgetState createState() => ChartWidgetState();
@override ChartWidgetState createState() => ChartWidgetState<T>();
}
class ChartWidgetState extends State<ChartWidget> {
class ChartWidgetState<T extends FlowData> extends State<ChartWidget> {
bool hoverImportant = false;
final segmentedTension = ValueNotifier<double>(1);
@@ -710,8 +708,7 @@ class ChartWidgetState extends State<ChartWidget> {
var secondaryTapDownPos = Offset.zero;
for (int i = 0; i < widget.dashboard.elements.length; i++)
widget.dashboard.elements[i].next.removeWhere((element) =>
widget.dashboard.findElementIndexById(element.destElementId) < 0
);
widget.dashboard.findElementIndexById(element.destElementId) < 0);
return Stack( children: [
Positioned.fill(
child: GestureDetector(
@@ -801,7 +798,7 @@ class ChartWidgetState extends State<ChartWidget> {
),
// Draw elements
for (int i = 0; i < widget.dashboard.elements.length; i++)
ElementWidget(
ElementWidget<T>(
key: UniqueKey(),
dashboard: widget.dashboard,
element: widget.dashboard.elements.elementAt(i),
@@ -904,10 +901,6 @@ class ChartWidgetState extends State<ChartWidget> {
dashboard: widget.dashboard,
width: MediaQuery.of(context).size.width)
),
widget.dashboard.isInfo ? Positioned(top: 50, right: 0, child:
FlowChartSelectedMenu(key: widget.dashboard.selectedMenuKey, chart: this,
dashboard: widget.dashboard, height: MediaQuery.of(context).size.height - 100)
) : Container()
],
);
}

View File

@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
class FlowChartLeftMenu<T extends FlowData> extends StatefulWidget {
Dashboard dashboard;
List<String> categories;
double height;
double itemWidth;
double innerMenuWidth;
Widget Function()? menuExtension;
TextEditingController ctrl = TextEditingController();
final List<T> Function(String cat) draggableItemBuilder;
List<Draggable<T>> Function(List<T> items) getDraggable;
FlowChartLeftMenu ({ super.key, required this.categories, required this.dashboard,
this.height = 100,
this.innerMenuWidth = 100,
this.itemWidth = 100,
this.menuExtension,
required this.getDraggable,
required this.draggableItemBuilder
});
@override FlowChartLeftMenuState createState() => FlowChartLeftMenuState();
}
class FlowChartLeftMenuState<T extends FlowData> extends State<FlowChartLeftMenu> {
@override Widget build(BuildContext context) {
List<Widget> menuItems = [];
for (var cat in widget.categories) {
if (widget.draggableItemBuilder(cat).isEmpty) continue;
var items = widget.draggableItemBuilder(cat).where(
(element) => element.getName().toLowerCase().contains(widget.ctrl.value.text.toLowerCase())).toSet().toList();
menuItems.add(
Container( width: widget.dashboard.isMenu ? widget.innerMenuWidth : 0, margin: const EdgeInsets.only(bottom: 0.3),
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey, width: .5))),
child: Stack( children: [
widget.dashboard.isMenu && widget.innerMenuWidth < 200 ? Wrap( alignment: WrapAlignment.start,
children: widget.getDraggable(items))
: ExpansionTile(
shape: const ContinuousRectangleBorder(side: BorderSide(color: Colors.transparent)),
initiallyExpanded: true,
title: SizedBox(
child : Row( children: [
Padding(padding: const EdgeInsets.only(right: 10),
child: Icon(cat.toUpperCase().contains("DATA") ? Icons.grid_on : Icons.bookmark, color: Colors.grey)),
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 5),
child: Text(cat.toUpperCase(), overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.black, fontSize: 11, fontWeight: FontWeight.w500))))
])
),
iconColor: Colors.white,
collapsedIconColor: Colors.white,
children: [
Container( width: widget.dashboard.isMenu ? widget.innerMenuWidth : 0, color: Colors.white,
child : Wrap( alignment: WrapAlignment.center,
children: widget.getDraggable(items))
)],
)
]
))
);
}
return Container(
height: widget.height - 50,
color: Colors.grey.shade300,
child: Stack( children: [
Container(
width: widget.innerMenuWidth,
height: 50,
decoration: const BoxDecoration(border: Border(left: BorderSide(color: Colors.white))),
child: TextFormField(
style: const TextStyle(color: Colors.black, fontSize: 15),
cursorColor: const Color.fromARGB(38, 166, 154, 1),
controller: widget.ctrl,
onChanged: (value) { setState(() { }); },
decoration: InputDecoration(
hintText: "search item...",
fillColor: Colors.white,
filled: true,
contentPadding: const EdgeInsets.only(left: 30, right: 30, top: 5, bottom: 5),
hintStyle: TextStyle(
color: Colors.grey,
fontSize: 15,
fontWeight: FontWeight.w400
),
border: InputBorder.none
)
)
),
Container(
margin: EdgeInsets.only(top: 50),
height: widget.height - 150,
constraints: BoxConstraints(minWidth: widget.itemWidth),
width: widget.dashboard.isMenu ? widget.innerMenuWidth : 0,
color: Colors.grey.shade300,
child: SingleChildScrollView( child: Column( children: [
...menuItems
]) )
),
Positioned( bottom: 0, height: 50,
child: widget.menuExtension != null ? widget.menuExtension!() : Container(),),
])
);
}
}

View File

@@ -35,6 +35,7 @@ class FlowChartMenuState extends State<FlowChartMenu> {
child: InkWell( mouseCursor: SystemMouseCursors.click,
onTap: () {
widget.dashboard.defaultName = "graph_${DateTime.now().toString().replaceAll(" ", "_").substring(0, DateTime.now().toString().length - 7)}";
widget.dashboard.isOpened = true;
showDialog(
barrierDismissible: false,
context: context, builder: (context) {

View File

@@ -6,16 +6,78 @@ import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:number_text_input_formatter/number_text_input_formatter.dart';
class FlowChartSelectedMenu extends StatefulWidget {
ChartWidgetState chart;
Dashboard dashboard;
double height = 100;
FlowChartSelectedMenu ({ super.key, required this.chart, required this.dashboard, this.height = 100 });
bool isDashboardInfo = true;
FlowChartSelectedMenu ({ super.key, required this.dashboard, this.height = 100 });
@override FlowChartSelectedMenuState createState() => FlowChartSelectedMenuState();
}
class FlowChartSelectedMenuState extends State<FlowChartSelectedMenu> {
@override Widget build(BuildContext context) {
return Container( // SHORTCUT
Widget? w;
if (widget.isDashboardInfo && widget.dashboard.elementSelected.length == 1) {
w = Container(
width: 200,
height: widget.height,
color: Colors.grey.shade300,
child: Column( children: [ ...widget.dashboard.infoItemWidget != null ?
widget.dashboard.infoItemWidget!(widget.dashboard.elementSelected.first.element)
: [],
widget.dashboard.arrowsSelected.isNotEmpty || widget.dashboard.elementSelected.isNotEmpty ? Container(
width: 200,
margin: EdgeInsets.only(top: 15),
decoration: BoxDecoration(border: Border(
top: BorderSide(color: Colors.grey, width: 1),
bottom: BorderSide(color: Colors.grey, width: 1))),
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
Tooltip( message: "remove",
child: InkWell( mouseCursor: SystemMouseCursors.click,
onTap: () {
widget.dashboard.arrows.removeWhere((element) {
if (element.isSelected && element.elementIndex != null && element.connIndex != null) {
widget.dashboard.elements[element.elementIndex!].next.removeAt(element.connIndex!);
}
return element.isSelected;
});
widget.dashboard.elements.removeWhere((element) => element.isSelected);
Future.delayed(Duration(milliseconds: 100), () {
widget.dashboard.chartKey.currentState?.setState(() { });
});
}, child: Container( margin: EdgeInsets.all(10),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), border: Border.all(color: Colors.black, width: 1)),
width: 140, height: 30,
child: Icon(Icons.delete_outline, color: Colors.black),
))
),
Tooltip( message: "copy",
child: InkWell( mouseCursor: SystemMouseCursors.click,
onTap: () {
for (var sel in widget.dashboard.elementSelected) {
widget.dashboard.elements.add(FlowElement.fromMap(widget.dashboard, sel.toMap()));
widget.dashboard.elements.last.position += Offset(50, 50);
}
Future.delayed(Duration(milliseconds: 100), () {
widget.dashboard.chartKey.currentState?.setState(() { });
});
}, child: Container( margin: EdgeInsets.all(10),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), border: Border.all(color: Colors.black, width: 1)),
width: 140, height: 30,
child: Icon(Icons.copy, color: Colors.black),
))
),
])
) : Container()
])
);
} else if (widget.isDashboardInfo && widget.dashboard.infoWidget != null) {
w = Container(
width: 200,
height: widget.height,
color: Colors.grey.shade300,
child: Column( children: widget.dashboard.infoWidget != null ? widget.dashboard.infoWidget!() : [])
);
} else {
w = Container( // SHORTCUT
width: 200,
height: widget.height,
color: Colors.grey.shade300,
@@ -292,6 +354,9 @@ class FlowChartSelectedMenuState extends State<FlowChartSelectedMenu> {
)))
]),
])),
widget.dashboard.elementSelected.isNotEmpty && widget.dashboard.elementSelected.length == 1 ? Container(
// TODO : TEST OMG
) : Container(),
widget.dashboard.elementSelected.isNotEmpty ? Container() : Container( padding: EdgeInsets.only(left: 10, right: 10, bottom: 20, top: 15),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey, width: 1))),
child: Column( children: [
@@ -477,7 +542,7 @@ class FlowChartSelectedMenuState extends State<FlowChartSelectedMenu> {
child: InkWell( mouseCursor: SystemMouseCursors.click,
onTap: () {
for (var sel in widget.dashboard.elementSelected) {
widget.dashboard.elements.add(FlowElement.fromMap(sel.toMap()));
widget.dashboard.elements.add(FlowElement.fromMap(widget.dashboard, sel.toMap()));
widget.dashboard.elements.last.position += Offset(50, 50);
}
Future.delayed(Duration(milliseconds: 100), () {
@@ -494,6 +559,35 @@ class FlowChartSelectedMenuState extends State<FlowChartSelectedMenu> {
])))
])
);
}
return Column( children: [
Container( // SHORTCUT
width: 200,
height: 50,
decoration: BoxDecoration(color: Colors.grey.shade300, border: Border(bottom: BorderSide(color: Colors.grey, width: 1))),
child: Row( children: [
Tooltip(
message: "dashboard information",
child: InkWell( onTap: () => setState(() {widget.isDashboardInfo = true; }),
mouseCursor: SystemMouseCursors.click,
child: Container( alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 10),
color: widget.isDashboardInfo ? Colors.grey : Colors.grey.shade300,
width: 100, child: Icon(Icons.info, color: Colors.white))
)
),
Tooltip(
message: "element style",
child: InkWell( onTap: () => setState(() {widget.isDashboardInfo = false; }),
mouseCursor: SystemMouseCursors.click,
child: Container( alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 10),
color: !widget.isDashboardInfo ? Colors.grey : Colors.grey.shade300,
width: 100, child: Icon(Icons.format_paint, color: Colors.white)),
))
])),
w
]);
}
}

View File

@@ -3,6 +3,8 @@ import 'dart:convert';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
import 'package:flutter_flow_chart/src/ui/segment_handler.dart';
@@ -349,6 +351,7 @@ class DrawArrowState extends State<DrawArrow> {
if ( widget.flow.widget.dashboard.arrows.where(
(element) => element.fromID == "${widget.srcElement.id}_${widget.index}").isEmpty) {
widget.flow.widget.dashboard.arrows.add(painter);
widget.flow.widget.dashboard.save!(widget.flow.widget.dashboard.id);
} else {
var i = widget.flow.widget.dashboard.arrows.indexWhere(
(element) => element.fromID == "${widget.srcElement.id}_${widget.index}");
@@ -416,24 +419,24 @@ class ArrowInfoWidgetState extends State<ArrowInfoWidget> {
/// [ArrowParams.startArrowPosition] and
/// [ArrowParams.endArrowPosition] alignment.
class ArrowPainter extends CustomPainter {
final String fromID;
final String toID;
String fromID;
String toID;
bool isSelected = false;
///
ArrowPainter({
this.elementIndex,
this.connIndex,
required this.toID,
required this.fromID,
this.toID = "",
this.fromID = "",
this.isSelected = false,
required this.params,
required this.from,
required this.to,
this.from = Offset.zero,
this.to = Offset.zero,
List<Pivot>? pivots,
}) : pivots = pivots ?? [];
///
final ArrowParams params;
ArrowParams params;
///
Offset from;
int? elementIndex;
@@ -457,6 +460,15 @@ class ArrowPainter extends CustomPainter {
final arrowSize = 15;
final arrowAngle= 25 * math.pi / 180;
ArrowPainter deserialize(Map<String, dynamic> map) {
params = ArrowParams.fromMap(map['params']);
fromID = map['from']['id'];
toID = map['to']['id'];
from = Offset(map['from']['x'], map['from']['y']);
to = Offset(map['to']['x'], map['to']['y']);
return this;
}
Map<String, dynamic> serialize() {
Map<String, dynamic> graphElement = {};
graphElement['from'] = { "id" : fromID.split("_")[0], "x" : from.dx, "y" : from.dy };

View File

@@ -12,7 +12,7 @@ import 'package:flutter_flow_chart/src/ui/resize_widget.dart';
import 'package:flutter_flow_chart/src/objects/any_widget.dart';
/// Widget that use [element] properties to display it on the dashboard scene
class ElementWidget extends StatefulWidget {
class ElementWidget<T extends FlowData> extends StatefulWidget {
///
ElementWidget({
required this.dashboard,
@@ -84,10 +84,10 @@ class ElementWidget extends StatefulWidget {
)? onHandlerSecondaryLongTapped;
@override
State<ElementWidget> createState() => ElementWidgetState();
State<ElementWidget> createState() => ElementWidgetState<T>();
}
class ElementWidgetState extends State<ElementWidget> {
class ElementWidgetState<T extends FlowData> extends State<ElementWidget> {
// local widget touch position when start dragging
Offset delta = Offset.zero;
@@ -219,6 +219,7 @@ class ElementWidgetState extends State<ElementWidget> {
sel.changePosition(sel.position + diff);
}
}
widget.dashboard.save!(widget.dashboard.id);
},
),
),
@@ -228,7 +229,8 @@ class ElementWidgetState extends State<ElementWidget> {
element: widget.element,
dashboard: widget.dashboard,
handlerColor: widget.isHovered ? Color.fromRGBO(38, 166, 154, 1) : Colors.transparent,
child: w );
child: w
);
return Transform.translate(
offset: widget.element.position,
transformHitTests: true,
@@ -245,7 +247,9 @@ class ElementWidgetState extends State<ElementWidget> {
widget.dashboard.removeElement(widget.element);
}, icon: Icon(Icons.delete_outline)),
IconButton(tooltip: "copy element", onPressed: () {
FlowElement newElement = FlowElement(
FlowElement<T> newElement = FlowElement<T>(
element: widget.element.element as T?,
dashboard: widget.dashboard,
kind: widget.element.kind,
position: widget.element.position + Offset(widget.element.size.width, widget.element.size.height),
size: widget.element.size,