Dashboard chart flow
This commit is contained in:
787
library/flutter_flow_chart/lib/src/dashboard.dart
Executable file
787
library/flutter_flow_chart/lib/src/dashboard.dart
Executable file
@@ -0,0 +1,787 @@
|
||||
// ignore_for_file: avoid_positional_boolean_parameters
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.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';
|
||||
import 'package:flutter_flow_chart/src/ui/segment_handler.dart';
|
||||
|
||||
/// Listener definition for a new connection
|
||||
typedef ConnectionListener = void Function(
|
||||
FlowElement srcElement,
|
||||
FlowElement destElement,
|
||||
);
|
||||
|
||||
/// Class to store all the scene elements.
|
||||
/// This also acts as the controller to the flow_chart widget
|
||||
/// It notifies changes to [FlowChart]
|
||||
//
|
||||
class Dashboard extends ChangeNotifier {
|
||||
GlobalKey<FlowChartSelectedMenuState> selectedMenuKey = GlobalKey<FlowChartSelectedMenuState>();
|
||||
GlobalKey<FlowChartMenuState> chartMenuKey = GlobalKey<FlowChartMenuState>();
|
||||
GlobalKey<ChartWidgetState> chartKey = GlobalKey<ChartWidgetState>();
|
||||
List<Map<String, dynamic>> tempHistory = [];
|
||||
List<Map<String, dynamic>> history = [];
|
||||
String name;
|
||||
String defaultName = "";
|
||||
bool isMenu = true;
|
||||
bool isInfo = true;
|
||||
bool isOpened = false;
|
||||
Color defaultColor = Colors.black;
|
||||
Map<String, dynamic>? loadedGraph;
|
||||
double defaultStroke = 1.7;
|
||||
double defaultDashSpace = 0;
|
||||
double defaultDashWidth = 0;
|
||||
double defaultBackWidth = 10;
|
||||
double defaultForwardWidth = 10;
|
||||
final void Function()? save;
|
||||
///
|
||||
Dashboard({
|
||||
required this.name,
|
||||
this.save,
|
||||
Offset? handlerFeedbackOffset,
|
||||
this.isMenu = true,
|
||||
this.defaultDashSpace = 0,
|
||||
this.defaultDashWidth = 0,
|
||||
this.blockDefaultZoomGestures = false,
|
||||
this.minimumZoomFactor = 0.25,
|
||||
this.defaultArrowDirection = ArrowDirection.forward,
|
||||
this.defaultArrowStyle = ArrowStyle.curve,
|
||||
this.loadedGraph,
|
||||
}) : elements = [],
|
||||
_dashboardPosition = Offset.zero,
|
||||
dashboardSize = Size.zero,
|
||||
gridBackgroundParams = GridBackgroundParams() {
|
||||
// This is a workaround to set the handlerFeedbackOffset
|
||||
// to improve the user experience on devices with touch screens
|
||||
// This will prevent the handler being covered by user's finger
|
||||
defaultName = name;
|
||||
if (loadedGraph != null) { deserialize(loadedGraph!); }
|
||||
if (handlerFeedbackOffset != null) {
|
||||
this.handlerFeedbackOffset = handlerFeedbackOffset;
|
||||
} else {
|
||||
if (kIsWeb) {
|
||||
this.handlerFeedbackOffset = Offset.zero;
|
||||
} else {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
this.handlerFeedbackOffset = const Offset(0, -50);
|
||||
} else {
|
||||
this.handlerFeedbackOffset = Offset.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
tempHistory = [];
|
||||
history = [];
|
||||
addToHistory();
|
||||
}
|
||||
|
||||
///
|
||||
factory Dashboard.fromMap(Map<String, dynamic> map) {
|
||||
final d = Dashboard(
|
||||
name: map['name'] as String,
|
||||
isMenu: map['isMenu'] as bool,
|
||||
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(
|
||||
(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>),
|
||||
),
|
||||
)
|
||||
..dashboardSize = Size(
|
||||
map['dashboardSizeWidth'] as double? ?? 0,
|
||||
map['dashboardSizeHeight'] as double? ?? 0,
|
||||
);
|
||||
|
||||
if (map['gridBackgroundParams'] != null) {
|
||||
d.gridBackgroundParams = GridBackgroundParams.fromMap(
|
||||
map['gridBackgroundParams'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
d
|
||||
..blockDefaultZoomGestures =
|
||||
(map['blockDefaultZoomGestures'] as bool? ?? false)
|
||||
..minimumZoomFactor = map['minimumZoomFactor'] as double? ?? 0.25;
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
void copyFromMap(Map<String, dynamic> map) {
|
||||
defaultArrowStyle = ArrowStyle.values[map['arrowStyle'] as int? ?? 0];
|
||||
defaultDashSpace = map['defaultDashSpace'] as double? ?? 0;
|
||||
defaultDashWidth = map['defaultDashWidth'] as double? ?? 0;
|
||||
defaultArrowDirection = ArrowDirection.values[
|
||||
map['defaultArrowDirection'] as int? ?? 0];
|
||||
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>),
|
||||
),
|
||||
);
|
||||
dashboardSize = Size(
|
||||
map['dashboardSizeWidth'] as double? ?? 0,
|
||||
map['dashboardSizeHeight'] as double? ?? 0,
|
||||
);
|
||||
|
||||
if (map['gridBackgroundParams'] != null) {
|
||||
gridBackgroundParams = GridBackgroundParams.fromMap(
|
||||
map['gridBackgroundParams'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
blockDefaultZoomGestures =
|
||||
(map['blockDefaultZoomGestures'] as bool? ?? false);
|
||||
minimumZoomFactor = map['minimumZoomFactor'] as double? ?? 0.25;
|
||||
if (save != null) { save!(); }
|
||||
}
|
||||
|
||||
///
|
||||
factory Dashboard.fromJson(String source) =>
|
||||
Dashboard.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
/// The current elements in the dashboard
|
||||
List<FlowElement> elements;
|
||||
List<FlowElement> get elementSelected =>
|
||||
elements.where((element) => element.isSelected).toList();
|
||||
|
||||
Offset _dashboardPosition;
|
||||
|
||||
/// Dashboard size
|
||||
Size dashboardSize;
|
||||
|
||||
List<ArrowPainter> arrows = [];
|
||||
List<ArrowPainter> get arrowsSelected =>
|
||||
arrows.where((element) => element.isSelected).toList();
|
||||
|
||||
/// The default style for the new created arrow
|
||||
ArrowStyle defaultArrowStyle;
|
||||
ArrowDirection defaultArrowDirection = ArrowDirection.forward;
|
||||
|
||||
/// [handlerFeedbackOffset] sets an offset for the handler when user
|
||||
/// is dragging it.
|
||||
/// This can be used to prevent the handler being covered by user's
|
||||
/// finger on touch screens.
|
||||
late Offset handlerFeedbackOffset;
|
||||
|
||||
/// Background parameters.
|
||||
GridBackgroundParams gridBackgroundParams;
|
||||
|
||||
///
|
||||
bool blockDefaultZoomGestures;
|
||||
|
||||
/// minimum zoom factor allowed
|
||||
/// default is 0.25
|
||||
/// setting it to 1 will prevent zooming out
|
||||
/// setting it to 0 will remove the limit
|
||||
double minimumZoomFactor;
|
||||
|
||||
final List<ConnectionListener> _connectionListeners = [];
|
||||
|
||||
Map<String, dynamic> serialize() {
|
||||
Map<String, dynamic> graph = {};
|
||||
graph['elements'] = {};
|
||||
for(var el in elements) {
|
||||
graph['elements'][el.id] = el.serialize();
|
||||
}
|
||||
graph['arrows'] = arrows.map((e) => e.serialize()).toList();
|
||||
return graph;
|
||||
}
|
||||
|
||||
void deserialize(Map<String, dynamic> graph) {
|
||||
elements.clear();
|
||||
arrows.clear();
|
||||
for(var el in graph['elements'].values) {
|
||||
List<ConnectionParams> nexts = [];
|
||||
var flow = FlowElement.deserialize(el);
|
||||
for(var ar in graph['arrows']) {
|
||||
nexts.add(ConnectionParams(
|
||||
srcElementId: ar['from']['id'],
|
||||
destElementId: ar['to']['id'],
|
||||
arrowParams: ArrowParams.fromMap(ar["params"]),
|
||||
pivots: [
|
||||
Pivot(Offset(ar['from']['x'], (ar['from']['y']))),
|
||||
Pivot(Offset(ar['to']['x'], (ar['to']['y'])))
|
||||
]
|
||||
));
|
||||
}
|
||||
flow.next = nexts;
|
||||
}
|
||||
}
|
||||
|
||||
/// add listener called when a new connection is created
|
||||
void addConnectionListener(ConnectionListener listener) {
|
||||
_connectionListeners.add(listener);
|
||||
}
|
||||
|
||||
/// remove connection listener
|
||||
void removeConnectionListener(ConnectionListener listener) {
|
||||
_connectionListeners.remove(listener);
|
||||
}
|
||||
|
||||
/// set grid background parameters
|
||||
void setGridBackgroundParams(GridBackgroundParams params) {
|
||||
gridBackgroundParams = params;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// set the feedback offset to help on mobile device to see the
|
||||
/// end of arrow and not hiding behind the finger when moving it
|
||||
void setHandlerFeedbackOffset(Offset offset) {
|
||||
handlerFeedbackOffset = offset;
|
||||
}
|
||||
|
||||
void addToHistory() {
|
||||
if (tempHistory.length >= 50) { tempHistory.removeAt(0); }
|
||||
tempHistory.add(toMap());
|
||||
if (save != null) { save!(); }
|
||||
history = tempHistory.map((e) => e).toList();
|
||||
chartKey.currentState?.setState(() { });
|
||||
chartMenuKey.currentState?.setState(() { });
|
||||
}
|
||||
bool isBack = false;
|
||||
void back() {
|
||||
tempHistory.removeLast();
|
||||
if (tempHistory.length == 0) return;
|
||||
copyFromMap(tempHistory.last);
|
||||
chartKey.currentState?.setState(() { });
|
||||
chartMenuKey.currentState?.setState(() { });
|
||||
}
|
||||
|
||||
bool canBack() {
|
||||
return tempHistory.length > 1;
|
||||
}
|
||||
|
||||
bool canForward() {
|
||||
return tempHistory.length < history.length;
|
||||
}
|
||||
|
||||
void forward() {
|
||||
if (canForward()) {
|
||||
tempHistory.add(history[tempHistory.length]);
|
||||
copyFromMap(tempHistory.last);
|
||||
chartKey.currentState?.setState(() { });
|
||||
chartMenuKey.currentState?.setState(() { });
|
||||
}
|
||||
}
|
||||
|
||||
/// set [resizable] element property
|
||||
void setElementResizable(
|
||||
FlowElement element,
|
||||
bool resizable, {
|
||||
bool notify = true,
|
||||
}) {
|
||||
element.isResizing = resizable;
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
FlowElement? getElement(String id, {bool notify = true}) {
|
||||
try { return elements.firstWhere((element) => element.id == id); }
|
||||
catch (e) { return null; }
|
||||
}
|
||||
|
||||
/// add a [FlowElement] to the dashboard
|
||||
void addElement(FlowElement element, {bool notify = true}) {
|
||||
if (element.id.isEmpty) {
|
||||
element.id = const Uuid().v4();
|
||||
}
|
||||
element.setScale(1, gridBackgroundParams.scale);
|
||||
elements.add(element);
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void setElement(FlowElement element, FlowElement set, {bool notify = true}) {
|
||||
element = set;
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void notifyListeners() {
|
||||
addToHistory();
|
||||
super.notifyListeners();
|
||||
}
|
||||
|
||||
/// Set a new [style] to the arrow staring from [src] pointing to [dest].
|
||||
/// If [notify] is true the dasboard is refreshed.
|
||||
/// The [tension] parameter is used when [style] is [ArrowStyle.segmented] to
|
||||
/// set the curve strength on pivot points. 0 means no curve.
|
||||
void setArrowStyle(
|
||||
FlowElement src,
|
||||
FlowElement dest,
|
||||
ArrowStyle style, {
|
||||
bool notify = true,
|
||||
double tension = 1.0,
|
||||
}) {
|
||||
for (final conn in src.next) {
|
||||
if (conn.destElementId == dest.id) {
|
||||
conn.arrowParams.style = style;
|
||||
conn.arrowParams.tension = tension;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a new [style] to the arrow staring from the [handler] of [src]
|
||||
/// element.
|
||||
/// If [notify] is true the dasboard is refreshed.
|
||||
/// The [tension] parameter is used when [style] is [ArrowStyle.segmented] to
|
||||
/// set the curve strength on pivot points. 0 means no curve.
|
||||
void setArrowStyleByHandler(
|
||||
FlowElement src,
|
||||
Handler handler,
|
||||
ArrowStyle style, {
|
||||
bool notify = true,
|
||||
double tension = 1.0,
|
||||
}) {
|
||||
// find arrows that start from [src] inside [handler]
|
||||
for (final conn in src.next) {
|
||||
if (conn.arrowParams.startArrowPosition == handler.toAlignment()) {
|
||||
conn.arrowParams.tension = tension;
|
||||
conn.arrowParams.style = style;
|
||||
}
|
||||
}
|
||||
// find arrow that ends to this [src] inside [handler]
|
||||
for (final element in elements) {
|
||||
for (final conn in element.next) {
|
||||
if (conn.arrowParams.endArrowPosition == handler.toAlignment() &&
|
||||
conn.destElementId == src.id) {
|
||||
conn.arrowParams.tension = tension;
|
||||
conn.arrowParams.style = style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// find the element by its [id]
|
||||
int findElementIndexById(String id) {
|
||||
return elements.indexWhere((element) => element.id == id);
|
||||
}
|
||||
|
||||
/// find the element by its [id] for convenience
|
||||
/// return null if not found
|
||||
FlowElement? findElementById(String id) {
|
||||
try {
|
||||
return elements.firstWhere((element) => element.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// find the connection from [srcElement] to [destElement]
|
||||
/// return null if not found.
|
||||
/// In case of multiple connections, first connection is returned.
|
||||
ConnectionParams? findConnectionByElements(
|
||||
FlowElement srcElement,
|
||||
FlowElement destElement,
|
||||
) {
|
||||
try {
|
||||
return srcElement.next
|
||||
.firstWhere((element) => element.destElementId == destElement.id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// find the source element of the [dest] element.
|
||||
FlowElement? findSrcElementByDestElement(FlowElement dest) {
|
||||
for (final element in elements) {
|
||||
for (final connection in element.next) {
|
||||
if (connection.destElementId == dest.id) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// remove all elements
|
||||
void removeAllElements({bool notify = true}) {
|
||||
elements.clear();
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
/// remove the [handler] connection of [element]
|
||||
void removeElementConnection(
|
||||
FlowElement element,
|
||||
Handler handler, {
|
||||
bool notify = true,
|
||||
}) {
|
||||
Alignment alignment;
|
||||
switch (handler) {
|
||||
case Handler.topCenter:
|
||||
alignment = Alignment.topCenter;
|
||||
case Handler.bottomCenter:
|
||||
alignment = Alignment.bottomCenter;
|
||||
case Handler.leftCenter:
|
||||
alignment = Alignment.centerLeft;
|
||||
case Handler.rightCenter:
|
||||
alignment = Alignment.centerRight;
|
||||
}
|
||||
|
||||
var isSrc = false;
|
||||
for (final connection in element.next) {
|
||||
if (connection.arrowParams.startArrowPosition == alignment) {
|
||||
isSrc = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSrc) {
|
||||
element.next.removeWhere(
|
||||
(handlerParam) =>
|
||||
handlerParam.arrowParams.startArrowPosition == alignment,
|
||||
);
|
||||
} else {
|
||||
final src = findSrcElementByDestElement(element);
|
||||
if (src != null) {
|
||||
src.next.removeWhere(
|
||||
(handlerParam) => handlerParam.destElementId == element.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
/// dissect an element connection
|
||||
/// [handler] is the handler that is in connection
|
||||
/// [point] is the point where the connection is dissected
|
||||
/// if [point] is null, point is automatically calculated
|
||||
void dissectElementConnection(
|
||||
FlowElement element,
|
||||
Handler handler, {
|
||||
Offset? point,
|
||||
bool notify = true,
|
||||
}) {
|
||||
Alignment alignment;
|
||||
switch (handler) {
|
||||
case Handler.topCenter:
|
||||
alignment = Alignment.topCenter;
|
||||
case Handler.bottomCenter:
|
||||
alignment = Alignment.bottomCenter;
|
||||
case Handler.leftCenter:
|
||||
alignment = Alignment.centerLeft;
|
||||
case Handler.rightCenter:
|
||||
alignment = Alignment.centerRight;
|
||||
}
|
||||
|
||||
ConnectionParams? conn;
|
||||
|
||||
var newPoint = Offset.zero;
|
||||
if (point == null) {
|
||||
try {
|
||||
// assuming element is the src
|
||||
conn = element.next.firstWhere(
|
||||
(handlerParam) =>
|
||||
handlerParam.arrowParams.startArrowPosition == alignment,
|
||||
);
|
||||
if (conn.arrowParams.style != ArrowStyle.segmented) return;
|
||||
|
||||
final dest = findElementById(conn.destElementId);
|
||||
newPoint = (dest!
|
||||
.getHandlerPosition(conn.arrowParams.endArrowPosition) +
|
||||
element
|
||||
.getHandlerPosition(conn.arrowParams.startArrowPosition)) /
|
||||
2;
|
||||
} catch (e) {
|
||||
// apparently is not
|
||||
final src = findSrcElementByDestElement(element)!;
|
||||
conn = src.next.firstWhere(
|
||||
(handlerParam) => handlerParam.destElementId == element.id,
|
||||
);
|
||||
if (conn.arrowParams.style != ArrowStyle.segmented) return;
|
||||
|
||||
newPoint = (element
|
||||
.getHandlerPosition(conn.arrowParams.endArrowPosition) +
|
||||
src.getHandlerPosition(conn.arrowParams.startArrowPosition)) /
|
||||
2;
|
||||
}
|
||||
} else {
|
||||
newPoint = point;
|
||||
}
|
||||
|
||||
conn?.dissect(newPoint);
|
||||
|
||||
if (notify && conn != null) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// remove the dissection of the connection
|
||||
void removeDissection(Pivot pivot, {bool notify = true}) {
|
||||
for (final element in elements) {
|
||||
for (final connection in element.next) {
|
||||
connection.pivots.removeWhere((item) => item == pivot);
|
||||
}
|
||||
}
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
/// remove the connection from [srcElement] to [destElement]
|
||||
void removeConnectionByElements(
|
||||
FlowElement srcElement,
|
||||
FlowElement destElement, {
|
||||
bool notify = true,
|
||||
}) {
|
||||
srcElement.next.removeWhere(
|
||||
(handlerParam) => handlerParam.destElementId == destElement.id,
|
||||
);
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
/// remove all the connection from the [element]
|
||||
void removeElementConnections(FlowElement element, {bool notify = true}) {
|
||||
element.next.clear();
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
/// remove all the elements with [id] from the dashboard
|
||||
void removeElementById(String id, {bool notify = true}) {
|
||||
// remove the element
|
||||
var elementId = '';
|
||||
elements.removeWhere((element) {
|
||||
if (element.id == id) {
|
||||
elementId = element.id;
|
||||
}
|
||||
return element.id == id;
|
||||
});
|
||||
|
||||
// remove all connections to the elements found
|
||||
for (final e in elements) {
|
||||
e.next.removeWhere((handlerParams) {
|
||||
return elementId.contains(handlerParams.destElementId);
|
||||
});
|
||||
}
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
/// remove element
|
||||
/// return true if it has been removed
|
||||
bool removeElement(FlowElement element, {bool notify = true}) {
|
||||
// remove the element
|
||||
var found = false;
|
||||
final elementId = element.id;
|
||||
elements.removeWhere((e) {
|
||||
if (e.id == element.id) found = true;
|
||||
return e.id == element.id;
|
||||
});
|
||||
|
||||
// remove all connections to the element
|
||||
for (final e in elements) {
|
||||
e.next.removeWhere(
|
||||
(handlerParams) => handlerParams.destElementId == elementId,
|
||||
);
|
||||
}
|
||||
if (notify) notifyListeners();
|
||||
return found;
|
||||
}
|
||||
|
||||
double currentZoom = 1.0;
|
||||
|
||||
/// [factor] needs to be a non negative value.
|
||||
/// 1 is the default value.
|
||||
/// Giving a value above 1 will zoom the dashboard by the given factor
|
||||
/// and vice versa. Negative values will be ignored.
|
||||
/// [zoomFactor] will not go below [minimumZoomFactor]
|
||||
/// [focalPoint] is the point where the zoom is centered
|
||||
/// default is the center of the dashboard
|
||||
void setZoomFactor(double factor, {Offset? focalPoint}) {
|
||||
if (factor < minimumZoomFactor || gridBackgroundParams.scale == factor) {
|
||||
return;
|
||||
}
|
||||
|
||||
focalPoint ??= Offset(dashboardSize.width / 2, dashboardSize.height / 2);
|
||||
|
||||
for (final element in elements) {
|
||||
// applying new zoom
|
||||
element
|
||||
..position = (element.position - focalPoint) /
|
||||
gridBackgroundParams.scale *
|
||||
factor +
|
||||
focalPoint
|
||||
..setScale(gridBackgroundParams.scale, factor);
|
||||
for (final conn in element.next) {
|
||||
for (final pivot in conn.pivots) {
|
||||
pivot.setScale(gridBackgroundParams.scale, focalPoint, factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gridBackgroundParams.setScale(factor, focalPoint);
|
||||
currentZoom = factor;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double getZoomFactor() {
|
||||
return currentZoom;
|
||||
}
|
||||
|
||||
/// shorthand to get the current zoom factor
|
||||
double get zoomFactor {
|
||||
return gridBackgroundParams.scale;
|
||||
}
|
||||
|
||||
/// needed to know the diagram widget position to compute
|
||||
/// offsets for drag and drop elements
|
||||
void setDashboardPosition(Offset position) {
|
||||
_dashboardPosition = position;
|
||||
}
|
||||
|
||||
/// Get the position.
|
||||
Offset get position => _dashboardPosition;
|
||||
|
||||
/// needed to know the diagram widget size
|
||||
void setDashboardSize(Size size) {
|
||||
dashboardSize = size;
|
||||
}
|
||||
|
||||
/// make an arrow connection from [sourceElement] to
|
||||
/// the elements with id [destId]
|
||||
/// [arrowParams] definition of arrow parameters
|
||||
void addNextById(
|
||||
FlowElement sourceElement,
|
||||
String destId,
|
||||
ArrowParams arrowParams, {
|
||||
bool notify = true,
|
||||
}) {
|
||||
var found = 0;
|
||||
arrowParams.setScale(1, gridBackgroundParams.scale);
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
if (elements[i].id == destId) {
|
||||
// if the [id] already exist, remove it and add this new connection
|
||||
/* sourceElement.next
|
||||
.removeWhere((element) => element.destElementId == destId); */
|
||||
final conn = ConnectionParams(
|
||||
srcElementId: sourceElement.id,
|
||||
destElementId: elements[i].id,
|
||||
arrowParams: arrowParams,
|
||||
pivots: [],
|
||||
);
|
||||
sourceElement.next.add(conn);
|
||||
for (final listener in _connectionListeners) {
|
||||
listener(sourceElement, elements[i]);
|
||||
}
|
||||
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == 0) {
|
||||
debugPrint('Element with $destId id not found!');
|
||||
return;
|
||||
}
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
//******************************* */
|
||||
/// manage load/save using json
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'name': name,
|
||||
'isMenu': isMenu,
|
||||
'arrows': arrows.map((x) => x.toMap()).toList(),
|
||||
'elements': elements.map((x) => x.toMap()).toList(),
|
||||
'dashboardSizeWidth': dashboardSize.width,
|
||||
'dashboardSizeHeight': dashboardSize.height,
|
||||
'gridBackgroundParams': gridBackgroundParams.toMap(),
|
||||
'blockDefaultZoomGestures': blockDefaultZoomGestures,
|
||||
'minimumZoomFactor': minimumZoomFactor,
|
||||
'defaultArrowDirection': defaultArrowDirection.index,
|
||||
'defaultDashSpace': defaultDashSpace,
|
||||
'defaultDashWidth': defaultDashWidth,
|
||||
'arrowStyle': defaultArrowStyle.index,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
///
|
||||
String prettyJson() {
|
||||
final spaces = ' ' * 2;
|
||||
final encoder = JsonEncoder.withIndent(spaces);
|
||||
return encoder.convert(toMap());
|
||||
}
|
||||
|
||||
/// recenter the dashboard
|
||||
void recenter() {
|
||||
final center = Offset(dashboardSize.width / 2, dashboardSize.height / 2);
|
||||
gridBackgroundParams.offset = center;
|
||||
if (elements.isNotEmpty) {
|
||||
final currentDeviation = elements.first.position - center;
|
||||
for (final element in elements) {
|
||||
element.position -= currentDeviation;
|
||||
for (final next in element.next) {
|
||||
for (final pivot in next.pivots) {
|
||||
pivot.pivot -= currentDeviation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// save the dashboard into [completeFilePath]
|
||||
void saveDashboard(String completeFilePath) {
|
||||
File(completeFilePath).writeAsStringSync(prettyJson(), flush: true);
|
||||
}
|
||||
|
||||
/// clear the dashboard and load the new one
|
||||
void loadDashboard(String completeFilePath) {
|
||||
final f = File(completeFilePath);
|
||||
if (f.existsSync()) {
|
||||
elements.clear();
|
||||
final source = json.decode(f.readAsStringSync()) as Map<String, dynamic>;
|
||||
|
||||
gridBackgroundParams = GridBackgroundParams.fromMap(
|
||||
source['gridBackgroundParams'] as Map<String, dynamic>,
|
||||
);
|
||||
blockDefaultZoomGestures = source['blockDefaultZoomGestures'] as bool;
|
||||
minimumZoomFactor = source['minimumZoomFactor'] as double;
|
||||
dashboardSize = Size(
|
||||
source['dashboardSizeWidth'] as double,
|
||||
source['dashboardSizeHeight'] as double,
|
||||
);
|
||||
|
||||
final loadedElements = List<FlowElement>.from(
|
||||
(source['elements'] as List<dynamic>).map<FlowElement>(
|
||||
(x) => FlowElement.fromMap(x as Map<String, dynamic>),
|
||||
),
|
||||
);
|
||||
elements
|
||||
..clear()
|
||||
..addAll(loadedElements);
|
||||
|
||||
recenter();
|
||||
}
|
||||
}
|
||||
}
|
||||
64
library/flutter_flow_chart/lib/src/elements/connection_params.dart
Executable file
64
library/flutter_flow_chart/lib/src/elements/connection_params.dart
Executable file
@@ -0,0 +1,64 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_flow_chart/src/ui/draw_arrow.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/segment_handler.dart';
|
||||
|
||||
/// Connection parameters.
|
||||
class ConnectionParams {
|
||||
///
|
||||
ConnectionParams({
|
||||
required this.srcElementId,
|
||||
required this.destElementId,
|
||||
required this.arrowParams,
|
||||
List<Pivot>? pivots,
|
||||
}) : pivots = pivots ?? [];
|
||||
|
||||
///
|
||||
factory ConnectionParams.fromMap(Map<String, dynamic> map) {
|
||||
return ConnectionParams(
|
||||
srcElementId: map['srcElementId'] as String,
|
||||
destElementId: map['destElementId'] as String,
|
||||
arrowParams: ArrowParams.fromMap(map['arrowParams'] as Map<String, dynamic>),
|
||||
pivots: (map['pivots'] as List?)
|
||||
?.map<Pivot>(
|
||||
(pivot) => Pivot.fromMap(pivot as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
factory ConnectionParams.fromJson(String source) =>
|
||||
ConnectionParams.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
/// Unique element ID where this connection points.
|
||||
final String destElementId;
|
||||
final String srcElementId;
|
||||
|
||||
|
||||
/// Arrow parameters.
|
||||
final ArrowParams arrowParams;
|
||||
|
||||
/// List of pivot points for the segmented arrow style.
|
||||
final List<Pivot> pivots;
|
||||
|
||||
///
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'srcElementId' : srcElementId,
|
||||
'destElementId': destElementId,
|
||||
'arrowParams': arrowParams.toMap(),
|
||||
'pivots': pivots.map((pivots) => pivots.toMap()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Divide the connection into segments
|
||||
void dissect(Offset point) {
|
||||
pivots.add(Pivot(point));
|
||||
}
|
||||
|
||||
///
|
||||
String toJson() => json.encode(toMap());
|
||||
}
|
||||
379
library/flutter_flow_chart/lib/src/elements/flow_element.dart
Executable file
379
library/flutter_flow_chart/lib/src/elements/flow_element.dart
Executable file
@@ -0,0 +1,379 @@
|
||||
// ignore_for_file: avoid_positional_boolean_parameters, avoid_dynamic_calls
|
||||
|
||||
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:uuid/uuid.dart';
|
||||
|
||||
/// Kinf od element
|
||||
enum ElementKind {
|
||||
///
|
||||
rectangle,
|
||||
|
||||
///
|
||||
diamond,
|
||||
|
||||
///
|
||||
storage,
|
||||
|
||||
///
|
||||
oval,
|
||||
|
||||
///
|
||||
parallelogram,
|
||||
|
||||
///
|
||||
hexagon,
|
||||
|
||||
///
|
||||
widget,
|
||||
}
|
||||
|
||||
/// Handler supported by elements
|
||||
enum Handler {
|
||||
///
|
||||
topCenter,
|
||||
|
||||
///
|
||||
bottomCenter,
|
||||
|
||||
///
|
||||
rightCenter,
|
||||
|
||||
///
|
||||
leftCenter;
|
||||
|
||||
/// Convert to [Alignment]
|
||||
Alignment toAlignment() {
|
||||
switch (this) {
|
||||
case Handler.topCenter:
|
||||
return Alignment.topCenter;
|
||||
case Handler.bottomCenter:
|
||||
return Alignment.bottomCenter;
|
||||
case Handler.rightCenter:
|
||||
return Alignment.centerRight;
|
||||
case Handler.leftCenter:
|
||||
return Alignment.centerLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Class to store [ElementWidget]s and notify its changes
|
||||
class FlowElement extends ChangeNotifier {
|
||||
bool isSelected = false;
|
||||
///
|
||||
FlowElement({
|
||||
Offset position = Offset.zero,
|
||||
String? id,
|
||||
this.size = Size.zero,
|
||||
this.text = '',
|
||||
this.textColor = Colors.black,
|
||||
this.fontFamily,
|
||||
this.widget,
|
||||
this.textSize = 24,
|
||||
this.textIsBold = false,
|
||||
this.kind = ElementKind.rectangle,
|
||||
this.handlers = const [
|
||||
Handler.topCenter,
|
||||
Handler.bottomCenter,
|
||||
Handler.rightCenter,
|
||||
Handler.leftCenter,
|
||||
],
|
||||
this.handlerSize = 15.0,
|
||||
this.backgroundColor = Colors.white,
|
||||
this.borderColor = Colors.blue,
|
||||
this.borderThickness = 3,
|
||||
this.elevation = 4,
|
||||
List<ConnectionParams>? next,
|
||||
}) : next = next ?? [],
|
||||
id = id ?? const Uuid().v4(),
|
||||
isResizing = false,
|
||||
// fixing offset issue under extreme scaling
|
||||
position = position -
|
||||
Offset(
|
||||
size.width / 2 + handlerSize / 2,
|
||||
size.height / 2 + handlerSize / 2,
|
||||
);
|
||||
|
||||
bool isElement(Offset pos) {
|
||||
print("${position.dx} <= ${pos.dx} <= ${position.dx + size.width}");
|
||||
if (position.dx <= pos.dx && pos.dx <= position.dx + size.width) {
|
||||
return position.dy <= pos.dy && pos.dy <= position.dy + size.height;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
factory FlowElement.fromMap(Map<String, dynamic> map) {
|
||||
final e = FlowElement(
|
||||
widget: map['widget'] as Widget?,
|
||||
size: Size(map['size.width'] as double, map['size.height'] as double),
|
||||
text: map['text'] as String,
|
||||
textColor: Color(map['textColor'] as int),
|
||||
fontFamily: map['fontFamily'] as String?,
|
||||
textSize: map['textSize'] as double,
|
||||
textIsBold: map['textIsBold'] as bool,
|
||||
kind: ElementKind.values[map['kind'] as int],
|
||||
handlers: List<Handler>.from(
|
||||
(map['handlers'] as List<dynamic>).map<Handler>(
|
||||
(x) => Handler.values[x as int],
|
||||
),
|
||||
),
|
||||
handlerSize: map['handlerSize'] as double,
|
||||
backgroundColor: Color(map['backgroundColor'] as int),
|
||||
borderColor: Color(map['borderColor'] as int),
|
||||
borderThickness: map['borderThickness'] as double,
|
||||
elevation: map['elevation'] as double,
|
||||
next: (map['next'] as List).isNotEmpty
|
||||
? List<ConnectionParams>.from(
|
||||
(map['next'] as List<dynamic>).map<dynamic>(
|
||||
(x) => ConnectionParams.fromMap(x as Map<String, dynamic>),
|
||||
),
|
||||
)
|
||||
: [],
|
||||
)
|
||||
..setId(map['id'] as String)
|
||||
..position = Offset(
|
||||
map['positionDx'] as double,
|
||||
map['positionDy'] as double,
|
||||
);
|
||||
return e;
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
factory FlowElement.fromJson(String source) =>
|
||||
FlowElement.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
/// Unique id set when adding a [FlowElement] with [Dashboard.addElement()]
|
||||
late String id;
|
||||
|
||||
/// The position of the [FlowElement]
|
||||
Offset position;
|
||||
|
||||
/// The size of the [FlowElement]
|
||||
Size size;
|
||||
|
||||
Widget? widget;
|
||||
/// Element text
|
||||
String text;
|
||||
|
||||
/// Text color
|
||||
Color textColor;
|
||||
|
||||
/// Text font family
|
||||
String? fontFamily;
|
||||
|
||||
/// Text size
|
||||
double textSize;
|
||||
|
||||
/// Makes text bold if true
|
||||
bool textIsBold;
|
||||
|
||||
/// Element shape
|
||||
ElementKind kind;
|
||||
|
||||
/// Connection handlers
|
||||
List<Handler> handlers;
|
||||
|
||||
/// The size of element handlers
|
||||
double handlerSize;
|
||||
|
||||
/// Background color of the element
|
||||
Color backgroundColor;
|
||||
|
||||
/// Border color of the element
|
||||
Color borderColor;
|
||||
|
||||
/// Border thickness of the element
|
||||
double borderThickness;
|
||||
|
||||
/// Shadow elevation
|
||||
double elevation;
|
||||
|
||||
/// List of connections from this element
|
||||
List<ConnectionParams> next;
|
||||
|
||||
/// Element text
|
||||
bool isResizing;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'kind: $kind text: $text';
|
||||
}
|
||||
|
||||
/// Get the handler center of this handler for the given alignment.
|
||||
Offset getHandlerPosition(Alignment alignment) {
|
||||
// The zero position coordinate is the top-left of this element.
|
||||
final ret = Offset(
|
||||
position.dx + (size.width * ((alignment.x + 1) / 2)) + handlerSize / 2,
|
||||
position.dy + (size.height * ((alignment.y + 1) / 2) + handlerSize / 2),
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// When setting to true, a handler will disply at the element bottom right
|
||||
/// to let the user to resize it. When finish it will disappear.
|
||||
void setIsResizing(bool resizing) {
|
||||
isResizing = resizing;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Sets a new scale
|
||||
void setScale(double currentZoom, double factor) {
|
||||
size = size / currentZoom * factor;
|
||||
handlerSize = handlerSize / currentZoom * factor;
|
||||
textSize = textSize / currentZoom * factor;
|
||||
for (final element in next) {
|
||||
element.arrowParams.setScale(currentZoom, factor);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Used internally to set an unique Uuid to this element
|
||||
void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/// Set text
|
||||
void setText(String text) {
|
||||
this.text = text;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set text color
|
||||
void setTextColor(Color color) {
|
||||
textColor = color;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set text font family
|
||||
void setFontFamily(String? fontFamily) {
|
||||
this.fontFamily = fontFamily;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set text size
|
||||
void setTextSize(double size) {
|
||||
textSize = size;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set text bold
|
||||
void setTextIsBold(bool isBold) {
|
||||
textIsBold = isBold;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set background color
|
||||
void setBackgroundColor(Color color) {
|
||||
backgroundColor = color;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set border color
|
||||
void setBorderColor(Color color) {
|
||||
borderColor = color;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set border thickness
|
||||
void setBorderThickness(double thickness) {
|
||||
borderThickness = thickness;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set elevation
|
||||
void setElevation(double elevation) {
|
||||
this.elevation = elevation;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Change element position in the dashboard
|
||||
void changePosition(Offset newPosition) {
|
||||
position = newPosition;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Change element size
|
||||
void changeSize(Size newSize) {
|
||||
size = newSize;
|
||||
if (size.width < 40) size = Size(40, size.height);
|
||||
if (size.height < 40) size = Size(size.width, 40);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant FlowElement other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return position.hashCode ^
|
||||
size.hashCode ^
|
||||
text.hashCode ^
|
||||
textColor.hashCode ^
|
||||
fontFamily.hashCode ^
|
||||
textSize.hashCode ^
|
||||
textIsBold.hashCode ^
|
||||
id.hashCode ^
|
||||
kind.hashCode ^
|
||||
handlers.hashCode ^
|
||||
handlerSize.hashCode ^
|
||||
backgroundColor.hashCode ^
|
||||
borderColor.hashCode ^
|
||||
borderThickness.hashCode ^
|
||||
elevation.hashCode ^
|
||||
next.hashCode;
|
||||
}
|
||||
|
||||
///
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'widget': widget,
|
||||
'positionDx': position.dx,
|
||||
'positionDy': position.dy,
|
||||
'size.width': size.width,
|
||||
'size.height': size.height,
|
||||
'text': text,
|
||||
'textColor': textColor.value,
|
||||
'fontFamily': fontFamily,
|
||||
'textSize': textSize,
|
||||
'textIsBold': textIsBold,
|
||||
'id': id,
|
||||
'kind': kind.index,
|
||||
'handlers': handlers.map((x) => x.index).toList(),
|
||||
'handlerSize': handlerSize,
|
||||
'backgroundColor': backgroundColor.value,
|
||||
'borderColor': borderColor.value,
|
||||
'borderThickness': borderThickness,
|
||||
'elevation': elevation,
|
||||
'next': next.map((x) => x.toMap()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
Map<String, dynamic> serialize() {
|
||||
Map<String, dynamic> graphElement = {};
|
||||
graphElement['id'] = id;
|
||||
graphElement['x'] = position.dx;
|
||||
graphElement['y'] = position.dy;
|
||||
graphElement['width'] = size.width;
|
||||
graphElement['height'] = size.height;
|
||||
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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
836
library/flutter_flow_chart/lib/src/flow_chart.dart
Executable file
836
library/flutter_flow_chart/lib/src/flow_chart.dart
Executable file
@@ -0,0 +1,836 @@
|
||||
// ignore: directives_ordering
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.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_menu.dart';
|
||||
import 'package:flutter_flow_chart/src/flow_chart_selected_menu.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/draw_arrow.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/element_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/grid_background.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/segment_handler.dart';
|
||||
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 {
|
||||
FlowChart({
|
||||
required this.dashboard,
|
||||
required this.itemWidget,
|
||||
super.key,
|
||||
this.itemWidgetTooltip,
|
||||
this.onElementPressed,
|
||||
this.onElementSecondaryTapped,
|
||||
this.onElementLongPressed,
|
||||
this.onElementSecondaryLongTapped,
|
||||
this.onDashboardTapped,
|
||||
this.onDashboardSecondaryTapped,
|
||||
this.onDashboardLongTapped,
|
||||
this.onDashboardSecondaryLongTapped,
|
||||
this.onHandlerPressed,
|
||||
this.onHandlerSecondaryTapped,
|
||||
this.onHandlerLongPressed,
|
||||
this.onHandlerSecondaryLongTapped,
|
||||
this.onPivotPressed,
|
||||
this.onPivotSecondaryPressed,
|
||||
this.onScaleUpdate,
|
||||
this.onNewConnection,
|
||||
this.width = 1000,
|
||||
this.height = 1000,
|
||||
this.innerMenuWidth = 200,
|
||||
this.itemWidth = 80,
|
||||
this.categories = const [],
|
||||
required this.draggableItemBuilder,
|
||||
this.onDashboardAlertOpened,
|
||||
}) {}
|
||||
|
||||
final List<String> categories;
|
||||
final double width;
|
||||
final double height;
|
||||
final double innerMenuWidth;
|
||||
|
||||
double itemWidth = 80;
|
||||
double zoom = 1;
|
||||
|
||||
final Widget Function(T data) itemWidget;
|
||||
final Widget Function(T data)? itemWidgetTooltip;
|
||||
final List<T> Function(String cat) draggableItemBuilder;
|
||||
|
||||
final Widget Function(BuildContext constext, Dashboard dash)? onDashboardAlertOpened;
|
||||
/// callback for tap on dashboard
|
||||
final void Function(BuildContext context, Offset position)? onDashboardTapped;
|
||||
|
||||
/// callback for long tap on dashboard
|
||||
final void Function(BuildContext context, Offset position)?
|
||||
onDashboardLongTapped;
|
||||
|
||||
/// callback for mouse right click on dashboard
|
||||
final void Function(BuildContext context, Offset postision)?
|
||||
onDashboardSecondaryTapped;
|
||||
|
||||
/// callback for mouse right click long press on dashboard
|
||||
final void Function(BuildContext context, Offset position)?
|
||||
onDashboardSecondaryLongTapped;
|
||||
|
||||
/// callback for element pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementPressed;
|
||||
|
||||
/// callback for mouse right click event on an element
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementSecondaryTapped;
|
||||
|
||||
/// callback for element long pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementLongPressed;
|
||||
|
||||
/// callback for right click long press event on an element
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementSecondaryLongTapped;
|
||||
|
||||
/// callback for onclick event of pivot
|
||||
final void Function(BuildContext context, Pivot pivot)? onPivotPressed;
|
||||
|
||||
/// callback for secondary press event of pivot
|
||||
final void Function(BuildContext context, Pivot pivot)?
|
||||
onPivotSecondaryPressed;
|
||||
|
||||
/// callback for handler pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerPressed;
|
||||
|
||||
/// callback for handler right click event
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryTapped;
|
||||
|
||||
/// callback for handler right click long press event
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryLongTapped;
|
||||
|
||||
/// callback for handler long pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerLongPressed;
|
||||
|
||||
/// callback when adding a new connection
|
||||
final ConnectionListener? onNewConnection;
|
||||
|
||||
/// main dashboard to use
|
||||
final Dashboard dashboard;
|
||||
|
||||
/// Trigger for the scale change
|
||||
final void Function(double scale)? onScaleUpdate;
|
||||
|
||||
@override
|
||||
State<FlowChart> createState() => FlowChartState<T>();
|
||||
}
|
||||
|
||||
class HoverMenuController {
|
||||
HoverMenuState? currentState;
|
||||
|
||||
void hideSubMenu() {
|
||||
currentState?.hideSubMenu();
|
||||
}
|
||||
}
|
||||
|
||||
class HoverMenu extends StatefulWidget {
|
||||
final Widget title;
|
||||
final double? width;
|
||||
final List<Widget> items;
|
||||
final HoverMenuController? controller;
|
||||
bool isHovered = false;
|
||||
|
||||
|
||||
HoverMenu({
|
||||
Key? key,
|
||||
required this.title,
|
||||
this.items = const [],
|
||||
this.width,
|
||||
this.controller,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
HoverMenuState createState() => HoverMenuState();
|
||||
}
|
||||
|
||||
class HoverMenuState extends State<HoverMenu> {
|
||||
OverlayEntry? _overlayEntry;
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(_onFocusChanged);
|
||||
|
||||
if (widget.controller != null) {
|
||||
widget.controller?.currentState = this;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_overlayEntry = _createOverlayEntry();
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
} else {
|
||||
_overlayEntry?.remove();
|
||||
_removeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _removeOverlay() {
|
||||
widget.isHovered = false;
|
||||
}
|
||||
|
||||
void hideSubMenu() {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
elevation: 0.0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
padding: EdgeInsets.zero,
|
||||
foregroundColor: Colors.transparent,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
focusNode: _focusNode,
|
||||
onHover: (isHovered) {
|
||||
if (isHovered && !widget.isHovered) {
|
||||
_focusNode.requestFocus();
|
||||
isHovered = true;
|
||||
} else {
|
||||
_focusNode.unfocus();
|
||||
widget.isHovered = false;
|
||||
}
|
||||
},
|
||||
onPressed: () {},
|
||||
child: widget.title,
|
||||
);
|
||||
}
|
||||
|
||||
OverlayEntry _createOverlayEntry() {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
return OverlayEntry(
|
||||
maintainState: true,
|
||||
builder: (context) => Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy + size.height,
|
||||
width: 300,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
children: widget.items)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
/// get dashboard position after first frame is drawn
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
if (mounted) {
|
||||
final object = context.findRenderObject() as RenderBox?;
|
||||
if (object != null) {
|
||||
final translation = object.getTransformTo(null).getTranslation();
|
||||
final size = object.semanticBounds.size;
|
||||
final position = Offset(translation.x, translation.y);
|
||||
widget.dashboard.setDashboardSize(size);
|
||||
widget.dashboard.setDashboardPosition(position);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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 ClipRect(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Stack(children: [
|
||||
// Draw the grid
|
||||
Container( child: DragTarget<T>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
List<dynamic> accepted,
|
||||
List<dynamic> rejected,
|
||||
) {
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: ChartWidget(
|
||||
key: widget.dashboard.chartKey,
|
||||
flowChart: this,
|
||||
dashboard: widget.dashboard,
|
||||
onNewConnection: widget.onNewConnection,
|
||||
onDashboardTapped: widget.onDashboardLongTapped,
|
||||
onScaleUpdate: widget.onScaleUpdate,
|
||||
onDashboardSecondaryTapped: widget.onDashboardSecondaryTapped,
|
||||
onDashboardLongTapped: widget.onDashboardLongTapped,
|
||||
onDashboardSecondaryLongTapped: widget.onDashboardSecondaryLongTapped,
|
||||
onElementLongPressed: widget.onElementLongPressed,
|
||||
onElementSecondaryLongTapped: widget.onElementSecondaryLongTapped,
|
||||
onElementPressed: widget.onElementPressed,
|
||||
onElementSecondaryTapped: widget.onElementSecondaryTapped,
|
||||
onHandlerPressed: widget.onHandlerLongPressed,
|
||||
onHandlerLongPressed: widget.onHandlerLongPressed,
|
||||
onPivotSecondaryPressed: widget.onPivotSecondaryPressed,
|
||||
));
|
||||
},
|
||||
onAcceptWithDetails: (DragTargetDetails<T> details) {
|
||||
var e = details.data;
|
||||
String newID = const Uuid().v4();
|
||||
FlowElement el = FlowElement(
|
||||
id: newID,
|
||||
position: details.offset,
|
||||
size: const Size(100, 100),
|
||||
text: '${widget.dashboard.elements.length}',
|
||||
handlerSize: 15,
|
||||
widget: widget.itemWidget(e),
|
||||
kind: ElementKind.widget,
|
||||
handlers: [
|
||||
Handler.bottomCenter,
|
||||
Handler.topCenter,
|
||||
Handler.leftCenter,
|
||||
Handler.rightCenter,
|
||||
],
|
||||
);
|
||||
widget.dashboard.addElement(el);
|
||||
},
|
||||
))]
|
||||
),
|
||||
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 to draw interactive connection when the user tap on handlers
|
||||
class DrawingArrowWidget extends StatefulWidget {
|
||||
///
|
||||
const DrawingArrowWidget({required this.style, super.key});
|
||||
|
||||
///
|
||||
final ArrowStyle style;
|
||||
|
||||
@override
|
||||
State<DrawingArrowWidget> createState() => _DrawingArrowWidgetState();
|
||||
}
|
||||
|
||||
class _DrawingArrowWidgetState extends State<DrawingArrowWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
DrawingArrow.instance.addListener(_arrowChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
DrawingArrow.instance.removeListener(_arrowChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _arrowChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (DrawingArrow.instance.isZero()) return const SizedBox.shrink();
|
||||
return CustomPaint(
|
||||
painter: ArrowPainter(
|
||||
fromID: DrawingArrow.instance.fromID,
|
||||
toID: "",
|
||||
params: DrawingArrow.instance.params,
|
||||
from: DrawingArrow.instance.from,
|
||||
to: DrawingArrow.instance.to,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChartWidget extends StatefulWidget {
|
||||
|
||||
ChartWidget ({ Key? key,
|
||||
required this.flowChart,
|
||||
this.onElementPressed,
|
||||
this.onElementSecondaryTapped,
|
||||
this.onElementLongPressed,
|
||||
this.onElementSecondaryLongTapped,
|
||||
this.onDashboardTapped,
|
||||
this.onDashboardSecondaryTapped,
|
||||
this.onDashboardLongTapped,
|
||||
this.onDashboardSecondaryLongTapped,
|
||||
this.onHandlerPressed,
|
||||
this.onHandlerSecondaryTapped,
|
||||
this.onHandlerLongPressed,
|
||||
this.onHandlerSecondaryLongTapped,
|
||||
this.onPivotPressed,
|
||||
this.onPivotSecondaryPressed,
|
||||
this.onScaleUpdate,
|
||||
required this.dashboard,
|
||||
this.onNewConnection,
|
||||
}) : super(key: key);
|
||||
|
||||
FlowChartState flowChart;
|
||||
|
||||
final void Function(BuildContext context, Offset position)? onDashboardTapped;
|
||||
|
||||
/// callback for long tap on dashboard
|
||||
final void Function(BuildContext context, Offset position)?
|
||||
onDashboardLongTapped;
|
||||
|
||||
/// callback for mouse right click on dashboard
|
||||
final void Function(BuildContext context, Offset postision)?
|
||||
onDashboardSecondaryTapped;
|
||||
|
||||
/// callback for mouse right click long press on dashboard
|
||||
final void Function(BuildContext context, Offset position)?
|
||||
onDashboardSecondaryLongTapped;
|
||||
|
||||
/// callback for element pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementPressed;
|
||||
|
||||
/// callback for mouse right click event on an element
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementSecondaryTapped;
|
||||
|
||||
/// callback for element long pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementLongPressed;
|
||||
|
||||
/// callback for right click long press event on an element
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
FlowElement element,
|
||||
)? onElementSecondaryLongTapped;
|
||||
|
||||
/// callback for onclick event of pivot
|
||||
final void Function(BuildContext context, Pivot pivot)? onPivotPressed;
|
||||
|
||||
/// callback for secondary press event of pivot
|
||||
final void Function(BuildContext context, Pivot pivot)?
|
||||
onPivotSecondaryPressed;
|
||||
|
||||
/// callback for handler pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerPressed;
|
||||
|
||||
/// callback for handler right click event
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryTapped;
|
||||
|
||||
/// callback for handler right click long press event
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryLongTapped;
|
||||
|
||||
/// Trigger for the scale change
|
||||
final void Function(double scale)? onScaleUpdate;
|
||||
|
||||
/// callback for handler long pressed
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerLongPressed;
|
||||
|
||||
/// callback when adding a new connection
|
||||
final ConnectionListener? onNewConnection;
|
||||
|
||||
final Dashboard dashboard;
|
||||
|
||||
@override ChartWidgetState createState() => ChartWidgetState();
|
||||
}
|
||||
class ChartWidgetState extends State<ChartWidget> {
|
||||
bool hoverImportant = false;
|
||||
final segmentedTension = ValueNotifier<double>(1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.dashboard.addListener(_elementChanged);
|
||||
if (widget.onScaleUpdate != null) {
|
||||
widget.dashboard.gridBackgroundParams.addOnScaleUpdateListener(
|
||||
widget.onScaleUpdate!,
|
||||
);
|
||||
}
|
||||
if (widget.onNewConnection != null) {
|
||||
widget.dashboard.addConnectionListener(widget.onNewConnection!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.dashboard.removeListener(_elementChanged);
|
||||
if (widget.onScaleUpdate != null) {
|
||||
widget.dashboard.gridBackgroundParams.removeOnScaleUpdateListener(
|
||||
widget.onScaleUpdate!,
|
||||
);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _elementChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
double _oldScaleUpdateDelta = 0;
|
||||
|
||||
@override Widget build(BuildContext context) {
|
||||
final gridKey = GlobalKey();
|
||||
var tapDownPos = Offset.zero;
|
||||
var secondaryTapDownPos = Offset.zero;
|
||||
return Stack( children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTapDown: (details) {
|
||||
hoverImportant = false;
|
||||
tapDownPos = details.localPosition;
|
||||
for (var arr in widget.dashboard.arrows) {
|
||||
if (arr.isLine(tapDownPos)) {
|
||||
hoverImportant = true;
|
||||
arr.isSelected = !arr.isSelected;
|
||||
for (var sel in widget.dashboard.elements) {
|
||||
sel.isSelected = false;
|
||||
}
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
widget.dashboard.selectedMenuKey.currentState?.setState(() {});
|
||||
DrawingArrow.instance.notifyListeners();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!hoverImportant) {
|
||||
for (var sel in widget.dashboard.elements) { sel.isSelected = false; }
|
||||
for (var sel in widget.dashboard.arrows) { sel.isSelected = false; }
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
widget.dashboard.selectedMenuKey.currentState?.setState(() {});
|
||||
DrawingArrow.instance.notifyListeners();
|
||||
});
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
onSecondaryTapDown: (details) {
|
||||
secondaryTapDownPos = details.localPosition;
|
||||
},
|
||||
onTap: widget.onDashboardTapped == null
|
||||
? null
|
||||
: () => widget.onDashboardTapped!(
|
||||
gridKey.currentContext!,
|
||||
tapDownPos,
|
||||
),
|
||||
onLongPress: widget.onDashboardLongTapped == null
|
||||
? null
|
||||
: () => widget.onDashboardLongTapped!(
|
||||
gridKey.currentContext!,
|
||||
tapDownPos,
|
||||
),
|
||||
onSecondaryTap: () {
|
||||
widget.onDashboardSecondaryTapped?.call(
|
||||
gridKey.currentContext!,
|
||||
secondaryTapDownPos,
|
||||
);
|
||||
},
|
||||
onSecondaryLongPress: () {
|
||||
widget.onDashboardSecondaryLongTapped?.call(
|
||||
gridKey.currentContext!,
|
||||
secondaryTapDownPos,
|
||||
);
|
||||
},
|
||||
onScaleUpdate: (details) {
|
||||
if (details.scale != 1) {
|
||||
widget.dashboard.setZoomFactor(
|
||||
details.scale + _oldScaleUpdateDelta,
|
||||
focalPoint: details.focalPoint,
|
||||
);
|
||||
}
|
||||
widget.dashboard.setDashboardPosition(
|
||||
widget.dashboard.position + details.focalPointDelta,
|
||||
);
|
||||
for (var i = 0; i < widget.dashboard.elements.length; i++) {
|
||||
widget.dashboard.elements[i].position += details.focalPointDelta;
|
||||
for (final conn in widget.dashboard.elements[i].next) {
|
||||
for (final pivot in conn.pivots) {
|
||||
pivot.pivot += details.focalPointDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||
widget.dashboard.gridBackgroundParams.offset = details.focalPointDelta;
|
||||
setState(() {});
|
||||
},
|
||||
onScaleEnd: (details) {
|
||||
_oldScaleUpdateDelta = widget.dashboard.zoomFactor - 1;
|
||||
},
|
||||
child: GridBackground(
|
||||
key: gridKey,
|
||||
params: widget.dashboard.gridBackgroundParams,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Draw elements
|
||||
for (int i = 0; i < widget.dashboard.elements.length; i++)
|
||||
ElementWidget(
|
||||
key: UniqueKey(),
|
||||
dashboard: widget.dashboard,
|
||||
element: widget.dashboard.elements.elementAt(i),
|
||||
onElementPressed: widget.onElementPressed == null
|
||||
? null
|
||||
: (context, position) => widget.onElementPressed!(
|
||||
context,
|
||||
position,
|
||||
widget.dashboard.elements.elementAt(i),
|
||||
),
|
||||
onElementSecondaryTapped: widget.onElementSecondaryTapped == null
|
||||
? null
|
||||
: (context, position) => widget.onElementSecondaryTapped!(
|
||||
context,
|
||||
position,
|
||||
widget.dashboard.elements.elementAt(i),
|
||||
),
|
||||
onElementLongPressed: widget.onElementLongPressed == null
|
||||
? null
|
||||
: (context, position) => widget.onElementLongPressed!(
|
||||
context,
|
||||
position,
|
||||
widget.dashboard.elements.elementAt(i),
|
||||
),
|
||||
onElementSecondaryLongTapped:
|
||||
widget.onElementSecondaryLongTapped == null
|
||||
? null
|
||||
: (context, position) =>
|
||||
widget.onElementSecondaryLongTapped!(
|
||||
context,
|
||||
position,
|
||||
widget.dashboard.elements.elementAt(i),
|
||||
),
|
||||
onHandlerPressed: widget.onHandlerPressed == null
|
||||
? null
|
||||
: (context, position, handler, element) => widget
|
||||
.onHandlerPressed!(context, position, handler, element),
|
||||
onHandlerSecondaryTapped: widget.onHandlerSecondaryTapped == null
|
||||
? null
|
||||
: (context, position, handler, element) =>
|
||||
widget.onHandlerSecondaryTapped!(
|
||||
context,
|
||||
position,
|
||||
handler,
|
||||
element,
|
||||
),
|
||||
onHandlerLongPressed: widget.onHandlerLongPressed == null
|
||||
? null
|
||||
: (context, position, handler, element) =>
|
||||
widget.onHandlerLongPressed!(
|
||||
context,
|
||||
position,
|
||||
handler,
|
||||
element,
|
||||
),
|
||||
onHandlerSecondaryLongTapped:
|
||||
widget.onHandlerSecondaryLongTapped == null
|
||||
? null
|
||||
: (context, position, handler, element) =>
|
||||
widget.onHandlerSecondaryLongTapped!(
|
||||
context,
|
||||
position,
|
||||
handler,
|
||||
element,
|
||||
),
|
||||
),
|
||||
// Draw arrows
|
||||
for (int i = 0; i < widget.dashboard.elements.length; i++)
|
||||
for (int n = 0; n < widget.dashboard.elements[i].next.length; n++)
|
||||
DrawArrow(
|
||||
flow: this,
|
||||
key: UniqueKey(),
|
||||
index: n,
|
||||
srcElement: widget.dashboard.elements[i],
|
||||
destElement: widget.dashboard.elements[widget.dashboard.findElementIndexById(
|
||||
widget.dashboard.elements[i].next[n].destElementId,
|
||||
)],
|
||||
arrowParams: widget.dashboard.elements[i].next[n].arrowParams,
|
||||
pivots: widget.dashboard.elements[i].next[n].pivots,
|
||||
),
|
||||
// drawing segment handlers
|
||||
for (int i = 0; i < widget.dashboard.elements.length; i++)
|
||||
for (int n = 0; n < widget.dashboard.elements[i].next.length; n++)
|
||||
if (widget.dashboard.elements[i].next[n].arrowParams.style == ArrowStyle.segmented)
|
||||
for (int j = 0; j < widget.dashboard.elements[i].next[n].pivots.length; j++)
|
||||
SegmentHandler(
|
||||
key: UniqueKey(),
|
||||
pivot: widget.dashboard.elements[i].next[n].pivots[j],
|
||||
dashboard: widget.dashboard,
|
||||
onPivotPressed: widget.onPivotPressed,
|
||||
onPivotSecondaryPressed: widget.onPivotSecondaryPressed,
|
||||
),
|
||||
// user drawing when connecting elements
|
||||
DrawingArrowWidget(style: widget.dashboard.defaultArrowStyle),
|
||||
Positioned(top: 0, right: 0, child: FlowChartMenu(
|
||||
key: widget.dashboard.chartMenuKey,
|
||||
chart: this,
|
||||
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()
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
264
library/flutter_flow_chart/lib/src/flow_chart_menu.dart
Normal file
264
library/flutter_flow_chart/lib/src/flow_chart_menu.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
import 'package:dotted_line/dotted_line.dart';
|
||||
|
||||
enum DisplayEnum {
|
||||
MENU,
|
||||
INFO
|
||||
}
|
||||
|
||||
class FlowChartMenu extends StatefulWidget {
|
||||
ChartWidgetState chart;
|
||||
Dashboard dashboard;
|
||||
double width = 100;
|
||||
|
||||
FlowChartMenu ({ super.key, required this.chart, required this.dashboard, this.width = 100 });
|
||||
@override FlowChartMenuState createState() => FlowChartMenuState();
|
||||
}
|
||||
class FlowChartMenuState extends State<FlowChartMenu> {
|
||||
@override Widget build(BuildContext context) {
|
||||
GlobalKey<FormFieldState> zoomKey = GlobalKey<FormFieldState>();
|
||||
return Container( // SHORTCUT
|
||||
width: widget.width,
|
||||
height: 50,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
color: const Color.fromRGBO(38, 166, 154, 1),
|
||||
child: Row( children : [ Expanded(flex: 2, child: Row( children: [
|
||||
widget.chart.widget.flowChart.widget.onDashboardAlertOpened == null ? Container() : Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(right: BorderSide(color: Colors.white, width: 1))
|
||||
),
|
||||
child: Row( children: [
|
||||
Tooltip( message: "open file", child:Container( child:
|
||||
Padding( padding: EdgeInsets.only(right: 15),
|
||||
child: InkWell( mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
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.chart.widget.flowChart.widget.onDashboardAlertOpened!(
|
||||
context, widget.dashboard));
|
||||
});
|
||||
},
|
||||
child: Icon(Icons.folder_open, color: Colors.white))))),
|
||||
])),
|
||||
InkWell( mouseCursor: SystemMouseCursors.click, child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(right: BorderSide(color: Colors.white, width: 1))
|
||||
),
|
||||
child: Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: PopupMenuButton<DisplayEnum>(
|
||||
child:
|
||||
Row( children: [ Icon(Icons.fullscreen, color: Colors.white), Icon(Icons.arrow_drop_down, size: 10, color: Colors.white) ]),
|
||||
initialValue: null,
|
||||
onSelected: (DisplayEnum value) {
|
||||
if (value == DisplayEnum.MENU) { widget.dashboard.isMenu = !widget.dashboard.isMenu; }
|
||||
if (value == DisplayEnum.INFO) { widget.dashboard.isInfo = !widget.dashboard.isInfo; }
|
||||
widget.chart.widget.flowChart.setState(() {});
|
||||
},
|
||||
tooltip: "display",
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<DisplayEnum>>[
|
||||
PopupMenuItem<DisplayEnum>(
|
||||
value: DisplayEnum.MENU,
|
||||
child: Row( children: [
|
||||
Icon(widget.dashboard.isMenu ? Icons.remove_red_eye : Icons.remove_red_eye_outlined),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text(widget.dashboard.isMenu ? 'hide menu' : 'show menu', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<DisplayEnum>(
|
||||
value: DisplayEnum.INFO,
|
||||
child: Row( children: [
|
||||
Icon(widget.dashboard.isMenu ? Icons.remove_red_eye : Icons.remove_red_eye_outlined),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text(widget.dashboard.isInfo ? 'hide info' : 'show info', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
]
|
||||
),)
|
||||
)
|
||||
),
|
||||
InkWell( mouseCursor: SystemMouseCursors.click, child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(right: BorderSide(color: Colors.white, width: 1))
|
||||
),
|
||||
child: Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: PopupMenuButton<double>(
|
||||
child:
|
||||
Row( children: [ Text("${(widget.dashboard.getZoomFactor() * 100).toInt()}% ",
|
||||
style: TextStyle(color: Colors.white),), Icon(Icons.arrow_drop_down, size: 10, color: Colors.white) ]),
|
||||
initialValue: [0.25, .5, 75, 1, 2, 3, 4].contains(widget.dashboard.currentZoom) ? widget.dashboard.currentZoom : null,
|
||||
onSelected: (double value) {
|
||||
widget.dashboard.setZoomFactor(value);
|
||||
},
|
||||
tooltip: "custom zoom",
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<double>>[
|
||||
PopupMenuItem<double>(
|
||||
enabled: false,
|
||||
padding: EdgeInsets.all(0),
|
||||
value: widget.dashboard.currentZoom,
|
||||
child: Wrap( alignment: WrapAlignment.center, children: [
|
||||
Padding( padding: EdgeInsets.only(left: 10, top: 10, bottom: 10) , child: TextFormField(
|
||||
key: zoomKey,
|
||||
cursorColor: const Color.fromARGB(38, 166, 154, 1),
|
||||
onChanged: (value) { },
|
||||
validator: (value) {
|
||||
try {
|
||||
double.parse(value ?? "");
|
||||
return null;
|
||||
} catch (e) { return "Invalid number"; }
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
constraints: BoxConstraints(maxWidth: 100, minWidth: 50, minHeight: 50, maxHeight: 50),
|
||||
hintText: "zoom...",
|
||||
fillColor: Colors.white,
|
||||
contentPadding: EdgeInsets.only(left: 20, right: 20, top: 5, bottom: 10),
|
||||
filled: true,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w300
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none
|
||||
)
|
||||
)),
|
||||
Tooltip(
|
||||
message: 'apply zoom',
|
||||
child:InkWell(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
if (zoomKey.currentState != null && zoomKey.currentState!.validate()) {
|
||||
widget.dashboard.setZoomFactor(double.parse(zoomKey.currentState?.value) / 100);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 10),
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: Colors.black,
|
||||
child: Icon(Icons.check, color: Colors.white)
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 0.25,
|
||||
child: Text('25%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 0.5,
|
||||
child: Text('50%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 0.75,
|
||||
child: Text('75%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 1,
|
||||
child: Text('100%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 1.25,
|
||||
child: Text('125%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 1.50,
|
||||
child: Text('150%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 2,
|
||||
child: Text('200%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 3,
|
||||
child: Text('300%'),
|
||||
),
|
||||
const PopupMenuItem<double>(
|
||||
value: 4,
|
||||
child: Text('400%'),
|
||||
),
|
||||
|
||||
],
|
||||
)
|
||||
),)),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(right: BorderSide(color: Colors.white, width: 1))
|
||||
),
|
||||
child: Row( children: [
|
||||
Tooltip( message: "zoom in", child: Container( child: Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: InkWell( mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
widget.dashboard.setZoomFactor(widget.dashboard.zoomFactor + 0.1);
|
||||
},
|
||||
child: Icon(Icons.zoom_in, color: Colors.white))))),
|
||||
Tooltip( message: "zoom out", child:Container( child: Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: InkWell( mouseCursor: SystemMouseCursors.click,
|
||||
onTap: () {
|
||||
widget.dashboard.setZoomFactor(widget.dashboard.zoomFactor - 0.1);
|
||||
},
|
||||
child: Icon(Icons.zoom_out, color: Colors.white))))),
|
||||
])),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(right: BorderSide(color: Colors.white, width: 1))
|
||||
),
|
||||
child: Row( children: [
|
||||
Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (widget.dashboard.canBack()) {
|
||||
widget.dashboard.back();
|
||||
}
|
||||
},
|
||||
mouseCursor: !widget.dashboard.canBack() ? MouseCursor.defer : SystemMouseCursors.click,
|
||||
child: Icon(Icons.undo, color: !widget.dashboard.canBack() ? Colors.grey.shade300 : Colors.white)),),
|
||||
Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (widget.dashboard.canForward()) {
|
||||
widget.dashboard.forward();
|
||||
}
|
||||
},
|
||||
mouseCursor: !widget.dashboard.canForward() ? MouseCursor.defer : SystemMouseCursors.click, child: Icon(Icons.redo,
|
||||
color: !widget.dashboard.canForward() ? Colors.grey.shade300 : Colors.white))),
|
||||
|
||||
])),
|
||||
Padding( child: Text("file opened : ${widget.dashboard.name}", overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Colors.white, fontSize: 14), textAlign: TextAlign.center),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20)),
|
||||
])),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MySeparator extends StatelessWidget {
|
||||
double width = 1; double dashWidth = 10; double dashSpace = 10;
|
||||
MySeparator({Key? key, this.width = 1, this.dashSpace = 10, this.dashWidth = 10,
|
||||
this.height = 1, this.color = Colors.black})
|
||||
: super(key: key);
|
||||
final double height;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container( width: width, child: dashSpace == 0 ?
|
||||
Divider( thickness: 2, color: color )
|
||||
: DottedLine(
|
||||
dashLength: dashWidth,
|
||||
dashGapLength: dashSpace,
|
||||
lineThickness: 2,
|
||||
dashColor: color
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
520
library/flutter_flow_chart/lib/src/flow_chart_selected_menu.dart
Normal file
520
library/flutter_flow_chart/lib/src/flow_chart_selected_menu.dart
Normal file
@@ -0,0 +1,520 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dotted_line/dotted_line.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/draw_arrow.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
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 });
|
||||
@override FlowChartSelectedMenuState createState() => FlowChartSelectedMenuState();
|
||||
}
|
||||
class FlowChartSelectedMenuState extends State<FlowChartSelectedMenu> {
|
||||
@override Widget build(BuildContext context) {
|
||||
return Container( // SHORTCUT
|
||||
width: 200,
|
||||
height: widget.height,
|
||||
color: Colors.grey.shade300,
|
||||
child: Column( children: [
|
||||
Container( padding: EdgeInsets.all(10), width: 200, height: 60,
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey, width: 1))),
|
||||
child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text("STYLE ${widget.dashboard.elementSelected.isNotEmpty ? "ELEMENT" : "ARROW"}", style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
|
||||
Text("<${widget.dashboard.arrowsSelected.isEmpty && widget.dashboard.elementSelected.isEmpty ? "general" : "selected"}>", style: TextStyle(fontSize: 12), textAlign: TextAlign.center),
|
||||
])),
|
||||
Container( width: 200, height: widget.height - 60, child: SingleChildScrollView( child: Column( children: [
|
||||
widget.dashboard.elementSelected.isNotEmpty ? Container() : Container( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20),
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey, width: 1))),
|
||||
child: Column( children: [
|
||||
Row( children: [
|
||||
InkWell( mouseCursor: SystemMouseCursors.click, child: Container(
|
||||
child: Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: PopupMenuButton<ArrowDash>(
|
||||
tooltip: "line defaults",
|
||||
constraints: BoxConstraints(maxWidth: 100),
|
||||
child: Row( children: [
|
||||
MySeparator(
|
||||
width: 45,
|
||||
dashWidth: widget.dashboard.defaultDashWidth,
|
||||
dashSpace: widget.dashboard.defaultDashSpace,
|
||||
color: Colors.black
|
||||
),
|
||||
Container(height: 25, width: 10),
|
||||
Icon(Icons.arrow_drop_down, size: 10, color: Colors.black) ]),
|
||||
initialValue: null,
|
||||
onSelected: (ArrowDash value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) {
|
||||
sel.params.dashSpace = spaceArrowDash(value);
|
||||
sel.params.dashWidth = widthArrowDash(value);
|
||||
}
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
widget.dashboard.defaultDashSpace = spaceArrowDash(value);
|
||||
widget.dashboard.defaultDashWidth = widthArrowDash(value);
|
||||
setState(() {});
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<ArrowDash>>[
|
||||
PopupMenuItem<ArrowDash>(
|
||||
value: ArrowDash.line,
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
MySeparator(width: 50, dashWidth: widthArrowDash(ArrowDash.line),
|
||||
dashSpace: spaceArrowDash(ArrowDash.line),)
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDash>(
|
||||
value: ArrowDash.largeDash,
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
MySeparator(width: 50, dashWidth: widthArrowDash(ArrowDash.largeDash), dashSpace: spaceArrowDash(ArrowDash.largeDash),)
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDash>(
|
||||
value: ArrowDash.mediumDash,
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
MySeparator(width: 50, dashWidth: widthArrowDash(ArrowDash.mediumDash), dashSpace: spaceArrowDash(ArrowDash.mediumDash),)
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDash>(
|
||||
value: ArrowDash.smallDash,
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
MySeparator(width: 50, dashWidth: widthArrowDash(ArrowDash.smallDash), dashSpace: spaceArrowDash(ArrowDash.smallDash),)
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDash>(
|
||||
value: ArrowDash.heavyDotted,
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
MySeparator(width: 50, dashWidth: widthArrowDash(ArrowDash.heavyDotted), dashSpace: spaceArrowDash(ArrowDash.heavyDotted),)
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDash>(
|
||||
value: ArrowDash.mediumDotted,
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
MySeparator(width: 50, dashWidth: widthArrowDash(ArrowDash.mediumDotted), dashSpace: spaceArrowDash(ArrowDash.mediumDotted),)
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDash>(
|
||||
value: ArrowDash.lightDotted,
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
MySeparator(width: 50, dashWidth: widthArrowDash(ArrowDash.lightDotted), dashSpace: spaceArrowDash(ArrowDash.lightDotted),)
|
||||
]),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
PopupMenuButton<void>(
|
||||
tooltip: "color picker",
|
||||
constraints: BoxConstraints(maxWidth: 664),
|
||||
child: Row( children: [
|
||||
Container(width: 15, height: 15, color: widget.dashboard.defaultColor),
|
||||
Container(height: 25, width: 5),
|
||||
Icon(Icons.arrow_drop_down, size: 10, color: Colors.black) ]),
|
||||
initialValue: null,
|
||||
onSelected: (void value) {},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<void>>[
|
||||
PopupMenuItem<void>(
|
||||
child: ColorPicker(pickerColor: Colors.black, onColorChanged: (value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) { sel.params.color = value; }
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
setState(() { widget.dashboard.defaultColor = value; });
|
||||
},),
|
||||
),
|
||||
]
|
||||
),
|
||||
Tooltip( message: "stroke width",
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 10),
|
||||
width: 45, height: 25,
|
||||
child: TextFormField( textAlign: TextAlign.center,
|
||||
readOnly: widget.dashboard.defaultDashWidth <= 0,
|
||||
initialValue: "${widget.dashboard.defaultStroke}",
|
||||
onChanged: (value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) { sel.params.thickness = double.parse(value); }
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
setState(() { widget.dashboard.defaultStroke = double.parse(value); });
|
||||
},
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
fillColor: Colors.white,
|
||||
filled: true,
|
||||
labelText: "stroke",
|
||||
labelStyle: TextStyle(fontSize: 10),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
),
|
||||
inputFormatters: [
|
||||
NumberTextInputFormatter(
|
||||
integerDigits: 100,
|
||||
decimalDigits: 1,
|
||||
maxValue: '99',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowNegative: false,
|
||||
overrideDecimalPoint: false,
|
||||
insertDecimalPoint: false,
|
||||
insertDecimalDigits: false,
|
||||
),
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
)))
|
||||
]),
|
||||
Row(children: [
|
||||
InkWell( mouseCursor: SystemMouseCursors.click, child: Container(
|
||||
child: Padding( padding: EdgeInsets.only(left: 10, top: 10, right: 10),
|
||||
child: PopupMenuButton<ArrowStyle>(
|
||||
child:
|
||||
Row( children: [ Icon(widget.dashboard.defaultArrowStyle == ArrowStyle.segmented ? Icons.turn_slight_left : widget.dashboard.defaultArrowStyle == ArrowStyle.curve ? Icons.roundabout_left : Icons.turn_sharp_left_outlined
|
||||
, color: Colors.black),
|
||||
Icon(Icons.arrow_drop_down, size: 10, color: Colors.black) ]),
|
||||
initialValue: null,
|
||||
onSelected: (ArrowStyle value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) { sel.params.style = value; }
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
widget.dashboard.defaultArrowStyle = value;
|
||||
setState(() {});
|
||||
},
|
||||
tooltip: "line styles",
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<ArrowStyle>>[
|
||||
PopupMenuItem<ArrowStyle>(
|
||||
value: ArrowStyle.segmented,
|
||||
child: Row( children: [
|
||||
Icon(Icons.turn_slight_left),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text('straight', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowStyle>(
|
||||
value: ArrowStyle.curve,
|
||||
child: Row( children: [
|
||||
Icon(Icons.roundabout_left),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text('curved', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowStyle>(
|
||||
value: ArrowStyle.rectangular,
|
||||
child: Row( children: [
|
||||
Icon(Icons.turn_sharp_left_outlined),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text('rectangular', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Tooltip( message: "space dash",
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 10),
|
||||
width: 105 / 2, height: 25,
|
||||
child: TextFormField( textAlign: TextAlign.center,
|
||||
readOnly: widget.dashboard.defaultDashWidth <= 0,
|
||||
initialValue: "${widget.dashboard.defaultDashWidth}",
|
||||
onChanged: (value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) { sel.params.dashWidth = double.parse(value); }
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
setState(() { widget.dashboard.defaultDashWidth = double.parse(value); });
|
||||
},
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
fillColor: widget.dashboard.defaultDashWidth <= 0 ? Colors.grey.shade200 : Colors.white,
|
||||
filled: true,
|
||||
labelText: "dash",
|
||||
labelStyle: TextStyle(fontSize: 10),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
),
|
||||
inputFormatters: [
|
||||
NumberTextInputFormatter(
|
||||
integerDigits: 100,
|
||||
decimalDigits: 0,
|
||||
maxValue: '99',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowNegative: false,
|
||||
overrideDecimalPoint: false,
|
||||
insertDecimalPoint: false,
|
||||
insertDecimalDigits: false,
|
||||
),
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
))),
|
||||
Tooltip( message: "space width",
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 10, top: 10),
|
||||
width: 105 / 2, height: 25,
|
||||
child: TextFormField( textAlign: TextAlign.center,
|
||||
initialValue: "${widget.dashboard.defaultDashSpace}",
|
||||
onChanged: (value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) { sel.params.dashSpace = double.parse(value); }
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
setState(() { widget.dashboard.defaultDashSpace = double.parse(value); });
|
||||
},
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
fillColor: widget.dashboard.defaultDashWidth <= 0 ? Colors.grey.shade200 : Colors.white,
|
||||
filled: true,
|
||||
labelText: "space",
|
||||
labelStyle: TextStyle(fontSize: 10),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
),
|
||||
inputFormatters: [
|
||||
NumberTextInputFormatter(
|
||||
integerDigits: 100,
|
||||
decimalDigits: 0,
|
||||
maxValue: '99',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowNegative: false,
|
||||
overrideDecimalPoint: false,
|
||||
insertDecimalPoint: false,
|
||||
insertDecimalDigits: false,
|
||||
),
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
)))
|
||||
]),
|
||||
])),
|
||||
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: [
|
||||
Row( mainAxisAlignment: MainAxisAlignment.center, children : [
|
||||
InkWell( mouseCursor: SystemMouseCursors.click, child: Container(
|
||||
child: Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: PopupMenuButton<ArrowDirection>(
|
||||
child:
|
||||
Row( children: [
|
||||
Icon(widget.dashboard.defaultArrowDirection == ArrowDirection.forward ? Icons.arrow_forward : widget.dashboard.defaultArrowDirection == ArrowDirection.backward ? Icons.arrow_back : Icons.sync_alt_outlined, color: Colors.black),
|
||||
Padding( padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Text(widget.dashboard.defaultArrowDirection == ArrowDirection.forward ? 'forward' : widget.dashboard.defaultArrowDirection == ArrowDirection.backward ? 'backward' : 'bidirectionnal')),
|
||||
Icon(Icons.arrow_drop_down, size: 10, color: Colors.black) ]),
|
||||
initialValue: null,
|
||||
onSelected: (ArrowDirection value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) { sel.params.direction = value; }
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
widget.dashboard.defaultArrowDirection = value;
|
||||
setState(() {});
|
||||
},
|
||||
tooltip: widget.dashboard.defaultArrowDirection == ArrowDirection.forward ? 'forward' : widget.dashboard.defaultArrowDirection == ArrowDirection.backward ? 'backward' : 'bidirectionnal',
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<ArrowDirection>>[
|
||||
PopupMenuItem<ArrowDirection>(
|
||||
value: ArrowDirection.forward,
|
||||
child: Row( children: [
|
||||
Icon(Icons.arrow_forward),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text('forward', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDirection>(
|
||||
value: ArrowDirection.backward,
|
||||
child: Row( children: [
|
||||
Icon(Icons.arrow_back),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text('curved', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
PopupMenuItem<ArrowDirection>(
|
||||
value: ArrowDirection.bidirectionnal,
|
||||
child: Row( children: [
|
||||
Icon(Icons.sync_alt_outlined),
|
||||
Padding( padding: EdgeInsets.only(left: 10),
|
||||
child: Text('bidirectionnal', textAlign: TextAlign.center,))
|
||||
]),
|
||||
),
|
||||
]
|
||||
),)
|
||||
)
|
||||
),
|
||||
]),
|
||||
Row(children: [
|
||||
Tooltip( message: "forward size",
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 10, top: 10),
|
||||
width: 135, height: 25,
|
||||
child: TextFormField( textAlign: TextAlign.center,
|
||||
initialValue: "${widget.dashboard.defaultForwardWidth}",
|
||||
onChanged: (value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) {
|
||||
try {
|
||||
sel.params.forwardWidth = double.parse(value);
|
||||
} catch(e) {
|
||||
sel.params.forwardWidth = 0;
|
||||
}
|
||||
}
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
setState(() {
|
||||
try {
|
||||
widget.dashboard.defaultForwardWidth = double.parse(value);
|
||||
} catch(e) {
|
||||
widget.dashboard.defaultForwardWidth = 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
fillColor: widget.dashboard.defaultDashWidth <= 0 ? Colors.grey.shade200 : Colors.white,
|
||||
filled: true,
|
||||
labelText: "forward size",
|
||||
labelStyle: TextStyle(fontSize: 10),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
),
|
||||
inputFormatters: [
|
||||
NumberTextInputFormatter(
|
||||
integerDigits: 50,
|
||||
decimalDigits: 0,
|
||||
maxValue: '50',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowNegative: false,
|
||||
overrideDecimalPoint: false,
|
||||
insertDecimalPoint: false,
|
||||
insertDecimalDigits: false,
|
||||
),
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
))),
|
||||
Padding( padding: EdgeInsets.only(top: 10, left: 5), child: Icon(Icons.arrow_forward, color: Colors.black))
|
||||
]),
|
||||
Row(children: [
|
||||
Tooltip( message: "back size",
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 10, top: 10),
|
||||
width: 135, height: 25,
|
||||
child: TextFormField( textAlign: TextAlign.center,
|
||||
initialValue: "${widget.dashboard.defaultBackWidth}",
|
||||
onChanged: (value) {
|
||||
if (widget.dashboard.elementSelected.isEmpty) {
|
||||
for(var sel in widget.dashboard.arrowsSelected) {
|
||||
try {
|
||||
sel.params.backwardWidth = double.parse(value);
|
||||
} catch(e) {
|
||||
sel.params.backwardWidth = 0;
|
||||
}
|
||||
}
|
||||
widget.dashboard.chartKey.currentState?.setState(() { });
|
||||
}
|
||||
setState(() {
|
||||
try {
|
||||
widget.dashboard.defaultBackWidth = double.parse(value);
|
||||
} catch(e) {
|
||||
widget.dashboard.defaultBackWidth = 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
style: TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
fillColor: widget.dashboard.defaultDashWidth <= 0 ? Colors.grey.shade200 : Colors.white,
|
||||
filled: true,
|
||||
labelText: "back size",
|
||||
labelStyle: TextStyle(fontSize: 10),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
),
|
||||
inputFormatters: [
|
||||
NumberTextInputFormatter(
|
||||
integerDigits: 50,
|
||||
decimalDigits: 0,
|
||||
maxValue: '50',
|
||||
decimalSeparator: '.',
|
||||
groupSeparator: ',',
|
||||
allowNegative: false,
|
||||
overrideDecimalPoint: false,
|
||||
insertDecimalPoint: false,
|
||||
insertDecimalDigits: false,
|
||||
),
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
))),
|
||||
Padding( padding: EdgeInsets.only(top: 10, left: 5), child: Icon(Icons.arrow_back, color: Colors.black))
|
||||
])
|
||||
])),
|
||||
widget.dashboard.arrowsSelected.isNotEmpty || widget.dashboard.elementSelected.isNotEmpty ? Container(
|
||||
width: 200,
|
||||
decoration: BoxDecoration(border: Border(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(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()
|
||||
])))
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MySeparator extends StatelessWidget {
|
||||
double width = 1; double dashWidth = 10; double dashSpace = 10;
|
||||
MySeparator({Key? key, this.width = 1, this.dashSpace = 10, this.dashWidth = 10,
|
||||
this.height = 1, this.color = Colors.black})
|
||||
: super(key: key);
|
||||
final double height;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container( width: width, child: dashSpace == 0 ?
|
||||
Divider( thickness: 2, color: color )
|
||||
: DottedLine(
|
||||
dashLength: dashWidth,
|
||||
dashGapLength: dashSpace,
|
||||
lineThickness: 2,
|
||||
dashColor: color
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
22
library/flutter_flow_chart/lib/src/objects/any_widget.dart
Normal file
22
library/flutter_flow_chart/lib/src/objects/any_widget.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
|
||||
class AnyWidget extends StatelessWidget {
|
||||
///
|
||||
const AnyWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width,
|
||||
height: element.size.height,
|
||||
child: element.widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
79
library/flutter_flow_chart/lib/src/objects/diamond_widget.dart
Executable file
79
library/flutter_flow_chart/lib/src/objects/diamond_widget.dart
Executable file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/src/elements/flow_element.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/element_text_widget.dart';
|
||||
|
||||
/// A kind of element
|
||||
class DiamondWidget extends StatelessWidget {
|
||||
///
|
||||
const DiamondWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width,
|
||||
height: element.size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: element.size,
|
||||
painter: _DiamondPainter(
|
||||
element: element,
|
||||
),
|
||||
),
|
||||
ElementTextWidget(element: element),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiamondPainter extends CustomPainter {
|
||||
_DiamondPainter({
|
||||
required this.element,
|
||||
});
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
final path = Path();
|
||||
|
||||
paint
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..style = PaintingStyle.fill
|
||||
..color = element.backgroundColor;
|
||||
|
||||
path
|
||||
..moveTo(size.width / 2, 0)
|
||||
..lineTo(size.width, size.height / 2)
|
||||
..lineTo(size.width / 2, size.height)
|
||||
..lineTo(0, size.height / 2)
|
||||
..close();
|
||||
|
||||
if (element.elevation > 0.01) {
|
||||
canvas.drawShadow(
|
||||
path.shift(Offset(element.elevation, element.elevation)),
|
||||
Colors.black,
|
||||
element.elevation,
|
||||
true,
|
||||
);
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
paint
|
||||
..strokeWidth = element.borderThickness
|
||||
..color = element.borderColor
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
|
||||
/// Common widget for the element text
|
||||
class ElementTextWidget extends StatelessWidget {
|
||||
///
|
||||
const ElementTextWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
child: Text(
|
||||
element.text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: element.textColor,
|
||||
fontSize: element.textSize,
|
||||
fontWeight: element.textIsBold ? FontWeight.bold : FontWeight.normal,
|
||||
fontFamily: element.fontFamily,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/src/elements/flow_element.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/element_text_widget.dart';
|
||||
|
||||
/// A kind of element
|
||||
class HexagonWidget extends StatelessWidget {
|
||||
///
|
||||
const HexagonWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width,
|
||||
height: element.size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: element.size,
|
||||
painter: _HexagonPainter(
|
||||
element: element,
|
||||
),
|
||||
),
|
||||
ElementTextWidget(element: element),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HexagonPainter extends CustomPainter {
|
||||
_HexagonPainter({
|
||||
required this.element,
|
||||
});
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
final path = Path();
|
||||
|
||||
paint
|
||||
..style = PaintingStyle.fill
|
||||
..color = element.backgroundColor;
|
||||
|
||||
path
|
||||
..moveTo(0, size.height / 2)
|
||||
..lineTo(size.width / 4, size.height)
|
||||
..lineTo(size.width * 3 / 4, size.height)
|
||||
..lineTo(size.width, size.height / 2)
|
||||
..lineTo(size.width * 3 / 4, 0)
|
||||
..lineTo(size.width / 4, 0)
|
||||
..close();
|
||||
|
||||
if (element.elevation > 0.01) {
|
||||
canvas.drawShadow(
|
||||
path.shift(Offset(element.elevation, element.elevation)),
|
||||
Colors.black,
|
||||
element.elevation,
|
||||
true,
|
||||
);
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
paint
|
||||
..strokeWidth = element.borderThickness
|
||||
..color = element.borderColor
|
||||
..style = PaintingStyle.stroke;
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
72
library/flutter_flow_chart/lib/src/objects/oval_widget.dart
Normal file
72
library/flutter_flow_chart/lib/src/objects/oval_widget.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/src/elements/flow_element.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/element_text_widget.dart';
|
||||
|
||||
/// A kind of element
|
||||
class OvalWidget extends StatelessWidget {
|
||||
///
|
||||
const OvalWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width,
|
||||
height: element.size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: element.size,
|
||||
painter: _OvalPainter(
|
||||
element: element,
|
||||
),
|
||||
),
|
||||
ElementTextWidget(element: element),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OvalPainter extends CustomPainter {
|
||||
_OvalPainter({
|
||||
required this.element,
|
||||
});
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
final path = Path();
|
||||
|
||||
paint
|
||||
..style = PaintingStyle.fill
|
||||
..color = element.backgroundColor;
|
||||
|
||||
path.addOval(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||
if (element.elevation > 0.01) {
|
||||
canvas.drawShadow(
|
||||
path.shift(Offset(element.elevation, element.elevation)),
|
||||
Colors.black,
|
||||
element.elevation,
|
||||
true,
|
||||
);
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
paint
|
||||
..strokeWidth = element.borderThickness
|
||||
..color = element.borderColor
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/src/elements/flow_element.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/element_text_widget.dart';
|
||||
|
||||
/// A kind of element
|
||||
class ParallelogramWidget extends StatelessWidget {
|
||||
///
|
||||
const ParallelogramWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width,
|
||||
height: element.size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: element.size,
|
||||
painter: _ParallelogramPainter(
|
||||
element: element,
|
||||
),
|
||||
),
|
||||
ElementTextWidget(element: element),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ParallelogramPainter extends CustomPainter {
|
||||
_ParallelogramPainter({
|
||||
required this.element,
|
||||
});
|
||||
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
final path = Path();
|
||||
|
||||
paint
|
||||
..style = PaintingStyle.fill
|
||||
..color = element.backgroundColor;
|
||||
|
||||
path
|
||||
..moveTo(size.width / 8, 0)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width - size.width / 8, size.height)
|
||||
..lineTo(0, size.height)
|
||||
..close();
|
||||
|
||||
if (element.elevation > 0.01) {
|
||||
canvas.drawShadow(
|
||||
path.shift(Offset(element.elevation, element.elevation)),
|
||||
Colors.black,
|
||||
element.elevation,
|
||||
true,
|
||||
);
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
paint
|
||||
..strokeWidth = element.borderThickness
|
||||
..color = element.borderColor
|
||||
..style = PaintingStyle.stroke;
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
46
library/flutter_flow_chart/lib/src/objects/rectangle_widget.dart
Executable file
46
library/flutter_flow_chart/lib/src/objects/rectangle_widget.dart
Executable file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/src/elements/flow_element.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/element_text_widget.dart';
|
||||
|
||||
/// A kind of element
|
||||
class RectangleWidget extends StatelessWidget {
|
||||
///
|
||||
const RectangleWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width,
|
||||
height: element.size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: element.backgroundColor,
|
||||
boxShadow: [
|
||||
if (element.elevation > 0.01)
|
||||
BoxShadow(
|
||||
color: Colors.grey,
|
||||
offset: Offset(element.elevation, element.elevation),
|
||||
blurRadius: element.elevation * 1.3,
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: element.borderColor,
|
||||
width: element.borderThickness,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElementTextWidget(element: element),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/src/elements/flow_element.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/element_text_widget.dart';
|
||||
|
||||
/// A kind of element
|
||||
class StorageWidget extends StatelessWidget {
|
||||
///
|
||||
const StorageWidget({
|
||||
required this.element,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width,
|
||||
height: element.size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: element.size,
|
||||
painter: _StoragePainter(
|
||||
element: element,
|
||||
),
|
||||
),
|
||||
ElementTextWidget(element: element),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StoragePainter extends CustomPainter {
|
||||
_StoragePainter({
|
||||
required this.element,
|
||||
});
|
||||
|
||||
final FlowElement element;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
final path = Path();
|
||||
final path2 = Path();
|
||||
|
||||
paint
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..style = PaintingStyle.fill
|
||||
..color = element.backgroundColor;
|
||||
|
||||
path2
|
||||
..moveTo(size.width, size.height / 4.0 / 2.0)
|
||||
..lineTo(size.width, size.height)
|
||||
..lineTo(0, size.height)
|
||||
..lineTo(0, size.height / 4.0 / 2.0)
|
||||
|
||||
// oval
|
||||
..addArc(Rect.fromLTWH(0, 0, size.width, size.height / 4.0), pi, pi)
|
||||
..addArc(Rect.fromLTWH(0, 0, size.width, size.height / 4.0), 0, pi)
|
||||
..addArc(Rect.fromLTWH(0, 4, size.width, size.height / 4.0 + 4), 0, pi);
|
||||
|
||||
if (element.elevation > 0.01) {
|
||||
canvas.drawShadow(
|
||||
path2.shift(Offset(element.elevation, element.elevation)),
|
||||
Colors.black,
|
||||
element.elevation,
|
||||
true,
|
||||
);
|
||||
}
|
||||
canvas.drawPath(path2, paint);
|
||||
|
||||
paint
|
||||
..strokeWidth = element.borderThickness
|
||||
..color = element.borderColor
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas
|
||||
..drawPath(path, paint)
|
||||
..drawPath(path2, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
859
library/flutter_flow_chart/lib/src/ui/draw_arrow.dart
Executable file
859
library/flutter_flow_chart/lib/src/ui/draw_arrow.dart
Executable file
@@ -0,0 +1,859 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/segment_handler.dart';
|
||||
|
||||
|
||||
enum ArrowDash {
|
||||
/// Arrow pointing from the source to the destination.
|
||||
line,
|
||||
largeDash,
|
||||
mediumDash,
|
||||
smallDash,
|
||||
heavyDotted,
|
||||
mediumDotted,
|
||||
lightDotted,
|
||||
}
|
||||
|
||||
double spaceArrowDash(ArrowDash dash) {
|
||||
if (dash == ArrowDash.line) { return 0;
|
||||
} else if (dash == ArrowDash.largeDash) { return 12;
|
||||
} else if (dash == ArrowDash.mediumDash) { return 10;
|
||||
} else if (dash == ArrowDash.smallDash) { return 5;
|
||||
} else if (dash == ArrowDash.heavyDotted) { return 2.5;
|
||||
} else if (dash == ArrowDash.mediumDotted) { return 5;
|
||||
} else if (dash == ArrowDash.lightDotted) { return 10; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
double widthArrowDash(ArrowDash dash) {
|
||||
if (dash == ArrowDash.line) { return 0;
|
||||
} else if (dash == ArrowDash.largeDash) { return 7;
|
||||
} else if (dash == ArrowDash.mediumDash) { return 5;
|
||||
} else if (dash == ArrowDash.smallDash) { return 2.5;
|
||||
} else if (dash == ArrowDash.heavyDotted) { return 1;
|
||||
} else if (dash == ArrowDash.mediumDotted) { return 1;
|
||||
} else if (dash == ArrowDash.lightDotted) { return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
enum ArrowDirection {
|
||||
/// Arrow pointing from the source to the destination.
|
||||
forward,
|
||||
|
||||
/// Arrow pointing from the destination to the source.
|
||||
backward,
|
||||
bidirectionnal,
|
||||
}
|
||||
/// Arrow style enumeration
|
||||
enum ArrowStyle {
|
||||
/// A curved arrow which points nicely to each handlers
|
||||
curve,
|
||||
|
||||
/// A segmented line where pivot points can be added and curvature between
|
||||
/// them can be adjusted with a tension.
|
||||
segmented,
|
||||
|
||||
/// A rectangular shaped line.
|
||||
rectangular,
|
||||
}
|
||||
|
||||
/// Arrow parameters used by [DrawArrow] widget
|
||||
class ArrowParams extends ChangeNotifier {
|
||||
///
|
||||
ArrowParams({
|
||||
this.dashWidth = 0,
|
||||
this.dashSpace = 0,
|
||||
this.backwardWidth = 10,
|
||||
this.forwardWidth = 10,
|
||||
this.thickness = 1.7,
|
||||
this.headRadius = 6,
|
||||
double tailLength = 25.0,
|
||||
this.color = Colors.black,
|
||||
this.style,
|
||||
this.tension = 1.0,
|
||||
this.direction = ArrowDirection.forward,
|
||||
this.startArrowPosition = Alignment.centerRight,
|
||||
this.endArrowPosition = Alignment.centerLeft,
|
||||
}) : _tailLength = tailLength;
|
||||
///
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'thickness': thickness,
|
||||
'head_radius': headRadius,
|
||||
'tail_length': _tailLength,
|
||||
'color': color.value,
|
||||
'arrow_style': style?.index,
|
||||
'tension': tension,
|
||||
'direction': direction?.index,
|
||||
'dash_width': dashWidth,
|
||||
'dash_space': dashSpace,
|
||||
'backward_arrow_width': backwardWidth,
|
||||
'forward_arrow_width': forwardWidth,
|
||||
'start_arrow_position_x': startArrowPosition.x,
|
||||
'start_arrow_position_y': startArrowPosition.y,
|
||||
'end_arrow_position_x': endArrowPosition.x,
|
||||
'end_arrow_position_y': endArrowPosition.y,
|
||||
};
|
||||
}
|
||||
///
|
||||
factory ArrowParams.fromMap(Map<String, dynamic> map) {
|
||||
return ArrowParams(
|
||||
dashSpace: map['dash_space'] as double,
|
||||
dashWidth: map['dash_width'] as double,
|
||||
backwardWidth: map['backward_arrow_width'] as double,
|
||||
forwardWidth: map['forward_arrow_width'] as double,
|
||||
thickness: map['thickness'] as double,
|
||||
direction: ArrowDirection.values[map['direction'] as int? ?? 0],
|
||||
headRadius: map['head_radius'] as double? ?? 6.0,
|
||||
tailLength: map['tail_length'] as double? ?? 25.0,
|
||||
color: Color(map['color'] as int),
|
||||
style: ArrowStyle.values[map['arrow_style'] as int? ?? 0],
|
||||
tension: map['tension'] as double? ?? 1,
|
||||
startArrowPosition: Alignment(
|
||||
map['start_arrow_position_x'] as double,
|
||||
map['start_arrow_position_y'] as double,
|
||||
),
|
||||
endArrowPosition: Alignment(
|
||||
map['end_arrow_position_x'] as double,
|
||||
map['end_arrow_position_y'] as double,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
factory ArrowParams.fromJson(String source) =>
|
||||
ArrowParams.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
/// Arrow thickness.
|
||||
double thickness;
|
||||
double forwardWidth;
|
||||
double backwardWidth;
|
||||
/// The radius of arrow tip.
|
||||
double headRadius;
|
||||
|
||||
/// Arrow color.
|
||||
Color color;
|
||||
|
||||
/// The start position alignment.
|
||||
final Alignment startArrowPosition;
|
||||
|
||||
/// The end position alignment.
|
||||
final Alignment endArrowPosition;
|
||||
|
||||
/// The tail length of the arrow.
|
||||
double _tailLength;
|
||||
|
||||
/// The style of the arrow.
|
||||
ArrowStyle? style;
|
||||
|
||||
ArrowDirection? direction;
|
||||
double dashWidth = 0;
|
||||
double dashSpace = 0;
|
||||
/// The curve tension for pivot points when using [ArrowStyle.segmented].
|
||||
/// 0 means no curve on segments.
|
||||
double tension;
|
||||
|
||||
///
|
||||
ArrowParams copyWith({
|
||||
double? thickness,
|
||||
Color? color,
|
||||
ArrowStyle? style,
|
||||
ArrowDirection? direction,
|
||||
double? tension,
|
||||
double? dashWidth,
|
||||
double? dashSpace,
|
||||
Alignment? startArrowPosition,
|
||||
Alignment? endArrowPosition,
|
||||
double? backwardWidth,
|
||||
double? forwardWidth,
|
||||
}) {
|
||||
return ArrowParams(
|
||||
backwardWidth: backwardWidth ?? this.backwardWidth,
|
||||
forwardWidth: forwardWidth ?? this.forwardWidth,
|
||||
thickness: thickness ?? this.thickness,
|
||||
color: color ?? this.color,
|
||||
style: style ?? this.style,
|
||||
dashSpace: dashSpace ?? this.dashSpace,
|
||||
dashWidth: dashWidth ?? this.dashWidth,
|
||||
direction: direction ?? this.direction,
|
||||
tension: tension ?? this.tension,
|
||||
startArrowPosition: startArrowPosition ?? this.startArrowPosition,
|
||||
endArrowPosition: endArrowPosition ?? this.endArrowPosition,
|
||||
);
|
||||
}
|
||||
///
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
///
|
||||
void setScale(double currentZoom, double factor) {
|
||||
thickness = thickness / currentZoom * factor;
|
||||
headRadius = headRadius / currentZoom * factor;
|
||||
_tailLength = _tailLength / currentZoom * factor;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///
|
||||
double get tailLength => _tailLength;
|
||||
}
|
||||
|
||||
/// Notifier to update arrows position, starting/ending points and params
|
||||
class DrawingArrow extends ChangeNotifier {
|
||||
DrawingArrow._();
|
||||
|
||||
/// Singleton instance of this.
|
||||
static final instance = DrawingArrow._();
|
||||
|
||||
/// Arrow parameters.
|
||||
ArrowParams params = ArrowParams();
|
||||
String fromID = "";
|
||||
String toID = "";
|
||||
|
||||
/// Sets the parameters.
|
||||
void setParams(ArrowParams params) {
|
||||
this.params = params;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Starting arrow offset.
|
||||
Offset from = Offset.zero;
|
||||
|
||||
///
|
||||
void setFrom(Offset from) {
|
||||
this.from = from;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Ending arrow offset.
|
||||
Offset to = Offset.zero;
|
||||
|
||||
///
|
||||
void setTo(Offset to) {
|
||||
this.to = to;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///
|
||||
bool isZero() {
|
||||
return from == Offset.zero && to == Offset.zero;
|
||||
}
|
||||
|
||||
///
|
||||
void reset() {
|
||||
params = ArrowParams();
|
||||
from = Offset.zero;
|
||||
to = Offset.zero;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw arrow from [srcElement] to [destElement]
|
||||
/// using [arrowParams] parameters
|
||||
class DrawArrow extends StatefulWidget {
|
||||
ChartWidgetState flow;
|
||||
///
|
||||
DrawArrow({
|
||||
required this.flow,
|
||||
required this.index,
|
||||
required this.srcElement,
|
||||
required this.destElement,
|
||||
required List<Pivot> pivots,
|
||||
super.key,
|
||||
ArrowParams? arrowParams,
|
||||
}) : arrowParams = arrowParams ?? ArrowParams(),
|
||||
pivots = PivotsNotifier(pivots);
|
||||
|
||||
final int index;
|
||||
|
||||
///
|
||||
final ArrowParams arrowParams;
|
||||
|
||||
///
|
||||
final FlowElement srcElement;
|
||||
|
||||
///
|
||||
final FlowElement destElement;
|
||||
|
||||
///
|
||||
final PivotsNotifier pivots;
|
||||
|
||||
@override
|
||||
State<DrawArrow> createState() => DrawArrowState();
|
||||
}
|
||||
|
||||
class DrawArrowState extends State<DrawArrow> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.srcElement.addListener(_elementChanged);
|
||||
widget.destElement.addListener(_elementChanged);
|
||||
widget.pivots.addListener(_elementChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.srcElement.removeListener(_elementChanged);
|
||||
widget.destElement.removeListener(_elementChanged);
|
||||
widget.pivots.removeListener(_elementChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _elementChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var from = Offset.zero;
|
||||
var to = Offset.zero;
|
||||
from = Offset(
|
||||
widget.srcElement.position.dx +
|
||||
widget.srcElement.handlerSize / 2.0 +
|
||||
(widget.srcElement.size.width *
|
||||
((widget.arrowParams.startArrowPosition.x + 1) / 2)),
|
||||
widget.srcElement.position.dy +
|
||||
widget.srcElement.handlerSize / 2.0 +
|
||||
(widget.srcElement.size.height *
|
||||
((widget.arrowParams.startArrowPosition.y + 1) / 2)),
|
||||
);
|
||||
to = Offset(
|
||||
widget.destElement.position.dx +
|
||||
widget.destElement.handlerSize / 2.0 +
|
||||
(widget.destElement.size.width *
|
||||
((widget.arrowParams.endArrowPosition.x + 1) / 2)),
|
||||
widget.destElement.position.dy +
|
||||
widget.destElement.handlerSize / 2.0 +
|
||||
(widget.destElement.size.height *
|
||||
((widget.arrowParams.endArrowPosition.y + 1) / 2)),
|
||||
);
|
||||
GlobalKey<GraphParamsWidgetState> key = GlobalKey<GraphParamsWidgetState>();
|
||||
print("THERE");
|
||||
return Stack( children : [
|
||||
GraphParamsWidget(key: key, element: widget.srcElement, index: widget.index, comp: widget.flow),
|
||||
RepaintBoundary(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
print(from);
|
||||
print(to);
|
||||
print(widget.pivots);
|
||||
var painter = ArrowPainter(
|
||||
connIndex: widget.index,
|
||||
elementIndex: widget.flow.widget.dashboard.elements.indexOf(widget.srcElement),
|
||||
fromID: "${widget.srcElement.id}_${widget.index}",
|
||||
toID: "${widget.destElement.id}_${widget.index}",
|
||||
params: widget.arrowParams,
|
||||
from: from, to: to,
|
||||
pivots: widget.pivots.value,
|
||||
);
|
||||
if ( widget.flow.widget.dashboard.arrows.where(
|
||||
(element) => element.fromID == "${widget.srcElement.id}${widget.index}").isEmpty) {
|
||||
widget.flow.widget.dashboard.arrows.add(painter);
|
||||
} else {
|
||||
var i = widget.flow.widget.dashboard.arrows.indexWhere(
|
||||
(element) => element.fromID == "${widget.srcElement.id}${widget.index}");
|
||||
painter.isSelected = widget.flow.widget.dashboard.arrows[i].isSelected;
|
||||
widget.flow.widget.dashboard.arrows[i] = painter;
|
||||
}
|
||||
return CustomPaint(
|
||||
painter: painter,
|
||||
child: Container(),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class GraphParamsWidget extends StatefulWidget {
|
||||
ChartWidgetState comp;
|
||||
bool isShowed = false;
|
||||
Offset? position;
|
||||
FlowElement element;
|
||||
int index;
|
||||
GraphParamsWidget({ Key? key, required this.element, required this.comp,
|
||||
required this.index }): super(key: key);
|
||||
@override GraphParamsWidgetState createState() => GraphParamsWidgetState();
|
||||
}
|
||||
class GraphParamsWidgetState extends State<GraphParamsWidget> {
|
||||
@override Widget build(BuildContext context) {
|
||||
return !widget.isShowed ? Container() : Positioned(
|
||||
top: (widget.position?.dy ?? 0) - 5, left: (widget.position?.dx ?? 0) - 5,
|
||||
child: MouseRegion( cursor: SystemMouseCursors.click,
|
||||
onHover: (event) => setState(() { widget.isShowed = true; }),
|
||||
child: Container(
|
||||
child: Row(children: [
|
||||
IconButton(onPressed: () {
|
||||
widget.comp.setState(() {
|
||||
widget.comp.widget.dashboard.arrows.removeWhere((el) => el.fromID == "${widget.element.id}${widget.index}");
|
||||
widget.element.next.removeAt(widget.index);
|
||||
});
|
||||
}, icon: Icon(Icons.delete))
|
||||
],))));
|
||||
}
|
||||
}
|
||||
|
||||
class ArrowInfoWidget extends StatefulWidget {
|
||||
ArrowInfoWidget ({ Key? key, }): super(key: key);
|
||||
@override ArrowInfoWidgetState createState() => ArrowInfoWidgetState();
|
||||
}
|
||||
class ArrowInfoWidgetState extends State<ArrowInfoWidget> {
|
||||
@override Widget build(BuildContext context) {
|
||||
return SingleChildScrollView( child: Column(children: [
|
||||
Container(height: 50,
|
||||
decoration: BoxDecoration(color: Colors.grey.shade300, border: Border(bottom: BorderSide(color: Colors.grey, width: 1))),
|
||||
child: Center( child: Text("<Arrow> Style", style: TextStyle(fontSize: 20)))),
|
||||
Container(height: 50,
|
||||
decoration: BoxDecoration(color: Colors.grey.shade300, border: Border(bottom: BorderSide(color: Colors.grey, width: 1))),
|
||||
child: Row(children: [],)
|
||||
),
|
||||
],) );
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint the arrow connection taking in count the
|
||||
/// [ArrowParams.startArrowPosition] and
|
||||
/// [ArrowParams.endArrowPosition] alignment.
|
||||
class ArrowPainter extends CustomPainter {
|
||||
final String fromID;
|
||||
final String toID;
|
||||
bool isSelected = false;
|
||||
///
|
||||
ArrowPainter({
|
||||
this.elementIndex,
|
||||
this.connIndex,
|
||||
required this.toID,
|
||||
required this.fromID,
|
||||
this.isSelected = false,
|
||||
required this.params,
|
||||
required this.from,
|
||||
required this.to,
|
||||
List<Pivot>? pivots,
|
||||
}) : pivots = pivots ?? [];
|
||||
|
||||
///
|
||||
final ArrowParams params;
|
||||
///
|
||||
Offset from;
|
||||
int? elementIndex;
|
||||
int? connIndex;
|
||||
///
|
||||
Offset to;
|
||||
|
||||
///
|
||||
Path path = Path();
|
||||
Path dashed = Path();
|
||||
Path subPath = Path();
|
||||
|
||||
///
|
||||
final List<List<Offset>> lines = [];
|
||||
|
||||
///
|
||||
final List<Pivot> pivots;
|
||||
|
||||
late DashedPathProperties _dashedPathProperties;
|
||||
|
||||
final arrowSize = 15;
|
||||
final arrowAngle= 25 * math.pi / 180;
|
||||
|
||||
Map<String, dynamic> serialize() {
|
||||
Map<String, dynamic> graphElement = {};
|
||||
graphElement['from'] = { "id" : fromID.split("_")[0], "x" : from.dx, "y" : from.dy };
|
||||
graphElement['to'] = { "id" : toID.split("_")[0], "x" : to.dx, "y" : to.dy };
|
||||
graphElement['params'] = params.toMap();
|
||||
return graphElement;
|
||||
}
|
||||
|
||||
factory ArrowPainter.fromMap(Map<String, dynamic> map) {
|
||||
final e = ArrowPainter(
|
||||
connIndex: map['connIndex'] as int,
|
||||
elementIndex: map['elementIndex'] as int,
|
||||
toID: map['toID'] as String,
|
||||
fromID: map['fromID'] as String,
|
||||
isSelected: map['isSelected'] as bool,
|
||||
params: ArrowParams.fromMap(map['params']),
|
||||
from: Offset(
|
||||
map['fromDx'] as double,
|
||||
map['fromDy'] as double,
|
||||
),
|
||||
to: Offset(
|
||||
map['toDx'] as double,
|
||||
map['toDy'] as double,
|
||||
),
|
||||
pivots: (map['pivots'] as List).map<Pivot>((e) => Pivot.fromMap(e as Map<String, dynamic>)).toList(),
|
||||
);
|
||||
return e;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'fromID': fromID,
|
||||
'elementIndex': elementIndex,
|
||||
'connIndex': connIndex,
|
||||
'isSelected': isSelected.toString(),
|
||||
'params': params.toJson(),
|
||||
'from': from.toString(),
|
||||
'to': to.toString(),
|
||||
'pivots': json.encode(pivots.map((e) => e.toMap()).toList()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (params.dashSpace > 0) {
|
||||
_dashedPathProperties = DashedPathProperties(
|
||||
path: Path(),
|
||||
dashLength: params.dashWidth,
|
||||
dashGapLength: params.dashSpace,
|
||||
);
|
||||
}
|
||||
final paint = Paint()..strokeWidth = params.thickness;
|
||||
from = Offset(from.dx - params.headRadius - 2, from.dy - params.headRadius - 2 );
|
||||
to = Offset(to.dx - params.headRadius - 3, to.dy - params.headRadius - 2 );
|
||||
|
||||
if (params.style == ArrowStyle.curve) { drawCurve(canvas, paint);
|
||||
} else if (params.style == ArrowStyle.segmented) { drawLine();
|
||||
} else if (params.style == ArrowStyle.rectangular) { drawRectangularLine(canvas, paint); }
|
||||
paint
|
||||
..color = isSelected ? Colors.red : params.color
|
||||
..style = PaintingStyle.stroke;
|
||||
canvas.drawPath(dashed, paint);
|
||||
canvas.drawPath(subPath, paint);
|
||||
}
|
||||
|
||||
Path drawArrow(Offset a, Offset b, Alignment? start, bool isCurve, bool backward) {
|
||||
double arrowSize = 0;
|
||||
if (backward) { arrowSize = params.backwardWidth;
|
||||
} else { arrowSize = params.forwardWidth; }
|
||||
const arrowAngle = math.pi / 6;
|
||||
final dX = b.dx - a.dx;
|
||||
final dY = b.dy - a.dy;
|
||||
double angle = math.atan2(dY, dX);
|
||||
|
||||
if (start == Alignment.bottomCenter) { angle = -33 - (isCurve ? 3 : 0);
|
||||
} else if (start == Alignment.topCenter) { angle = 33 + (isCurve ? 3 : 0);
|
||||
} else if (start == Alignment.centerRight) { angle = 66;
|
||||
} else if (start == Alignment.centerLeft) { angle = 0; }
|
||||
|
||||
subPath.moveTo(b.dx - arrowSize * math.cos(angle - arrowAngle),
|
||||
b.dy - arrowSize * math.sin(angle - arrowAngle));
|
||||
subPath.lineTo(b.dx, b.dy);
|
||||
subPath.lineTo(b.dx - arrowSize * math.cos(angle + arrowAngle),
|
||||
b.dy - arrowSize * math.sin(angle + arrowAngle));
|
||||
return subPath;
|
||||
}
|
||||
|
||||
/// Draw a segmented line with a tension between points.
|
||||
void drawLine() {
|
||||
final points = [from];
|
||||
for (final pivot in pivots) {
|
||||
points.add(pivot.pivot);
|
||||
}
|
||||
points.add(to);
|
||||
|
||||
path.moveTo(points.first.dx, points.first.dy);
|
||||
|
||||
for (var i = 0; i < points.length - 1; i++) {
|
||||
final p0 = (i > 0) ? points[i - 1] : points[0];
|
||||
final p1 = points[i];
|
||||
final p2 = points[i + 1];
|
||||
final p3 = (i != points.length - 2) ? points[i + 2] : p2;
|
||||
|
||||
final cp1x = p1.dx + (p2.dx - p0.dx) / 6 * params.tension;
|
||||
final cp1y = p1.dy + (p2.dy - p0.dy) / 6 * params.tension;
|
||||
|
||||
final cp2x = p2.dx - (p3.dx - p1.dx) / 6 * params.tension;
|
||||
final cp2y = p2.dy - (p3.dy - p1.dy) / 6 * params.tension;
|
||||
|
||||
path.cubicTo(cp1x, cp1y, cp2x, cp2y, p2.dx, p2.dy);
|
||||
}
|
||||
|
||||
if (params.dashSpace > 0) {
|
||||
dashed = _getDashedPath(path, params.dashWidth, params.dashSpace);
|
||||
} else { dashed = path; }
|
||||
if (ArrowDirection.forward == params.direction || ArrowDirection.bidirectionnal == params.direction) {
|
||||
subPath = drawArrow(from, to, null, false, false);
|
||||
}
|
||||
if (ArrowDirection.backward == params.direction || ArrowDirection.bidirectionnal == params.direction) {
|
||||
subPath = drawArrow(to, from, null, false, true); }
|
||||
}
|
||||
|
||||
/// Draw a rectangular line
|
||||
void drawRectangularLine(Canvas canvas, Paint paint) {
|
||||
// calculating offsetted pivot
|
||||
var pivot1 = Offset(from.dx, from.dy);
|
||||
if (params.startArrowPosition.y == 1) {
|
||||
pivot1 = Offset(from.dx, from.dy + params.tailLength);
|
||||
} else if (params.startArrowPosition.y == -1) {
|
||||
pivot1 = Offset(from.dx, from.dy - params.tailLength);
|
||||
}
|
||||
|
||||
final pivot2 = Offset(to.dx, pivot1.dy);
|
||||
|
||||
path
|
||||
..moveTo(from.dx, from.dy)
|
||||
..lineTo(pivot1.dx, pivot1.dy)
|
||||
..lineTo(pivot2.dx, pivot2.dy)
|
||||
..lineTo(to.dx, to.dy);
|
||||
|
||||
lines.addAll([
|
||||
[from, pivot2],
|
||||
[pivot2, to],
|
||||
]);
|
||||
if (params.dashSpace != 0) {
|
||||
dashed = _getDashedPath(path, params.dashWidth, params.dashSpace);
|
||||
} else { dashed = path; }
|
||||
Alignment start = Alignment.centerLeft;
|
||||
if (pivot2.dy != to.dy) {
|
||||
if (to.dy < pivot2.dy) { start = Alignment.bottomCenter;
|
||||
} else { start = Alignment.topCenter; }
|
||||
}
|
||||
if (pivot2.dx != to.dx) {
|
||||
if (to.dx < pivot2.dx) { start = Alignment.centerRight;
|
||||
} else { start = Alignment.centerLeft; }
|
||||
}
|
||||
if (ArrowDirection.forward == params.direction || ArrowDirection.bidirectionnal == params.direction) {
|
||||
subPath = drawArrow(from, to, start, false, false);
|
||||
}
|
||||
if (ArrowDirection.backward == params.direction || ArrowDirection.bidirectionnal == params.direction) {
|
||||
subPath = drawArrow(to, from, params.startArrowPosition, false, true); }
|
||||
}
|
||||
|
||||
/// Draws a curve starting/ending the handler linearly from the center
|
||||
/// of the element.
|
||||
void drawCurve(Canvas canvas, Paint paint) {
|
||||
var distance = 0.0;
|
||||
var dx = 0.0;
|
||||
var dy = 0.0;
|
||||
|
||||
final p0 = Offset(from.dx, from.dy);
|
||||
final p4 = Offset(to.dx, to.dy);
|
||||
distance = (p4 - p0).distance / 3;
|
||||
|
||||
// checks for the arrow direction
|
||||
if (params.startArrowPosition.x > 0) {
|
||||
dx = distance;
|
||||
} else if (params.startArrowPosition.x < 0) {
|
||||
dx = -distance;
|
||||
}
|
||||
if (params.startArrowPosition.y > 0) {
|
||||
dy = distance;
|
||||
} else if (params.startArrowPosition.y < 0) {
|
||||
dy = -distance;
|
||||
}
|
||||
final p1 = Offset(from.dx + dx, from.dy + dy);
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
|
||||
// checks for the arrow direction
|
||||
if (params.endArrowPosition.x > 0) {
|
||||
dx = distance;
|
||||
} else if (params.endArrowPosition.x < 0) {
|
||||
dx = -distance;
|
||||
}
|
||||
if (params.endArrowPosition.y > 0) {
|
||||
dy = distance;
|
||||
} else if (params.endArrowPosition.y < 0) {
|
||||
dy = -distance;
|
||||
}
|
||||
final p3 = params.endArrowPosition == Alignment.center
|
||||
? Offset(to.dx, to.dy)
|
||||
: Offset(to.dx + dx, to.dy + dy);
|
||||
final p2 = Offset(
|
||||
p1.dx + (p3.dx - p1.dx) / 2,
|
||||
p1.dy + (p3.dy - p1.dy) / 2,
|
||||
);
|
||||
|
||||
path
|
||||
..moveTo(p0.dx, p0.dy)
|
||||
..conicTo(p1.dx, p1.dy, p2.dx, p2.dy, 1)
|
||||
..conicTo(p3.dx, p3.dy, p4.dx, p4.dy, 1);
|
||||
if (params.dashSpace > 0) {
|
||||
dashed = _getDashedPath(path, params.dashWidth, params.dashSpace);
|
||||
} else { dashed = path; }
|
||||
Alignment start = Alignment.center;
|
||||
if (p3.dy != p4.dy) {
|
||||
if (p4.dy < p3.dy) { start = Alignment.bottomCenter;
|
||||
} else { start = Alignment.topCenter; }
|
||||
}
|
||||
if (p3.dx != p4.dx) {
|
||||
if (p4.dx < p3.dx) { start = Alignment.centerRight;
|
||||
} else { start = Alignment.centerLeft; }
|
||||
}
|
||||
if (ArrowDirection.forward == params.direction || ArrowDirection.bidirectionnal == params.direction) {
|
||||
subPath = drawArrow(from, to, start, start == Alignment.center, false);
|
||||
}
|
||||
if (ArrowDirection.backward == params.direction || ArrowDirection.bidirectionnal == params.direction) {
|
||||
subPath = drawArrow(to, from, params.startArrowPosition, params.startArrowPosition == Alignment.center, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(ArrowPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Path _getDashedPath(
|
||||
Path originalPath, double dashLength, double dashGapLength) {
|
||||
final metricsIterator = originalPath.computeMetrics().iterator;
|
||||
while (metricsIterator.moveNext()) {
|
||||
final metric = metricsIterator.current;
|
||||
_dashedPathProperties.extractedPathLength = 0.0;
|
||||
while (_dashedPathProperties.extractedPathLength < metric.length) {
|
||||
if (_dashedPathProperties.addDashNext) {
|
||||
_dashedPathProperties.addDash(metric, dashLength);
|
||||
} else {
|
||||
_dashedPathProperties.addDashGap(metric, dashGapLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _dashedPathProperties.path;
|
||||
}
|
||||
|
||||
|
||||
bool isLine(Offset position) {
|
||||
for (double i=-20; i < 20; i++) {
|
||||
for (double y=-20; y < 20; y++) {
|
||||
var pos = position + Offset(i, y);
|
||||
if (path.contains(pos)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool? hitTest(Offset position) {
|
||||
/* graphkey?.currentState?.widget.isShowed = isLine(position);
|
||||
if (isLine(position)) { graphkey?.currentState?.widget.position = graphkey?.currentState?.widget.position ?? position; }
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
if (graphkey != null && graphkey!.currentState != null && !graphkey!.currentState!.widget.isShowed) {
|
||||
graphkey?.currentState?.widget.position = null;
|
||||
}
|
||||
});
|
||||
graphkey?.currentState?.setState(() {});*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifier for pivot points.
|
||||
class PivotsNotifier extends ValueNotifier<List<Pivot>> {
|
||||
///
|
||||
PivotsNotifier(super.value) {
|
||||
for (final pivot in value) {
|
||||
pivot.addListener(notifyListeners);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a pivot point.
|
||||
void add(Pivot pivot) {
|
||||
value.add(pivot);
|
||||
pivot.addListener(notifyListeners);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Remove a pivot point.
|
||||
void remove(Pivot pivot) {
|
||||
value.remove(pivot);
|
||||
pivot.removeListener(notifyListeners);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Insert a pivot point.
|
||||
void insert(int index, Pivot pivot) {
|
||||
value.insert(index, pivot);
|
||||
pivot.addListener(notifyListeners);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Remove a pivot point by its index.
|
||||
void removeAt(int index) {
|
||||
value.removeAt(index).removeListener(notifyListeners);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class DashedPathProperties {
|
||||
double extractedPathLength;
|
||||
Path path;
|
||||
|
||||
final double _dashLength;
|
||||
double _remainingDashLength;
|
||||
double _remainingDashGapLength;
|
||||
bool _previousWasDash = false;
|
||||
|
||||
DashedPathProperties({
|
||||
required this.path,
|
||||
required double dashLength,
|
||||
required double dashGapLength,
|
||||
}) :
|
||||
_dashLength = dashLength,
|
||||
_remainingDashLength = dashLength,
|
||||
_remainingDashGapLength = dashGapLength,
|
||||
_previousWasDash = false,
|
||||
extractedPathLength = 0.0;
|
||||
|
||||
bool get addDashNext {
|
||||
if (_remainingDashLength != _dashLength || !_previousWasDash) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void addDash(ui.PathMetric metric, double dashLength) {
|
||||
// Calculate lengths (actual + available)
|
||||
final end = _calculateLength(metric, _remainingDashLength);
|
||||
final availableEnd = _calculateLength(metric, dashLength);
|
||||
// Add path
|
||||
final pathSegment = metric.extractPath(extractedPathLength, end);
|
||||
path.addPath(pathSegment, Offset.zero);
|
||||
// Update
|
||||
final delta = _remainingDashLength - (end - extractedPathLength);
|
||||
_remainingDashLength = _updateRemainingLength(
|
||||
delta: delta,
|
||||
end: end,
|
||||
availableEnd: availableEnd,
|
||||
initialLength: dashLength,
|
||||
);
|
||||
extractedPathLength = end;
|
||||
_previousWasDash = true;
|
||||
}
|
||||
|
||||
void addDashGap(ui.PathMetric metric, double dashGapLength) {
|
||||
// Calculate lengths (actual + available)
|
||||
final end = _calculateLength(metric, _remainingDashGapLength);
|
||||
final availableEnd = _calculateLength(metric, dashGapLength);
|
||||
// Move path's end point
|
||||
ui.Tangent tangent = metric.getTangentForOffset(end)!;
|
||||
path.moveTo(tangent.position.dx, tangent.position.dy);
|
||||
// Update
|
||||
final delta = end - extractedPathLength;
|
||||
_remainingDashGapLength = _updateRemainingLength(
|
||||
delta: delta,
|
||||
end: end,
|
||||
availableEnd: availableEnd,
|
||||
initialLength: dashGapLength,
|
||||
);
|
||||
extractedPathLength = end;
|
||||
_previousWasDash = false;
|
||||
}
|
||||
|
||||
double _calculateLength(ui.PathMetric metric, double addedLength) {
|
||||
return math.min(extractedPathLength + addedLength, metric.length);
|
||||
}
|
||||
|
||||
double _updateRemainingLength({
|
||||
required double delta,
|
||||
required double end,
|
||||
required double availableEnd,
|
||||
required double initialLength,
|
||||
}) {
|
||||
return (delta > 0 && availableEnd == end) ? delta : initialLength;
|
||||
}
|
||||
}
|
||||
296
library/flutter_flow_chart/lib/src/ui/element_handlers.dart
Executable file
296
library/flutter_flow_chart/lib/src/ui/element_handlers.dart
Executable file
@@ -0,0 +1,296 @@
|
||||
import 'package:flutter/material.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/ui/draw_arrow.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/handler_widget.dart';
|
||||
|
||||
/// Draw handlers over the element
|
||||
class ElementHandlers extends StatelessWidget {
|
||||
///
|
||||
const ElementHandlers({
|
||||
required this.dashboard,
|
||||
required this.element,
|
||||
required this.handlerSize,
|
||||
required this.child,
|
||||
required this.onHandlerPressed,
|
||||
required this.onHandlerSecondaryTapped,
|
||||
required this.onHandlerLongPressed,
|
||||
required this.onHandlerSecondaryLongTapped,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final Dashboard dashboard;
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
///
|
||||
final Widget child;
|
||||
|
||||
///
|
||||
final double handlerSize;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerPressed;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerLongPressed;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryTapped;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryLongTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: element.size.width + handlerSize,
|
||||
height: element.size.height + handlerSize,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
child,
|
||||
for (int i = 0; i < element.handlers.length; i++)
|
||||
_ElementHandler(
|
||||
element: element,
|
||||
handler: element.handlers[i],
|
||||
dashboard: dashboard,
|
||||
handlerSize: handlerSize,
|
||||
onHandlerPressed: onHandlerPressed,
|
||||
onHandlerSecondaryTapped: onHandlerSecondaryTapped,
|
||||
onHandlerLongPressed: onHandlerLongPressed,
|
||||
onHandlerSecondaryLongTapped: onHandlerSecondaryLongTapped,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ElementHandler extends StatelessWidget {
|
||||
const _ElementHandler({
|
||||
required this.element,
|
||||
required this.handler,
|
||||
required this.dashboard,
|
||||
required this.handlerSize,
|
||||
required this.onHandlerPressed,
|
||||
required this.onHandlerSecondaryTapped,
|
||||
required this.onHandlerLongPressed,
|
||||
required this.onHandlerSecondaryLongTapped,
|
||||
});
|
||||
final FlowElement element;
|
||||
final Handler handler;
|
||||
final Dashboard dashboard;
|
||||
final double handlerSize;
|
||||
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerPressed;
|
||||
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryTapped;
|
||||
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerLongPressed;
|
||||
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryLongTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var isDragging = false;
|
||||
|
||||
Alignment alignment;
|
||||
switch (handler) {
|
||||
case Handler.topCenter:
|
||||
alignment = Alignment.topCenter;
|
||||
case Handler.bottomCenter:
|
||||
alignment = Alignment.bottomCenter;
|
||||
case Handler.leftCenter:
|
||||
alignment = Alignment.centerLeft;
|
||||
case Handler.rightCenter:
|
||||
alignment = Alignment.centerRight;
|
||||
}
|
||||
|
||||
var tapDown = Offset.zero;
|
||||
var secondaryTapDown = Offset.zero;
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: DragTarget<Map<dynamic, dynamic>>(
|
||||
onWillAcceptWithDetails: (details) {
|
||||
DrawingArrow.instance.fromID = element.id;
|
||||
DrawingArrow.instance.setParams(
|
||||
DrawingArrow.instance.params.copyWith(
|
||||
endArrowPosition: alignment,
|
||||
style: dashboard.defaultArrowStyle,
|
||||
direction: dashboard.defaultArrowDirection,
|
||||
thickness: dashboard.defaultStroke,
|
||||
dashSpace: dashboard.defaultDashSpace,
|
||||
dashWidth: dashboard.defaultDashWidth,
|
||||
color: dashboard.defaultColor,
|
||||
backwardWidth: dashboard.defaultBackWidth,
|
||||
forwardWidth: dashboard.defaultForwardWidth,
|
||||
),
|
||||
);
|
||||
return element != details.data['srcElement'] as FlowElement;
|
||||
},
|
||||
onAcceptWithDetails: (details) {
|
||||
dashboard.addNextById(
|
||||
details.data['srcElement'] as FlowElement,
|
||||
element.id,
|
||||
DrawingArrow.instance.params.copyWith(
|
||||
endArrowPosition: alignment,
|
||||
),
|
||||
);
|
||||
},
|
||||
onLeave: (data) {
|
||||
DrawingArrow.instance.setParams(
|
||||
DrawingArrow.instance.params.copyWith(
|
||||
endArrowPosition: Alignment.center,
|
||||
style: dashboard.defaultArrowStyle,
|
||||
direction: dashboard.defaultArrowDirection,
|
||||
thickness: dashboard.defaultStroke,
|
||||
dashSpace: dashboard.defaultDashSpace,
|
||||
dashWidth: dashboard.defaultDashWidth,
|
||||
color: dashboard.defaultColor,
|
||||
backwardWidth: dashboard.defaultBackWidth,
|
||||
forwardWidth: dashboard.defaultForwardWidth,
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
return Draggable(
|
||||
feedback: const SizedBox.shrink(),
|
||||
feedbackOffset: dashboard.handlerFeedbackOffset,
|
||||
childWhenDragging: HandlerWidget(
|
||||
width: handlerSize,
|
||||
height: handlerSize,
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
data: {
|
||||
'srcElement': element,
|
||||
'alignment': alignment,
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTapDown: (details) =>
|
||||
tapDown = details.globalPosition - dashboard.position,
|
||||
onSecondaryTapDown: (details) => secondaryTapDown =
|
||||
details.globalPosition - dashboard.position,
|
||||
onTap: () {
|
||||
onHandlerPressed?.call(
|
||||
context,
|
||||
tapDown,
|
||||
handler,
|
||||
element,
|
||||
);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
onHandlerSecondaryTapped?.call(
|
||||
context,
|
||||
secondaryTapDown,
|
||||
handler,
|
||||
element,
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
onHandlerLongPressed?.call(
|
||||
context,
|
||||
tapDown,
|
||||
handler,
|
||||
element,
|
||||
);
|
||||
},
|
||||
onSecondaryLongPress: () {
|
||||
onHandlerSecondaryLongTapped?.call(
|
||||
context,
|
||||
secondaryTapDown,
|
||||
handler,
|
||||
element,
|
||||
);
|
||||
},
|
||||
child: HandlerWidget(
|
||||
width: handlerSize,
|
||||
height: handlerSize,
|
||||
),
|
||||
),
|
||||
onDragUpdate: (details) {
|
||||
if (!isDragging && DrawingArrow.instance.from == Offset.zero) {
|
||||
DrawingArrow.instance.fromID = element.id;
|
||||
DrawingArrow.instance.params = ArrowParams(
|
||||
startArrowPosition: alignment,
|
||||
endArrowPosition: Alignment.center,
|
||||
style: dashboard.defaultArrowStyle,
|
||||
direction: dashboard.defaultArrowDirection,
|
||||
thickness: dashboard.defaultStroke,
|
||||
dashSpace: dashboard.defaultDashSpace,
|
||||
dashWidth: dashboard.defaultDashWidth,
|
||||
color: dashboard.defaultColor,
|
||||
backwardWidth: dashboard.defaultBackWidth,
|
||||
forwardWidth: dashboard.defaultForwardWidth,
|
||||
);
|
||||
DrawingArrow.instance.from =
|
||||
details.globalPosition - Offset(-10, 45);
|
||||
isDragging = true;
|
||||
}
|
||||
DrawingArrow.instance.to = details.globalPosition - Offset(-10, 50);
|
||||
DrawingArrow.instance.setParams(
|
||||
DrawingArrow.instance.params.copyWith(
|
||||
endArrowPosition: Alignment.center,
|
||||
style: dashboard.defaultArrowStyle,
|
||||
direction: dashboard.defaultArrowDirection,
|
||||
thickness: dashboard.defaultStroke,
|
||||
dashSpace: dashboard.defaultDashSpace,
|
||||
dashWidth: dashboard.defaultDashWidth,
|
||||
color: dashboard.defaultColor,
|
||||
backwardWidth: dashboard.defaultBackWidth,
|
||||
forwardWidth: dashboard.defaultForwardWidth,
|
||||
),
|
||||
);
|
||||
},
|
||||
onDragEnd: (details) {
|
||||
DrawingArrow.instance.reset();
|
||||
isDragging = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
262
library/flutter_flow_chart/lib/src/ui/element_widget.dart
Executable file
262
library/flutter_flow_chart/lib/src/ui/element_widget.dart
Executable file
@@ -0,0 +1,262 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/diamond_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/hexagon_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/oval_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/parallelogram_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/rectangle_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/objects/storage_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/draw_arrow.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/element_handlers.dart';
|
||||
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 {
|
||||
///
|
||||
ElementWidget({
|
||||
required this.dashboard,
|
||||
required this.element,
|
||||
super.key,
|
||||
this.onElementPressed,
|
||||
this.onElementSecondaryTapped,
|
||||
this.onElementLongPressed,
|
||||
this.onElementSecondaryLongTapped,
|
||||
this.onHandlerPressed,
|
||||
this.onHandlerSecondaryTapped,
|
||||
this.onHandlerLongPressed,
|
||||
this.onHandlerSecondaryLongTapped,
|
||||
});
|
||||
|
||||
///
|
||||
final Dashboard dashboard;
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
///
|
||||
bool isHovered = false;
|
||||
|
||||
///
|
||||
final void Function(BuildContext context, Offset position)? onElementPressed;
|
||||
|
||||
///
|
||||
final void Function(BuildContext context, Offset position)?
|
||||
onElementSecondaryTapped;
|
||||
|
||||
///
|
||||
final void Function(BuildContext context, Offset position)?
|
||||
onElementLongPressed;
|
||||
|
||||
///
|
||||
final void Function(BuildContext context, Offset position)?
|
||||
onElementSecondaryLongTapped;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerPressed;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryTapped;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerLongPressed;
|
||||
|
||||
///
|
||||
final void Function(
|
||||
BuildContext context,
|
||||
Offset position,
|
||||
Handler handler,
|
||||
FlowElement element,
|
||||
)? onHandlerSecondaryLongTapped;
|
||||
|
||||
@override
|
||||
State<ElementWidget> createState() => ElementWidgetState();
|
||||
}
|
||||
|
||||
class ElementWidgetState extends State<ElementWidget> {
|
||||
// local widget touch position when start dragging
|
||||
Offset delta = Offset.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.element.addListener(_elementChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.element.removeListener(_elementChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _elementChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget element;
|
||||
|
||||
switch (widget.element.kind) {
|
||||
case ElementKind.diamond:
|
||||
element = DiamondWidget(element: widget.element);
|
||||
case ElementKind.storage:
|
||||
element = StorageWidget(element: widget.element);
|
||||
case ElementKind.oval:
|
||||
element = OvalWidget(element: widget.element);
|
||||
case ElementKind.parallelogram:
|
||||
element = ParallelogramWidget(element: widget.element);
|
||||
case ElementKind.hexagon:
|
||||
element = HexagonWidget(element: widget.element);
|
||||
case ElementKind.rectangle:
|
||||
element = RectangleWidget(element: widget.element);
|
||||
case ElementKind.widget:
|
||||
if (widget.element.widget == null) { element = RectangleWidget(element: widget.element);
|
||||
} else { element = AnyWidget(element: widget.element); }
|
||||
}
|
||||
|
||||
var tapLocation = Offset.zero;
|
||||
var secondaryTapDownPos = Offset.zero;
|
||||
|
||||
|
||||
Widget w = GestureDetector(
|
||||
onTapDown: (details) => tapLocation = details.globalPosition,
|
||||
onSecondaryTapDown: (details) =>
|
||||
secondaryTapDownPos = details.globalPosition,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
widget.element.isSelected = !widget.element.isSelected;
|
||||
for (var sel in widget.dashboard.arrows) { sel.isSelected = false; }
|
||||
widget.dashboard.selectedMenuKey.currentState?. setState(() { });
|
||||
Future.delayed(Duration(seconds: 1), () { DrawingArrow.instance.notifyListeners(); });
|
||||
});
|
||||
widget.onElementPressed?.call(context, tapLocation);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
widget.onElementSecondaryTapped?.call(context, secondaryTapDownPos);
|
||||
},
|
||||
onLongPress: () {
|
||||
widget.onElementLongPressed?.call(context, tapLocation);
|
||||
},
|
||||
onSecondaryLongPress: () {
|
||||
widget.onElementSecondaryLongTapped
|
||||
?.call(context, secondaryTapDownPos);
|
||||
},
|
||||
child: Listener(
|
||||
onPointerDown: (event) {
|
||||
delta = event.localPosition;
|
||||
},
|
||||
child: Draggable<FlowElement>(
|
||||
data: widget.element,
|
||||
dragAnchorStrategy: childDragAnchorStrategy,
|
||||
childWhenDragging: const SizedBox.shrink(),
|
||||
feedback: Material(
|
||||
color: Colors.transparent,
|
||||
child: Padding( padding: EdgeInsets.all(6),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border.all(
|
||||
color: Colors.red, width: 2)),
|
||||
width: widget.element.size.width - 12,
|
||||
height: widget.element.size.height - 12, child: element)
|
||||
),
|
||||
),
|
||||
child: ElementHandlers(
|
||||
dashboard: widget.dashboard,
|
||||
element: widget.element,
|
||||
handlerSize: widget.element.handlerSize,
|
||||
onHandlerPressed: widget.onHandlerPressed,
|
||||
onHandlerSecondaryTapped: widget.onHandlerSecondaryTapped,
|
||||
onHandlerLongPressed: widget.onHandlerLongPressed,
|
||||
onHandlerSecondaryLongTapped: widget.onHandlerSecondaryLongTapped,
|
||||
child: Container(
|
||||
margin: EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: widget.element.isSelected ? Colors.red : Colors.grey.shade300,
|
||||
width: widget.element.isSelected ? 2 : 1),
|
||||
),
|
||||
child: InkWell( mouseCursor: SystemMouseCursors.grab, child: element )
|
||||
)
|
||||
),
|
||||
onDragStarted: () {
|
||||
widget.element.isSelected = true;
|
||||
widget.dashboard.selectedMenuKey.currentState?. setState(() { });
|
||||
},
|
||||
onDragUpdate: (details) {
|
||||
var diff = Offset(widget.element.position.dx, widget.element.position.dy);
|
||||
widget.element.isSelected = true;
|
||||
widget.element.changePosition(
|
||||
details.globalPosition - delta - Offset(0, 50),
|
||||
);
|
||||
diff = widget.element.position - diff;
|
||||
if (widget.element.isSelected) {
|
||||
for (var sel in widget.dashboard.elementSelected) {
|
||||
if (widget.element == sel) { continue; }
|
||||
sel.changePosition(sel.position + diff);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDragEnd: (details) {
|
||||
var diff = Offset(widget.element.position.dx, widget.element.position.dy);
|
||||
widget.element.changePosition(details.offset - Offset(0, 50));
|
||||
diff = widget.element.position - diff;
|
||||
if (widget.element.isSelected) {
|
||||
for (var sel in widget.dashboard.elementSelected) {
|
||||
if (widget.element == sel) { continue; }
|
||||
sel.changePosition(sel.position + diff);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
w = ResizeWidget(
|
||||
comp: this,
|
||||
element: widget.element,
|
||||
dashboard: widget.dashboard,
|
||||
handlerColor: widget.isHovered ? Color.fromRGBO(38, 166, 154, 1) : Colors.transparent,
|
||||
child: w );
|
||||
return Transform.translate(
|
||||
offset: widget.element.position,
|
||||
transformHitTests: true,
|
||||
child: MouseRegion(
|
||||
onEnter: (event) { setState(() { widget.isHovered = true; }); },
|
||||
onExit: (event) { setState(() { widget.isHovered = false; }); },
|
||||
child: Wrap( direction: Axis.vertical, children: [
|
||||
w,
|
||||
Container(
|
||||
constraints: BoxConstraints( minWidth: widget.element.size.width ),
|
||||
child: Row( mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: (!widget.isHovered ? [] : [
|
||||
IconButton(tooltip: "remove element", onPressed: () {
|
||||
widget.dashboard.removeElement(widget.element);
|
||||
}, icon: Icon(Icons.delete_outline)),
|
||||
IconButton(tooltip: "copy element", onPressed: () {
|
||||
FlowElement newElement = FlowElement(
|
||||
kind: widget.element.kind,
|
||||
position: widget.element.position + Offset(widget.element.size.width, widget.element.size.height),
|
||||
size: widget.element.size,
|
||||
widget: widget.element.widget,
|
||||
);
|
||||
widget.dashboard.addElement(newElement);
|
||||
}, icon: Icon(Icons.copy, size: 20)),
|
||||
]))
|
||||
)
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
201
library/flutter_flow_chart/lib/src/ui/grid_background.dart
Executable file
201
library/flutter_flow_chart/lib/src/ui/grid_background.dart
Executable file
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Defines grid parameters.
|
||||
class GridBackgroundParams extends ChangeNotifier {
|
||||
/// [gridSquare] is the raw size of the grid square when scale is 1
|
||||
GridBackgroundParams({
|
||||
double gridSquare = 20.0,
|
||||
this.gridThickness = 0.7,
|
||||
this.secondarySquareStep = 5,
|
||||
this.backgroundColor = Colors.white,
|
||||
this.gridColor = Colors.black12,
|
||||
void Function(double scale)? onScaleUpdate,
|
||||
}) : rawGridSquareSize = gridSquare {
|
||||
if (onScaleUpdate != null) {
|
||||
_onScaleUpdateListeners.add(onScaleUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
factory GridBackgroundParams.fromMap(Map<String, dynamic> map) {
|
||||
final params = GridBackgroundParams(
|
||||
gridSquare: map['gridSquare'] as double? ?? 20.0,
|
||||
gridThickness: map['gridThickness'] as double? ?? 0.7,
|
||||
secondarySquareStep: map['secondarySquareStep'] as int? ?? 5,
|
||||
backgroundColor: Color(map['backgroundColor'] as int? ?? 0xFFFFFFFF),
|
||||
gridColor: Color(map['gridColor'] as int? ?? 0xFFFFFFFF),
|
||||
)
|
||||
..scale = map['scale'] as double? ?? 1.0
|
||||
.._offset = Offset(
|
||||
map['offset.dx'] as double? ?? 0.0,
|
||||
map['offset.dy'] as double? ?? 0.0,
|
||||
);
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/// Unscaled size of the grid square
|
||||
/// i.e. the size of the square when scale is 1
|
||||
final double rawGridSquareSize;
|
||||
|
||||
/// Thickness of lines.
|
||||
final double gridThickness;
|
||||
|
||||
/// How many vertical or horizontal lines to draw the marked lines.
|
||||
final int secondarySquareStep;
|
||||
|
||||
/// Grid background color.
|
||||
final Color backgroundColor;
|
||||
|
||||
/// Grid lines color.
|
||||
final Color gridColor;
|
||||
|
||||
/// offset to move the grid
|
||||
Offset _offset = Offset.zero;
|
||||
|
||||
/// Scale of the grid.
|
||||
double scale = 1;
|
||||
|
||||
/// Add listener for scaling
|
||||
void addOnScaleUpdateListener(void Function(double scale) listener) {
|
||||
_onScaleUpdateListeners.add(listener);
|
||||
}
|
||||
|
||||
/// Remove listener for scaling
|
||||
void removeOnScaleUpdateListener(void Function(double scale) listener) {
|
||||
_onScaleUpdateListeners.remove(listener);
|
||||
}
|
||||
|
||||
final List<void Function(double scale)> _onScaleUpdateListeners = [];
|
||||
|
||||
///
|
||||
set offset(Offset delta) {
|
||||
_offset += delta;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///
|
||||
void setScale(double factor, Offset focalPoint) {
|
||||
_offset = Offset(
|
||||
focalPoint.dx * (1 - factor),
|
||||
focalPoint.dy * (1 - factor),
|
||||
);
|
||||
scale = factor;
|
||||
|
||||
for (final listener in _onScaleUpdateListeners) {
|
||||
listener(scale);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// size of the grid square with scale applied
|
||||
double get gridSquare => rawGridSquareSize * scale;
|
||||
|
||||
///
|
||||
Offset get offset => _offset;
|
||||
|
||||
///
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'offset.dx': _offset.dx,
|
||||
'offset.dy': _offset.dy,
|
||||
'scale': scale,
|
||||
'gridSquare': rawGridSquareSize,
|
||||
'gridThickness': gridThickness,
|
||||
'secondarySquareStep': secondarySquareStep,
|
||||
'backgroundColor': backgroundColor.value,
|
||||
'gridColor': gridColor.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses a CustomPainter to draw a grid with the given parameters
|
||||
class GridBackground extends StatelessWidget {
|
||||
GridBackground({
|
||||
super.key,
|
||||
GridBackgroundParams? params,
|
||||
}) : params = params ?? GridBackgroundParams();
|
||||
final GridBackgroundParams params;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: params,
|
||||
builder: (context, _) {
|
||||
return RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: _GridBackgroundPainter(
|
||||
params: params,
|
||||
dx: params.offset.dx,
|
||||
dy: params.offset.dy,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridBackgroundPainter extends CustomPainter {
|
||||
_GridBackgroundPainter({
|
||||
required this.params,
|
||||
required this.dx,
|
||||
required this.dy,
|
||||
});
|
||||
final GridBackgroundParams params;
|
||||
final double dx;
|
||||
final double dy;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint paint = Paint();
|
||||
|
||||
// Background
|
||||
paint.color = params.backgroundColor;
|
||||
canvas.drawRect(
|
||||
Rect.fromPoints(const Offset(0, 0), Offset(size.width, size.height)),
|
||||
paint,
|
||||
);
|
||||
|
||||
// grid
|
||||
paint.color = params.gridColor;
|
||||
paint.style = PaintingStyle.stroke;
|
||||
|
||||
// Calculate the starting points for x and y
|
||||
double startX = dx % (params.gridSquare * params.secondarySquareStep);
|
||||
double startY = dy % (params.gridSquare * params.secondarySquareStep);
|
||||
|
||||
// Calculate the number of lines to draw outside the visible area
|
||||
int extraLines = 2;
|
||||
|
||||
// Draw vertical lines
|
||||
for (double x = startX - extraLines * params.gridSquare;
|
||||
x < size.width + extraLines * params.gridSquare;
|
||||
x += params.gridSquare) {
|
||||
paint.strokeWidth = ((x - startX) / params.gridSquare).round() %
|
||||
params.secondarySquareStep ==
|
||||
0
|
||||
? params.gridThickness * 2.0
|
||||
: params.gridThickness;
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
|
||||
}
|
||||
|
||||
// Draw horizontal lines
|
||||
for (double y = startY - extraLines * params.gridSquare;
|
||||
y < size.height + extraLines * params.gridSquare;
|
||||
y += params.gridSquare) {
|
||||
paint.strokeWidth = ((y - startY) / params.gridSquare).round() %
|
||||
params.secondarySquareStep ==
|
||||
0
|
||||
? params.gridThickness * 2.0
|
||||
: params.gridThickness;
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_GridBackgroundPainter oldDelegate) {
|
||||
debugPrint('shouldRepaint ${oldDelegate.dx} $dx ${oldDelegate.dy} $dy');
|
||||
return oldDelegate.dx != dx || oldDelegate.dy != dy;
|
||||
}
|
||||
}
|
||||
55
library/flutter_flow_chart/lib/src/ui/handler_widget.dart
Executable file
55
library/flutter_flow_chart/lib/src/ui/handler_widget.dart
Executable file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// The arrow tip.
|
||||
class HandlerWidget extends StatelessWidget {
|
||||
///
|
||||
HandlerWidget({
|
||||
required this.width,
|
||||
required this.height,
|
||||
super.key,
|
||||
this.backgroundColor = Colors.white,
|
||||
this.borderColor = const Color.fromRGBO(38, 166, 154, 1),
|
||||
this.icon,
|
||||
this.isRadius = false,
|
||||
});
|
||||
|
||||
///
|
||||
final double width;
|
||||
|
||||
///
|
||||
final double height;
|
||||
|
||||
///
|
||||
final Color backgroundColor;
|
||||
|
||||
///
|
||||
final Color borderColor;
|
||||
|
||||
///
|
||||
final Widget? icon;
|
||||
|
||||
bool isRadius = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(isRadius ? 0 : width),
|
||||
),
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: borderColor,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: FittedBox(child: icon),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
102
library/flutter_flow_chart/lib/src/ui/resize_widget.dart
Normal file
102
library/flutter_flow_chart/lib/src/ui/resize_widget.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/element_widget.dart';
|
||||
import 'package:flutter_flow_chart/src/ui/handler_widget.dart';
|
||||
|
||||
/// The widget to press and drag to resize the element
|
||||
class ResizeWidget extends StatefulWidget {
|
||||
///
|
||||
ResizeWidget({
|
||||
required this.comp,
|
||||
required this.element,
|
||||
required this.dashboard,
|
||||
required this.child,
|
||||
this.handlerColor =const Color.fromRGBO(38, 166, 154, 1),
|
||||
this.additionnalHeight = 0,
|
||||
|
||||
super.key,
|
||||
});
|
||||
|
||||
Color handlerColor = Color.fromRGBO(38, 166, 154, 1);
|
||||
final ElementWidgetState comp;
|
||||
///
|
||||
final Dashboard dashboard;
|
||||
|
||||
///
|
||||
final FlowElement element;
|
||||
|
||||
///
|
||||
final Widget child;
|
||||
|
||||
///
|
||||
final double additionnalHeight;
|
||||
|
||||
@override
|
||||
State<ResizeWidget> createState() => _ResizeWidgetState();
|
||||
}
|
||||
|
||||
class _ResizeWidgetState extends State<ResizeWidget> {
|
||||
late Size elementStartSize;
|
||||
late Offset elementStartPosition;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: widget.element.size.width,
|
||||
height: widget.element.size.height + widget.additionnalHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
widget.child,
|
||||
_handler(Alignment.topLeft),
|
||||
_handler(Alignment.topRight),
|
||||
_handler(Alignment.bottomLeft),
|
||||
_handler(Alignment.bottomRight),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _handler(Alignment alignment) {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
elementStartSize = widget.element.size;
|
||||
},
|
||||
onPointerMove: (event) {
|
||||
if (alignment == Alignment.topLeft) {
|
||||
elementStartSize += -event.localDelta;
|
||||
widget.element.changePosition(
|
||||
widget.element.position + event.localDelta,
|
||||
);
|
||||
} else if (alignment == Alignment.topRight) {
|
||||
elementStartSize += -Offset(-event.localDelta.dx, event.localDelta.dy);
|
||||
var l = Offset(0, event.localDelta.dy);
|
||||
widget.element.changePosition(
|
||||
widget.element.position + l,
|
||||
);
|
||||
} else if (alignment == Alignment.bottomLeft) {
|
||||
elementStartSize += -Offset(event.localDelta.dx, -event.localDelta.dy);
|
||||
var l = Offset(event.localDelta.dx, 0);
|
||||
widget.element.changePosition(
|
||||
widget.element.position + l,
|
||||
);
|
||||
} else { elementStartSize += event.localDelta; }
|
||||
widget.element.changeSize(elementStartSize);
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
widget.dashboard.setElementResizable(widget.element, false);
|
||||
},
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: InkWell(
|
||||
mouseCursor: SystemMouseCursors.resizeColumn,
|
||||
child: HandlerWidget(
|
||||
width: 15,
|
||||
height: 15,
|
||||
borderColor: Colors.transparent,
|
||||
backgroundColor: widget.handlerColor,
|
||||
isRadius: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
131
library/flutter_flow_chart/lib/src/ui/segment_handler.dart
Executable file
131
library/flutter_flow_chart/lib/src/ui/segment_handler.dart
Executable file
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_flow_chart/flutter_flow_chart.dart';
|
||||
|
||||
/// Widget that use the element properties to display it on the dashboard scene.
|
||||
class SegmentHandler extends StatefulWidget {
|
||||
///
|
||||
const SegmentHandler({
|
||||
required this.pivot,
|
||||
required this.dashboard,
|
||||
super.key,
|
||||
this.onPivotPressed,
|
||||
this.onPivotSecondaryPressed,
|
||||
});
|
||||
|
||||
///
|
||||
final Pivot pivot;
|
||||
|
||||
///
|
||||
final Dashboard dashboard;
|
||||
|
||||
///
|
||||
final void Function(BuildContext context, Pivot position)? onPivotPressed;
|
||||
|
||||
///
|
||||
final void Function(BuildContext context, Pivot position)?
|
||||
onPivotSecondaryPressed;
|
||||
|
||||
@override
|
||||
State<SegmentHandler> createState() => _SegmentHandlerState();
|
||||
}
|
||||
|
||||
class _SegmentHandlerState extends State<SegmentHandler> {
|
||||
Offset delta = Offset.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.pivot.addListener(_update);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.pivot.removeListener(_update);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.translate(
|
||||
offset:
|
||||
widget.pivot.pivot - const Offset(5, 5) * widget.dashboard.zoomFactor,
|
||||
child: Listener(
|
||||
onPointerDown: (evt) {
|
||||
delta = evt.delta;
|
||||
},
|
||||
child: Draggable(
|
||||
feedback: const SizedBox(),
|
||||
onDragUpdate: (details) {
|
||||
widget.pivot.pivot =
|
||||
details.globalPosition - delta;
|
||||
},
|
||||
onDragEnd: (details) {
|
||||
widget.pivot.pivot = details.offset;
|
||||
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.onPivotPressed?.call(context, widget.pivot);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
widget.onPivotSecondaryPressed?.call(context, widget.pivot);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
radius: widget.dashboard.zoomFactor * 5,
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _update() {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
class Pivot extends ChangeNotifier {
|
||||
///
|
||||
Pivot(Offset pivot) : _pivot = pivot;
|
||||
|
||||
///
|
||||
Pivot.fromMap(Map<String, dynamic> map)
|
||||
: _pivot = Offset(
|
||||
map['pivot.dx'] as double,
|
||||
map['pivot.dy'] as double,
|
||||
);
|
||||
Offset _pivot;
|
||||
|
||||
///
|
||||
Offset get pivot => _pivot;
|
||||
|
||||
///
|
||||
set pivot(Offset value) {
|
||||
_pivot = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'pivot.dx': _pivot.dx,
|
||||
'pivot.dy': _pivot.dy,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
void setScale(double scale, Offset focalPoint, double factor) {
|
||||
pivot = ((pivot - focalPoint) / scale) * factor + focalPoint;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is Pivot && other._pivot == _pivot;
|
||||
|
||||
@override
|
||||
int get hashCode => _pivot.hashCode;
|
||||
}
|
||||
Reference in New Issue
Block a user