From e21c6bf9c34b9bece12c4f9bb9dd560b4136f80a Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 9 Jul 2022 19:59:30 -0700 Subject: [PATCH] Drag and Drop (#62) * Flutter 3.0.3 * Container click without ink passes coordinates in event * Drag and Drop implementation --- .appveyor.yml | 4 +- ci/install_flutter.ps1 | 2 +- client/lib/controls/container.dart | 5 +- client/lib/controls/create_control.dart | 14 ++++ client/lib/controls/drag_target.dart | 93 ++++++++++++++++++++++++ client/lib/controls/draggable.dart | 72 +++++++++++++++++++ client/lib/models/control_type.dart | 2 + sdk/python/flet/__init__.py | 2 + sdk/python/flet/drag_target.py | 94 +++++++++++++++++++++++++ sdk/python/flet/draggable.py | 93 ++++++++++++++++++++++++ 10 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 client/lib/controls/drag_target.dart create mode 100644 client/lib/controls/draggable.dart create mode 100644 sdk/python/flet/drag_target.py create mode 100644 sdk/python/flet/draggable.py diff --git a/.appveyor.yml b/.appveyor.yml index 27ec65939..64f2b042f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -135,7 +135,7 @@ for: install: - brew install cocoapods - - curl https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.0.2-stable.zip -o flutter_macos_stable.zip + - curl https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.0.3-stable.zip -o flutter_macos_stable.zip - unzip -qq flutter_macos_stable.zip - export PATH="$PATH:`pwd`/flutter/bin" - flutter --version @@ -204,7 +204,7 @@ for: install: - export LANG=en_US.UTF-8 - - curl https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.0.2-stable.zip -o flutter_macos_stable.zip + - curl https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.0.3-stable.zip -o flutter_macos_stable.zip - unzip -qq flutter_macos_stable.zip - export PATH="$PATH:`pwd`/flutter/bin" - flutter --version diff --git a/ci/install_flutter.ps1 b/ci/install_flutter.ps1 index 4b0ec097b..f3b21d7ac 100644 --- a/ci/install_flutter.ps1 +++ b/ci/install_flutter.ps1 @@ -1,7 +1,7 @@ $distPath = "$env:TEMP\flutter_windows_stable.zip" Write-Host "Downloading Flutter SDK..." -(New-Object Net.WebClient).DownloadFile("https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.0.2-stable.zip", $distPath) +(New-Object Net.WebClient).DownloadFile("https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.0.3-stable.zip", $distPath) Write-Host "Unpacking Flutter SDK..." 7z x $distPath -o"$env:SystemDrive\" | Out-Null diff --git a/client/lib/controls/container.dart b/client/lib/controls/container.dart index 9b1964f2c..728c07bd7 100644 --- a/client/lib/controls/container.dart +++ b/client/lib/controls/container.dart @@ -81,12 +81,13 @@ class ContainerControl extends StatelessWidget { cursor: SystemMouseCursors.click, child: GestureDetector( child: container, - onTap: () { + onTapDown: (details) { debugPrint("Container ${control.id} clicked!"); ws.pageEventFromWeb( eventTarget: control.id, eventName: "click", - eventData: control.attrs["data"] ?? ""); + eventData: control.attrString("data", "")! + + "${details.localPosition.dx}:${details.localPosition.dy} ${details.globalPosition.dx}:${details.globalPosition.dy}"); }, ), ); diff --git a/client/lib/controls/create_control.dart b/client/lib/controls/create_control.dart index ee924fda4..b4e1df62e 100644 --- a/client/lib/controls/create_control.dart +++ b/client/lib/controls/create_control.dart @@ -14,6 +14,8 @@ import 'clipboard.dart'; import 'column.dart'; import 'container.dart'; import 'divider.dart'; +import 'drag_target.dart'; +import 'draggable.dart'; import 'dropdown.dart'; import 'elevated_button.dart'; import 'floating_action_button.dart'; @@ -145,6 +147,18 @@ Widget createControl(Control? parent, String id, bool parentDisabled) { control: controlView.control, children: controlView.children, parentDisabled: parentDisabled); + case ControlType.draggable: + return DraggableControl( + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); + case ControlType.dragTarget: + return DragTargetControl( + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); case ControlType.card: return CardControl( parent: parent, diff --git a/client/lib/controls/drag_target.dart b/client/lib/controls/drag_target.dart new file mode 100644 index 000000000..550b8ce33 --- /dev/null +++ b/client/lib/controls/drag_target.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:flet_view/controls/error.dart'; +import 'package:flutter/material.dart'; + +import '../models/control.dart'; +import '../web_socket_client.dart'; +import 'create_control.dart'; + +class DragTargetControl extends StatelessWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + + const DragTargetControl( + {Key? key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled}) + : super(key: key); + + @override + Widget build(BuildContext context) { + debugPrint("DragTarget build: ${control.id}"); + + var group = control.attrString("group", ""); + var contentCtrls = + children.where((c) => c.name == "content" && c.isVisible); + bool disabled = control.isDisabled || parentDisabled; + + Widget? child = contentCtrls.isNotEmpty + ? createControl(control, contentCtrls.first.id, disabled) + : null; + + if (child == null) { + return const ErrorControl("DragTarget should have content."); + } + + return DragTarget( + builder: ( + BuildContext context, + List accepted, + List rejected, + ) { + debugPrint( + "DragTarget.builder ${control.id}: accepted=${accepted.length}, rejected=${rejected.length}"); + return child; + }, + onWillAccept: (data) { + debugPrint("DragTarget.onAccept ${control.id}: $data"); + String srcGroup = ""; + if (data != null) { + var jd = json.decode(data); + srcGroup = jd["group"] as String; + } + var groupsEqual = srcGroup == group; + ws.pageEventFromWeb( + eventTarget: control.id, + eventName: "will_accept", + eventData: + control.attrString("data", "")! + groupsEqual.toString()); + return groupsEqual; + }, + onAccept: (data) { + debugPrint("DragTarget.onAccept ${control.id}: $data"); + var jd = json.decode(data); + var srcId = jd["id"] as String; + ws.pageEventFromWeb( + eventTarget: control.id, + eventName: "accept", + eventData: control.attrString("data", "")! + srcId); + }, + // onAcceptWithDetails: (details) { + // debugPrint( + // "onAcceptWithDetails: ${details.data} ${details.offset}"); + // }, + onLeave: (data) { + debugPrint("DragTarget.onLeave ${control.id}: $data"); + String srcId = ""; + if (data != null) { + var jd = json.decode(data); + srcId = jd["id"] as String; + } + ws.pageEventFromWeb( + eventTarget: control.id, + eventName: "leave", + eventData: control.attrString("data", "")! + srcId); + }, + ); + } +} diff --git a/client/lib/controls/draggable.dart b/client/lib/controls/draggable.dart new file mode 100644 index 000000000..e4d8d7c9d --- /dev/null +++ b/client/lib/controls/draggable.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; + +import 'package:flet_view/controls/error.dart'; +import 'package:flutter/material.dart'; + +import '../models/control.dart'; +import 'create_control.dart'; + +class DraggableControl extends StatelessWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + + const DraggableControl( + {Key? key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled}) + : super(key: key); + + @override + Widget build(BuildContext context) { + debugPrint("DragTarget build: ${control.id}"); + + var group = control.attrString("group", ""); + var contentCtrls = + children.where((c) => c.name == "content" && c.isVisible); + var contentWhenDraggingCtrls = + children.where((c) => c.name == "content_when_dragging" && c.isVisible); + var contentFeedbackCtrls = + children.where((c) => c.name == "content_feedback" && c.isVisible); + bool disabled = control.isDisabled || parentDisabled; + + Widget? child = contentCtrls.isNotEmpty + ? createControl(control, contentCtrls.first.id, disabled) + : null; + + Widget? childWhenDragging = contentWhenDraggingCtrls.isNotEmpty + ? createControl(control, contentWhenDraggingCtrls.first.id, disabled) + : null; + + Widget? childFeedback = contentFeedbackCtrls.isNotEmpty + ? createControl(control, contentFeedbackCtrls.first.id, disabled) + : null; + + if (child == null) { + return const ErrorControl("Draggable should have content."); + } + + var data = json.encode({"id": control.id, "group": group}); + + return Draggable( + data: data, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: child, + ), + childWhenDragging: childWhenDragging, + feedback: MouseRegion( + cursor: SystemMouseCursors.grabbing, + child: childFeedback ?? Opacity(opacity: 0.5, child: child), + ), + // dragAnchorStrategy: (d, context, offset) { + // debugPrint("dragAnchorStrategy: ${offset.dx}, ${offset.dy}"); + // return offset; + // } + //feedbackOffset: const Offset(-30, -30), + ); + } +} diff --git a/client/lib/models/control_type.dart b/client/lib/models/control_type.dart index d04e5fb8f..029e6f93d 100644 --- a/client/lib/models/control_type.dart +++ b/client/lib/models/control_type.dart @@ -9,6 +9,8 @@ enum ControlType { column, container, divider, + draggable, + dragTarget, dropdown, dropdownOption, elevatedButton, diff --git a/sdk/python/flet/__init__.py b/sdk/python/flet/__init__.py index 6a121099e..d6571513a 100644 --- a/sdk/python/flet/__init__.py +++ b/sdk/python/flet/__init__.py @@ -8,6 +8,8 @@ from flet.container import Container from flet.control import Control from flet.divider import Divider +from flet.drag_target import DragTarget +from flet.draggable import Draggable from flet.dropdown import Dropdown from flet.elevated_button import ElevatedButton from flet.filled_button import FilledButton diff --git a/sdk/python/flet/drag_target.py b/sdk/python/flet/drag_target.py new file mode 100644 index 000000000..95be8b4d9 --- /dev/null +++ b/sdk/python/flet/drag_target.py @@ -0,0 +1,94 @@ +from beartype import beartype + +from flet.control import Control +from flet.ref import Ref + + +class DragTarget(Control): + def __init__( + self, + ref: Ref = None, + disabled: bool = None, + visible: bool = None, + data: any = None, + # + # Specific + # + group: str = None, + content: Control = None, + on_will_accept=None, + on_accept=None, + on_leave=None, + ): + + Control.__init__( + self, + ref=ref, + disabled=disabled, + visible=visible, + data=data, + ) + + self.__content: Control = None + + self.group = group + self.content = content + self.on_will_accept = on_will_accept + self.on_accept = on_accept + self.on_leave = on_leave + + def _get_control_name(self): + return "dragtarget" + + def _get_children(self): + children = [] + if self.__content: + self.__content._set_attr_internal("n", "content") + children.append(self.__content) + return children + + # group + @property + def group(self): + return self._get_attr("group") + + @group.setter + @beartype + def group(self, value): + self._set_attr("group", value) + + # content + @property + def content(self): + return self.__content + + @content.setter + def content(self, value): + self.__content = value + + # on_will_accept + @property + def on_will_accept(self): + return self._get_event_handler("will_accept") + + @on_will_accept.setter + def on_will_accept(self, handler): + self._add_event_handler("will_accept", handler) + + # on_accept + @property + def on_accept(self): + return self._get_event_handler("accept") + + @on_accept.setter + def on_accept(self, handler): + self._add_event_handler("accept", handler) + + # on_leave + @property + def on_leave(self): + return self._get_event_handler("leave") + + @on_leave.setter + def on_leave(self, handler): + self._add_event_handler("leave", handler) diff --git a/sdk/python/flet/draggable.py b/sdk/python/flet/draggable.py new file mode 100644 index 000000000..cd64d5c5d --- /dev/null +++ b/sdk/python/flet/draggable.py @@ -0,0 +1,93 @@ +from typing import List, Optional + +from beartype import beartype + +from flet.control import Control +from flet.ref import Ref + + +class Draggable(Control): + def __init__( + self, + ref: Ref = None, + disabled: bool = None, + visible: bool = None, + data: any = None, + # + # Specific + # + group: str = None, + content: Control = None, + content_when_dragging: Control = None, + content_feedback: Control = None, + ): + + Control.__init__( + self, + ref=ref, + disabled=disabled, + visible=visible, + data=data, + ) + + self.__content: Control = None + self.__content_when_dragging: Control = None + self.__content_feedback: Control = None + + self.group = group + self.content = content + self.content_when_dragging = content_when_dragging + self.content_feedback = content_feedback + + def _get_control_name(self): + return "draggable" + + def _get_children(self): + children = [] + if self.__content: + self.__content._set_attr_internal("n", "content") + children.append(self.__content) + if self.__content_feedback: + self.__content_feedback._set_attr_internal("n", "content_when_dragging") + children.append(self.__content_feedback) + if self.__content_feedback: + self.__content_feedback._set_attr_internal("n", "content_feedback") + children.append(self.__content_feedback) + return children + + # group + @property + def group(self): + return self._get_attr("group") + + @group.setter + @beartype + def group(self, value): + self._set_attr("group", value) + + # content + @property + def content(self): + return self.__content + + @content.setter + def content(self, value): + self.__content = value + + # content_when_dragging + @property + def content_when_dragging(self): + return self.__content_when_dragging + + @content_when_dragging.setter + def content_when_dragging(self, value): + self.__content_when_dragging = value + + # content_feedback + @property + def content_feedback(self): + return self.__content_feedback + + @content_feedback.setter + def content_feedback(self, value): + self.__content_feedback = value