From fe53a9d62c04a406632b1a35c3ccd0c1be9b1dc9 Mon Sep 17 00:00:00 2001 From: LorTos Date: Mon, 3 Apr 2023 11:18:02 +0200 Subject: [PATCH 1/4] feat: support multiple drop targets --- .../desktop_drop/lib/desktop_drop_web.dart | 130 ++++++++++------- packages/desktop_drop/lib/src/channel.dart | 54 ++++--- .../desktop_drop/lib/src/drop_target.dart | 135 +++++++++--------- 3 files changed, 183 insertions(+), 136 deletions(-) diff --git a/packages/desktop_drop/lib/desktop_drop_web.dart b/packages/desktop_drop/lib/desktop_drop_web.dart index a60d5d1b..8856c91e 100644 --- a/packages/desktop_drop/lib/desktop_drop_web.dart +++ b/packages/desktop_drop/lib/desktop_drop_web.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:html' as html show window, Url; +import 'dart:html' as html show window, Url, DataTransfer; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; @@ -25,66 +25,92 @@ class DesktopDropWeb { pluginInstance._registerEvents(); } + html.DataTransfer? _dataTransfer; + void _registerEvents() { - html.window.onDrop.listen((event) { - event.preventDefault(); + html.window.onDragEnter.listen( + (event) { + event.preventDefault(); + _dataTransfer = event.dataTransfer; + channel.invokeMethod('entered', [ + event.client.x.toDouble(), + event.client.y.toDouble(), + ]); + }, + ); - final results = []; + html.window.onDragOver.listen( + (event) { + event.preventDefault(); + _dataTransfer = event.dataTransfer; + channel.invokeMethod('updated', [ + event.client.x.toDouble(), + event.client.y.toDouble(), + ]); + }, + ); - try { - final items = event.dataTransfer.files; - if (items != null) { - for (final item in items) { - results.add( - WebDropItem( - uri: html.Url.createObjectUrl(item), - name: item.name, - size: item.size, - type: item.type, - relativePath: item.relativePath, - lastModified: item.lastModified != null - ? DateTime.fromMillisecondsSinceEpoch(item.lastModified!) - : item.lastModifiedDate, - ), - ); + html.window.onDrop.listen( + (event) { + event.preventDefault(); + _dataTransfer = null; + final results = []; + + try { + final items = event.dataTransfer.files; + if (items != null) { + for (final item in items) { + results.add( + WebDropItem( + uri: html.Url.createObjectUrl(item), + name: item.name, + size: item.size, + type: item.type, + relativePath: item.relativePath, + lastModified: item.lastModified != null + ? DateTime.fromMillisecondsSinceEpoch(item.lastModified!) + : item.lastModifiedDate, + ), + ); + } } + } catch (e, s) { + debugPrint('desktop_drop_web: $e $s'); + } finally { + channel.invokeMethod( + "performOperation_web", + results.map((e) => e.toJson()).toList(), + ); } - } catch (e, s) { - debugPrint('desktop_drop_web: $e $s'); - } finally { - channel.invokeMethod( - "performOperation_web", - results.map((e) => e.toJson()).toList(), - ); - } - }); - - html.window.onDragEnter.listen((event) { - event.preventDefault(); - channel.invokeMethod('entered', [ - event.client.x.toDouble(), - event.client.y.toDouble(), - ]); - }); - - html.window.onDragOver.listen((event) { - event.preventDefault(); - channel.invokeMethod('updated', [ - event.client.x.toDouble(), - event.client.y.toDouble(), - ]); - }); + }, + ); - html.window.onDragLeave.listen((event) { - event.preventDefault(); - channel.invokeMethod('exited', [ - event.client.x.toDouble(), - event.client.y.toDouble(), - ]); - }); + html.window.onDragLeave.listen( + (event) { + event.preventDefault(); + _dataTransfer = null; + channel.invokeMethod('exited', [ + event.client.x.toDouble(), + event.client.y.toDouble(), + ]); + }, + ); } Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'updateDroppableStatus': + final enable = call.arguments as bool; + final current = _dataTransfer?.dropEffect; + final newValue = enable ? 'copy' : 'move'; + if (current != newValue) { + _dataTransfer?.dropEffect = newValue; + } + return; + default: + break; + } + throw PlatformException( code: 'Unimplemented', details: 'desktop_drop for web doesn\'t implement \'${call.method}\'', diff --git a/packages/desktop_drop/lib/src/channel.dart b/packages/desktop_drop/lib/src/channel.dart index 492a9319..45d827c1 100644 --- a/packages/desktop_drop/lib/src/channel.dart +++ b/packages/desktop_drop/lib/src/channel.dart @@ -8,7 +8,10 @@ import 'drop_item.dart'; import 'events.dart'; import 'utils/platform.dart' if (dart.library.html) 'utils/platform_web.dart'; -typedef RawDropListener = void Function(DropEvent); +abstract class RawDropListener { + void onEvent(DropEvent event); + bool isInBounds(DropEvent event); +} class DesktopDrop { static const MethodChannel _channel = MethodChannel('desktop_drop'); @@ -19,22 +22,24 @@ class DesktopDrop { final _listeners = {}; - var _inited = false; + var _initialized = false; Offset? _offset; void init() { - if (_inited) { + if (_initialized) { return; } - _inited = true; - _channel.setMethodCallHandler((call) async { - try { - return await _handleMethodChannel(call); - } catch (e, s) { - debugPrint('_handleMethodChannel: $e $s'); - } - }); + _initialized = true; + _channel.setMethodCallHandler( + (call) async { + try { + return await _handleMethodChannel(call); + } catch (e, s) { + debugPrint('_handleMethodChannel: $e $s'); + } + }, + ); } Future _handleMethodChannel(MethodCall call) async { @@ -45,15 +50,14 @@ class DesktopDrop { _notifyEvent(DropEnterEvent(location: _offset!)); break; case "updated": - if (_offset == null && Platform.isLinux) { - final position = (call.arguments as List).cast(); - _offset = Offset(position[0], position[1]); - _notifyEvent(DropEnterEvent(location: _offset!)); - return; - } final position = (call.arguments as List).cast(); + final previousOffset = _offset; _offset = Offset(position[0], position[1]); - _notifyEvent(DropUpdateEvent(location: _offset!)); + if (previousOffset == null) { + _notifyEvent(DropEnterEvent(location: _offset!)); + } else { + _notifyEvent(DropUpdateEvent(location: _offset!)); + } break; case "exited": _notifyEvent(DropExitEvent(location: _offset ?? Offset.zero)); @@ -109,9 +113,19 @@ class DesktopDrop { } void _notifyEvent(DropEvent event) { - for (final listener in _listeners) { - listener(event); + final reversedListeners = _listeners.toList(growable: false).reversed; + var foundTargetListener = false; + for (final listener in reversedListeners) { + final isInBounds = listener.isInBounds(event); + if (isInBounds && !foundTargetListener) { + foundTargetListener = true; + listener.onEvent(event); + } else { + listener.onEvent(DropExitEvent(location: event.location)); + } } + + _channel.invokeMethod('updateDroppableStatus', foundTargetListener); } void addRawDropEventListener(RawDropListener listener) { diff --git a/packages/desktop_drop/lib/src/drop_target.dart b/packages/desktop_drop/lib/src/drop_target.dart index 7f03f4e8..59a58815 100644 --- a/packages/desktop_drop/lib/src/drop_target.dart +++ b/packages/desktop_drop/lib/src/drop_target.dart @@ -1,4 +1,5 @@ import 'package:cross_file/cross_file.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'channel.dart'; @@ -77,26 +78,25 @@ enum _DragTargetStatus { idle, } -class _DropTargetState extends State { +class _DropTargetState extends State implements RawDropListener { _DragTargetStatus _status = _DragTargetStatus.idle; + Offset? _latestGlobalPosition; + Offset? _latestLocalPosition; @override void initState() { super.initState(); DesktopDrop.instance.init(); if (widget.enable) { - DesktopDrop.instance.addRawDropEventListener(_onDropEvent); + DesktopDrop.instance.addRawDropEventListener(this); } } @override void didUpdateWidget(DropTarget oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.enable && !oldWidget.enable) { - DesktopDrop.instance.addRawDropEventListener(_onDropEvent); - } else if (!widget.enable && oldWidget.enable) { - DesktopDrop.instance.removeRawDropEventListener(_onDropEvent); - if (_status != _DragTargetStatus.idle) { + if (widget.enable != oldWidget.enable) { + if (!widget.enable) { _updateStatus( _DragTargetStatus.idle, localLocation: Offset.zero, @@ -106,17 +106,67 @@ class _DropTargetState extends State { } } - void _onDropEvent(DropEvent event) { + void _updateStatus( + _DragTargetStatus status, { + bool debugRequiredStatus = true, + required Offset localLocation, + required Offset globalLocation, + }) { + _status = status; + final details = DropEventDetails( + localPosition: localLocation, + globalPosition: globalLocation, + ); + switch (_status) { + case _DragTargetStatus.enter: + widget.onDragEntered?.call(details); + break; + case _DragTargetStatus.update: + widget.onDragUpdated?.call(details); + break; + case _DragTargetStatus.idle: + _latestGlobalPosition = null; + _latestLocalPosition = null; + widget.onDragExited?.call(details); + break; + } + } + + @override + void dispose() { + if (widget.enable) { + DesktopDrop.instance.removeRawDropEventListener(this); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; + + @override + bool isInBounds(DropEvent event) { final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null) { - return; + if (renderBox == null || !widget.enable) { + return false; } - final globalPosition = _scaleHoverPoint(context, event.location); + final globalPosition = _scaleHoverPoint(event.location); final position = renderBox.globalToLocal(globalPosition); - bool inBounds = renderBox.paintBounds.contains(position); + _latestGlobalPosition = globalPosition; + _latestLocalPosition = position; + + return renderBox.hitTest(BoxHitTestResult(), position: position); + } + + @override + void onEvent(DropEvent event) { + final inBounds = isInBounds(event); + final position = _latestLocalPosition; + final globalPosition = _latestGlobalPosition; + if (position == null || globalPosition == null) { + return; + } if (event is DropEnterEvent) { if (!inBounds) { - assert(_status == _DragTargetStatus.idle); } else { _updateStatus( _DragTargetStatus.enter, @@ -131,9 +181,7 @@ class _DropTargetState extends State { globalLocation: globalPosition, localLocation: position, ); - } else if ((_status == _DragTargetStatus.enter || - _status == _DragTargetStatus.update) && - inBounds) { + } else if ((_status == _DragTargetStatus.enter || _status == _DragTargetStatus.update) && inBounds) { _updateStatus( _DragTargetStatus.update, globalLocation: globalPosition, @@ -153,9 +201,7 @@ class _DropTargetState extends State { globalLocation: globalPosition, localLocation: position, ); - } else if (event is DropDoneEvent && - (_status != _DragTargetStatus.idle || Platform.isLinux) && - inBounds) { + } else if (event is DropDoneEvent && (_status != _DragTargetStatus.idle || Platform.isLinux) && inBounds) { _updateStatus( _DragTargetStatus.idle, debugRequiredStatus: false, @@ -170,51 +216,12 @@ class _DropTargetState extends State { } } - void _updateStatus( - _DragTargetStatus status, { - bool debugRequiredStatus = true, - required Offset localLocation, - required Offset globalLocation, - }) { - assert(!debugRequiredStatus || _status != status); - _status = status; - final details = DropEventDetails( - localPosition: localLocation, - globalPosition: globalLocation, - ); - switch (_status) { - case _DragTargetStatus.enter: - widget.onDragEntered?.call(details); - break; - case _DragTargetStatus.update: - widget.onDragUpdated?.call(details); - break; - case _DragTargetStatus.idle: - widget.onDragExited?.call(details); - break; - } - } - - @override - void dispose() { - if (widget.enable) { - DesktopDrop.instance.removeRawDropEventListener(_onDropEvent); + Offset _scaleHoverPoint(Offset point) { + if (Platform.isWindows || Platform.isAndroid) { + final pixelRatio = MediaQuery.of(context).devicePixelRatio; + final scaleAmount = 1 / pixelRatio; + return point.scale(scaleAmount, scaleAmount); } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -Offset _scaleHoverPoint(BuildContext context, Offset point) { - if (Platform.isWindows || Platform.isAndroid) { - return point.scale( - 1 / MediaQuery.of(context).devicePixelRatio, - 1 / MediaQuery.of(context).devicePixelRatio, - ); + return point; } - return point; } From 0cd762483dc43591ca6b900b2fd7bc66e1e78dff Mon Sep 17 00:00:00 2001 From: LorTos Date: Tue, 4 Apr 2023 15:05:08 +0200 Subject: [PATCH 2/4] feat: refactor to use RenderObjects instead of widgets --- packages/desktop_drop/example/lib/main.dart | 19 +- .../desktop_drop/example/macos/Podfile.lock | 2 +- packages/desktop_drop/example/pubspec.lock | 30 +- packages/desktop_drop/lib/src/channel.dart | 103 ++++--- .../desktop_drop/lib/src/drop_target.dart | 271 +++++++----------- packages/desktop_drop/lib/src/events.dart | 20 +- .../desktop_drop/lib/src/utils/platform.dart | 11 - .../lib/src/utils/platform_web.dart | 9 - packages/desktop_drop/pubspec.yaml | 3 +- 9 files changed, 206 insertions(+), 262 deletions(-) delete mode 100644 packages/desktop_drop/lib/src/utils/platform.dart delete mode 100644 packages/desktop_drop/lib/src/utils/platform_web.dart diff --git a/packages/desktop_drop/example/lib/main.dart b/packages/desktop_drop/example/lib/main.dart index 0a65ec37..c4aa1cda 100644 --- a/packages/desktop_drop/example/lib/main.dart +++ b/packages/desktop_drop/example/lib/main.dart @@ -51,28 +51,28 @@ class _ExampleDragTargetState extends State { @override Widget build(BuildContext context) { return DropTarget( - onDragDone: (detail) async { + onDragDone: (files, location) async { setState(() { - _list.addAll(detail.files); + _list.addAll(files); }); debugPrint('onDragDone:'); - for (final file in detail.files) { + for (final file in files) { debugPrint(' ${file.path} ${file.name}' ' ${await file.lastModified()}' ' ${await file.length()}' ' ${file.mimeType}'); } }, - onDragUpdated: (details) { + onDragUpdated: (localPosition) { setState(() { - offset = details.localPosition; + offset = localPosition; }); }, - onDragEntered: (detail) { + onDragEntered: (localPosition) { setState(() { _dragging = true; - offset = detail.localPosition; + offset = localPosition; }); }, onDragExited: (detail) { @@ -87,10 +87,7 @@ class _ExampleDragTargetState extends State { color: _dragging ? Colors.blue.withOpacity(0.4) : Colors.black26, child: Stack( children: [ - if (_list.isEmpty) - const Center(child: Text("Drop here")) - else - Text(_list.map((e) => e.path).join("\n")), + if (_list.isEmpty) const Center(child: Text("Drop here")) else Text(_list.map((e) => e.path).join("\n")), if (offset != null) Align( alignment: Alignment.topRight, diff --git a/packages/desktop_drop/example/macos/Podfile.lock b/packages/desktop_drop/example/macos/Podfile.lock index 582d3d89..cb95be65 100644 --- a/packages/desktop_drop/example/macos/Podfile.lock +++ b/packages/desktop_drop/example/macos/Podfile.lock @@ -15,7 +15,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/packages/desktop_drop/example/pubspec.lock b/packages/desktop_drop/example/pubspec.lock index c09f4e10..bccbd2ce 100644 --- a/packages/desktop_drop/example/pubspec.lock +++ b/packages/desktop_drop/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" cross_file: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: "486b7bc707424572cdf7bd7e812a0c146de3fd47ecadf070254cc60383f21dd8" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.5" desktop_drop: dependency: "direct main" description: @@ -131,10 +131,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" path: dependency: transitive description: @@ -160,18 +160,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -192,10 +192,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" vector_math: dependency: transitive description: @@ -208,10 +208,10 @@ packages: dependency: transitive description: name: web - sha256: "14f1f70c51119012600c5f1f60ca68efda5a9b6077748163c6af2893ec5df8fc" + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.2.1-beta" + version: "0.1.4-beta" sdks: - dart: ">=3.2.0-157.0.dev <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=1.20.0" diff --git a/packages/desktop_drop/lib/src/channel.dart b/packages/desktop_drop/lib/src/channel.dart index 45d827c1..2b8dd9ac 100644 --- a/packages/desktop_drop/lib/src/channel.dart +++ b/packages/desktop_drop/lib/src/channel.dart @@ -1,16 +1,19 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:cross_file/cross_file.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'drop_item.dart'; import 'events.dart'; -import 'utils/platform.dart' if (dart.library.html) 'utils/platform_web.dart'; abstract class RawDropListener { - void onEvent(DropEvent event); - bool isInBounds(DropEvent event); + /// Returns true if event was handled, false otherwise + bool handleDropEvent(DropEvent event); + + Offset globalToLocalOffset(Offset global); } class DesktopDrop { @@ -20,10 +23,9 @@ class DesktopDrop { static final instance = DesktopDrop._(); - final _listeners = {}; - var _initialized = false; + RawDropListener? _currentTargetListener; Offset? _offset; void init() { @@ -47,25 +49,25 @@ class DesktopDrop { case "entered": final position = (call.arguments as List).cast(); _offset = Offset(position[0], position[1]); - _notifyEvent(DropEnterEvent(location: _offset!)); + _notifyPositionEvent(DropEnterEvent(location: _offset!)); break; case "updated": final position = (call.arguments as List).cast(); final previousOffset = _offset; _offset = Offset(position[0], position[1]); if (previousOffset == null) { - _notifyEvent(DropEnterEvent(location: _offset!)); + _notifyPositionEvent(DropEnterEvent(location: _offset!)); } else { - _notifyEvent(DropUpdateEvent(location: _offset!)); + _notifyPositionEvent(DropUpdateEvent(location: _offset!)); } break; case "exited": - _notifyEvent(DropExitEvent(location: _offset ?? Offset.zero)); + _notifyPositionEvent(DropExitEvent(location: _offset ?? Offset.zero)); _offset = null; break; case "performOperation": final paths = (call.arguments as List).cast(); - _notifyEvent( + _notifyDoneEvent( DropDoneEvent( location: _offset ?? Offset.zero, files: paths.map((e) => XFile(e)).toList(), @@ -85,10 +87,12 @@ class DesktopDrop { } return ''; }).where((e) => e.isNotEmpty); - _notifyEvent(DropDoneEvent( - location: Offset(offset[0], offset[1]), - files: paths.map((e) => XFile(e)).toList(), - )); + _notifyDoneEvent( + DropDoneEvent( + location: Offset(offset[0], offset[1]), + files: paths.map((e) => XFile(e)).toList(), + ), + ); break; case "performOperation_web": final results = (call.arguments as List) @@ -102,7 +106,7 @@ class DesktopDrop { mimeType: e.type, )) .toList(); - _notifyEvent( + _notifyDoneEvent( DropDoneEvent(location: _offset ?? Offset.zero, files: results), ); _offset = null; @@ -112,29 +116,62 @@ class DesktopDrop { } } - void _notifyEvent(DropEvent event) { - final reversedListeners = _listeners.toList(growable: false).reversed; - var foundTargetListener = false; - for (final listener in reversedListeners) { - final isInBounds = listener.isInBounds(event); - if (isInBounds && !foundTargetListener) { - foundTargetListener = true; - listener.onEvent(event); + void _notifyPositionEvent(DropEvent event) { + final RawDropListener? target; + + if (event is DropExitEvent) { + target = null; + } else { + final result = BoxHitTestResult(); + WidgetsBinding.instance.renderView.hitTest(result, position: event.location); + + target = result.path.firstWhereOrNull((entry) => entry.target is RawDropListener)?.target as RawDropListener?; + } + + if (_currentTargetListener != target) { + final previous = _currentTargetListener; + if (previous != null) { + previous.handleDropEvent( + DropExitEvent( + location: previous.globalToLocalOffset(event.location), + ), + ); + } + } + if (target != null) { + final position = target.globalToLocalOffset(event.location); + if (_currentTargetListener == null) { + target.handleDropEvent(DropEnterEvent(location: position)); } else { - listener.onEvent(DropExitEvent(location: event.location)); + target.handleDropEvent(DropUpdateEvent(location: position)); } } - - _channel.invokeMethod('updateDroppableStatus', foundTargetListener); + _currentTargetListener = target; + _channel.invokeMethod('updateDroppableStatus', target != null); } - void addRawDropEventListener(RawDropListener listener) { - assert(!_listeners.contains(listener)); - _listeners.add(listener); - } + void _notifyDoneEvent(DropDoneEvent event) { + final result = BoxHitTestResult(); + WidgetsBinding.instance.renderView.hitTest(result, position: event.location); - void removeRawDropEventListener(RawDropListener listener) { - assert(_listeners.contains(listener)); - _listeners.remove(listener); + final target = result.path.firstWhereOrNull((entry) => entry.target is RawDropListener)?.target as RawDropListener?; + final previous = _currentTargetListener; + if (previous != null) { + previous.handleDropEvent( + DropExitEvent( + location: previous.globalToLocalOffset(event.location), + ), + ); + _currentTargetListener = null; + } + if (target != null) { + target.handleDropEvent( + DropDoneEvent( + location: target.globalToLocalOffset(event.location), + files: event.files, + ), + ); + } + _channel.invokeMethod('updateDroppableStatus', false); } } diff --git a/packages/desktop_drop/lib/src/drop_target.dart b/packages/desktop_drop/lib/src/drop_target.dart index 59a58815..900592c6 100644 --- a/packages/desktop_drop/lib/src/drop_target.dart +++ b/packages/desktop_drop/lib/src/drop_target.dart @@ -4,224 +4,157 @@ import 'package:flutter/widgets.dart'; import 'channel.dart'; import 'events.dart'; -import 'utils/platform.dart' if (dart.library.html) 'utils/platform_web.dart'; - -@immutable -class DropDoneDetails { - const DropDoneDetails({ - required this.files, - required this.localPosition, - required this.globalPosition, - }); - - final List files; - final Offset localPosition; - final Offset globalPosition; -} - -class DropEventDetails { - DropEventDetails({ - required this.localPosition, - required this.globalPosition, - }); - - final Offset localPosition; - final Offset globalPosition; -} +typedef OnDragDoneCallback = void Function(List files, Offset localPosition); -typedef OnDragDoneCallback = void Function(DropDoneDetails details); +typedef OnDragCallback = void Function(Offset localPosition); -typedef OnDragCallback = void Function(Detail details); +typedef OnDragActiveStatusChange = void Function(bool isActive); /// A widget that accepts draggable files. -class DropTarget extends StatefulWidget { +class DropTarget extends SingleChildRenderObjectWidget { const DropTarget({ Key? key, - required this.child, + super.child, this.onDragEntered, this.onDragExited, this.onDragDone, this.onDragUpdated, - this.enable = true, + this.onDragActiveStatusChange, + this.isEnabled = true, }) : super(key: key); - final Widget child; - /// Callback when drag entered target area. - final OnDragCallback? onDragEntered; + final OnDragCallback? onDragEntered; /// Callback when drag exited target area. - final OnDragCallback? onDragExited; + final OnDragCallback? onDragExited; /// Callback when drag hover on target area. - final OnDragCallback? onDragUpdated; + final OnDragCallback? onDragUpdated; /// Callback when drag dropped on target area. final OnDragDoneCallback? onDragDone; - /// Whether to enable drop target. - /// - /// ATTENTION: You should disable drop target when you push a new page/widget in - /// front of this drop target, since the drop target will still receive drag events - /// even it is invisible. - /// https://github.com/MixinNetwork/flutter-plugins/issues/2 - final bool enable; + final OnDragActiveStatusChange? onDragActiveStatusChange; + + final bool isEnabled; @override - State createState() => _DropTargetState(); -} + _DropTargetRenderObject createRenderObject(BuildContext context) => _DropTargetRenderObject( + isEnabled: isEnabled, + onDragEntered: onDragEntered, + onDragExited: onDragExited, + onDragUpdated: onDragUpdated, + onDragDone: onDragDone, + onDragActiveStatusChange: onDragActiveStatusChange, + ); -enum _DragTargetStatus { - enter, - update, - idle, + @override + void updateRenderObject(BuildContext context, covariant _DropTargetRenderObject renderObject) { + renderObject + ..isEnabled = isEnabled + ..onDragEntered = onDragEntered + ..onDragExited = onDragExited + ..onDragUpdated = onDragUpdated + ..onDragDone = onDragDone + ..onDragActiveStatusChange = onDragActiveStatusChange; + } } -class _DropTargetState extends State implements RawDropListener { - _DragTargetStatus _status = _DragTargetStatus.idle; - Offset? _latestGlobalPosition; - Offset? _latestLocalPosition; - - @override - void initState() { - super.initState(); +class _DropTargetRenderObject extends RenderProxyBoxWithHitTestBehavior implements RawDropListener { + _DropTargetRenderObject({ + required bool isEnabled, + required this.onDragEntered, + required this.onDragExited, + required this.onDragUpdated, + required this.onDragDone, + required this.onDragActiveStatusChange, + }) : super(behavior: HitTestBehavior.opaque) { DesktopDrop.instance.init(); - if (widget.enable) { - DesktopDrop.instance.addRawDropEventListener(this); - } + this.isEnabled = isEnabled; } - @override - void didUpdateWidget(DropTarget oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.enable != oldWidget.enable) { - if (!widget.enable) { - _updateStatus( - _DragTargetStatus.idle, - localLocation: Offset.zero, - globalLocation: Offset.zero, - ); + bool _isActive = false; + set isActive(bool newValue) { + if (newValue == _isActive) { + return; + } + final position = _latestLocalPosition!; + if (newValue) { + if (_isActive) { + onDragUpdated?.call(position); + } else { + onDragEntered?.call(position); } + } else { + _latestLocalPosition = null; + onDragExited?.call(position); } + _isActive = newValue; + onDragActiveStatusChange?.call(newValue); } - void _updateStatus( - _DragTargetStatus status, { - bool debugRequiredStatus = true, - required Offset localLocation, - required Offset globalLocation, - }) { - _status = status; - final details = DropEventDetails( - localPosition: localLocation, - globalPosition: globalLocation, - ); - switch (_status) { - case _DragTargetStatus.enter: - widget.onDragEntered?.call(details); - break; - case _DragTargetStatus.update: - widget.onDragUpdated?.call(details); - break; - case _DragTargetStatus.idle: - _latestGlobalPosition = null; - _latestLocalPosition = null; - widget.onDragExited?.call(details); - break; + bool _isEnabled = false; + set isEnabled(bool value) { + if (value != _isEnabled) { + _isEnabled = value; + if (!value) { + isActive = false; + } } } + /// Callback when drag entered target area. + OnDragCallback? onDragEntered; + + /// Callback when drag exited target area. + OnDragCallback? onDragExited; + + /// Callback when drag hover on target area. + OnDragCallback? onDragUpdated; + + /// Callback when drag dropped on target area. + OnDragDoneCallback? onDragDone; + + OnDragActiveStatusChange? onDragActiveStatusChange; + + Offset? _latestLocalPosition; + @override void dispose() { - if (widget.enable) { - DesktopDrop.instance.removeRawDropEventListener(this); - } + isEnabled = false; super.dispose(); } @override - Widget build(BuildContext context) => widget.child; - - @override - bool isInBounds(DropEvent event) { - final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null || !widget.enable) { - return false; + bool hitTest(BoxHitTestResult result, {required Offset position}) { + if (!_isEnabled) { + return child?.hitTest(result, position: position) ?? false; } - final globalPosition = _scaleHoverPoint(event.location); - final position = renderBox.globalToLocal(globalPosition); - _latestGlobalPosition = globalPosition; - _latestLocalPosition = position; - - return renderBox.hitTest(BoxHitTestResult(), position: position); + return super.hitTest(result, position: position); } @override - void onEvent(DropEvent event) { - final inBounds = isInBounds(event); - final position = _latestLocalPosition; - final globalPosition = _latestGlobalPosition; - if (position == null || globalPosition == null) { - return; - } - if (event is DropEnterEvent) { - if (!inBounds) { - } else { - _updateStatus( - _DragTargetStatus.enter, - globalLocation: globalPosition, - localLocation: position, - ); - } - } else if (event is DropUpdateEvent) { - if (_status == _DragTargetStatus.idle && inBounds) { - _updateStatus( - _DragTargetStatus.enter, - globalLocation: globalPosition, - localLocation: position, - ); - } else if ((_status == _DragTargetStatus.enter || _status == _DragTargetStatus.update) && inBounds) { - _updateStatus( - _DragTargetStatus.update, - globalLocation: globalPosition, - localLocation: position, - debugRequiredStatus: false, - ); - } else if (_status != _DragTargetStatus.idle && !inBounds) { - _updateStatus( - _DragTargetStatus.idle, - globalLocation: globalPosition, - localLocation: position, - ); - } - } else if (event is DropExitEvent && _status != _DragTargetStatus.idle) { - _updateStatus( - _DragTargetStatus.idle, - globalLocation: globalPosition, - localLocation: position, - ); - } else if (event is DropDoneEvent && (_status != _DragTargetStatus.idle || Platform.isLinux) && inBounds) { - _updateStatus( - _DragTargetStatus.idle, - debugRequiredStatus: false, - globalLocation: globalPosition, - localLocation: position, - ); - widget.onDragDone?.call(DropDoneDetails( - files: event.files, - localPosition: position, - globalPosition: globalPosition, - )); + bool handleDropEvent(DropEvent event) { + _latestLocalPosition = event.location; + + if (!_isEnabled) { + isActive = false; + return false; } - } - Offset _scaleHoverPoint(Offset point) { - if (Platform.isWindows || Platform.isAndroid) { - final pixelRatio = MediaQuery.of(context).devicePixelRatio; - final scaleAmount = 1 / pixelRatio; - return point.scale(scaleAmount, scaleAmount); + if (event is DropEnterEvent || event is DropUpdateEvent) { + isActive = true; + } else if (event is DropExitEvent) { + isActive = false; + } else if (event is DropDoneEvent) { + onDragDone?.call(event.files, _latestLocalPosition!); + isActive = false; } - return point; + return true; } + + @override + Offset globalToLocalOffset(Offset global) => globalToLocal(global); } diff --git a/packages/desktop_drop/lib/src/events.dart b/packages/desktop_drop/lib/src/events.dart index f23e8056..5c0ad461 100644 --- a/packages/desktop_drop/lib/src/events.dart +++ b/packages/desktop_drop/lib/src/events.dart @@ -4,36 +4,32 @@ import 'package:flutter/painting.dart'; abstract class DropEvent { Offset location; - DropEvent(this.location); + DropEvent({required this.location}); @override - String toString() { - return '$runtimeType($location)'; - } + String toString() => '$runtimeType($location)'; } class DropEnterEvent extends DropEvent { - DropEnterEvent({required Offset location}) : super(location); + DropEnterEvent({required super.location}); } class DropExitEvent extends DropEvent { - DropExitEvent({required Offset location}) : super(location); + DropExitEvent({required super.location}); } class DropUpdateEvent extends DropEvent { - DropUpdateEvent({required Offset location}) : super(location); + DropUpdateEvent({required super.location}); } class DropDoneEvent extends DropEvent { final List files; DropDoneEvent({ - required Offset location, + required super.location, required this.files, - }) : super(location); + }); @override - String toString() { - return '$runtimeType($location, $files)'; - } + String toString() => '$runtimeType($location, $files)'; } diff --git a/packages/desktop_drop/lib/src/utils/platform.dart b/packages/desktop_drop/lib/src/utils/platform.dart deleted file mode 100644 index 93be0bc9..00000000 --- a/packages/desktop_drop/lib/src/utils/platform.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'dart:io' as io; - -class Platform { - static bool get isLinux => io.Platform.isLinux; - - static bool get isWindows => io.Platform.isWindows; - - static bool get isWeb => false; - - static bool get isAndroid => io.Platform.isAndroid; -} diff --git a/packages/desktop_drop/lib/src/utils/platform_web.dart b/packages/desktop_drop/lib/src/utils/platform_web.dart deleted file mode 100644 index ae2c8188..00000000 --- a/packages/desktop_drop/lib/src/utils/platform_web.dart +++ /dev/null @@ -1,9 +0,0 @@ -class Platform { - static bool get isLinux => false; - - static bool get isWindows => false; - - static bool get isWeb => true; - - static bool get isAndroid => false; -} diff --git a/packages/desktop_drop/pubspec.yaml b/packages/desktop_drop/pubspec.yaml index da850a04..6761cab4 100644 --- a/packages/desktop_drop/pubspec.yaml +++ b/packages/desktop_drop/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.4.4 homepage: https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_drop environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=1.20.0" dependencies: @@ -13,6 +13,7 @@ dependencies: flutter_web_plugins: sdk: flutter cross_file: ^0.3.3+4 + collection: any dev_dependencies: flutter_test: From 28f0f25bac23f64b95a737e84d329056998f071a Mon Sep 17 00:00:00 2001 From: LorTos Date: Tue, 4 Apr 2023 17:01:34 +0200 Subject: [PATCH 3/4] feat: improve dropEffect setter --- packages/desktop_drop/lib/desktop_drop_web.dart | 15 +++++++++++++-- packages/desktop_drop/lib/src/channel.dart | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/desktop_drop/lib/desktop_drop_web.dart b/packages/desktop_drop/lib/desktop_drop_web.dart index 8856c91e..7d100b5e 100644 --- a/packages/desktop_drop/lib/desktop_drop_web.dart +++ b/packages/desktop_drop/lib/desktop_drop_web.dart @@ -25,7 +25,18 @@ class DesktopDropWeb { pluginInstance._registerEvents(); } - html.DataTransfer? _dataTransfer; + html.DataTransfer? __dataTransfer; + + html.DataTransfer? get _dataTransfer => __dataTransfer; + + set _dataTransfer(html.DataTransfer? newValue) { + if (__dataTransfer != newValue) { + if (__dataTransfer != null) { + newValue?.dropEffect = __dataTransfer!.dropEffect; + } + __dataTransfer = newValue; + } + } void _registerEvents() { html.window.onDragEnter.listen( @@ -103,7 +114,7 @@ class DesktopDropWeb { final enable = call.arguments as bool; final current = _dataTransfer?.dropEffect; final newValue = enable ? 'copy' : 'move'; - if (current != newValue) { + if (current != newValue) { _dataTransfer?.dropEffect = newValue; } return; diff --git a/packages/desktop_drop/lib/src/channel.dart b/packages/desktop_drop/lib/src/channel.dart index 2b8dd9ac..3a32045a 100644 --- a/packages/desktop_drop/lib/src/channel.dart +++ b/packages/desktop_drop/lib/src/channel.dart @@ -137,6 +137,7 @@ class DesktopDrop { ), ); } + _channel.invokeMethod('updateDroppableStatus', target != null); } if (target != null) { final position = target.globalToLocalOffset(event.location); @@ -147,7 +148,6 @@ class DesktopDrop { } } _currentTargetListener = target; - _channel.invokeMethod('updateDroppableStatus', target != null); } void _notifyDoneEvent(DropDoneEvent event) { From cd00b8cf7f8246e4a60103c72562d20ad125db23 Mon Sep 17 00:00:00 2001 From: LorTos Date: Wed, 15 Nov 2023 15:54:38 +0700 Subject: [PATCH 4/4] feat: send updateDroppableStatus only on web --- packages/desktop_drop/lib/src/channel.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/desktop_drop/lib/src/channel.dart b/packages/desktop_drop/lib/src/channel.dart index 3a32045a..5f6995d4 100644 --- a/packages/desktop_drop/lib/src/channel.dart +++ b/packages/desktop_drop/lib/src/channel.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -137,7 +138,9 @@ class DesktopDrop { ), ); } - _channel.invokeMethod('updateDroppableStatus', target != null); + if (kIsWeb) { + _channel.invokeMethod('updateDroppableStatus', target != null); + } } if (target != null) { final position = target.globalToLocalOffset(event.location); @@ -172,6 +175,8 @@ class DesktopDrop { ), ); } - _channel.invokeMethod('updateDroppableStatus', false); + if (kIsWeb) { + _channel.invokeMethod('updateDroppableStatus', false); + } } }