-
Notifications
You must be signed in to change notification settings - Fork 249
[desktop_drop] macOS: robust multi-source drag & drop (file URLs, promises, directories) — 0.7.0 #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[desktop_drop] macOS: robust multi-source drag & drop (file URLs, promises, directories) — 0.7.0 #434
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,8 +2,32 @@ import 'dart:typed_data'; | |||||||
|
|
||||||||
| import 'package:cross_file/cross_file.dart'; | ||||||||
|
|
||||||||
| /// A dropped item. | ||||||||
| /// | ||||||||
| /// On desktop, this is usually a filesystem path (file or directory). | ||||||||
| /// | ||||||||
| /// macOS specifics: | ||||||||
| /// - If the drag source provided a real file URL (e.g. Finder/JetBrains), | ||||||||
| /// [extraAppleBookmark] will typically be non-null and allow security-scoped | ||||||||
| /// access when running sandboxed. | ||||||||
| /// - If the drag source used a file promise (e.g. VS Code/Electron), the | ||||||||
| /// system delivers bytes into a per-drop temporary folder inside your app's | ||||||||
| /// container. In that case [fromPromise] is true and [extraAppleBookmark] | ||||||||
| /// is usually null/empty. There is no original source path in this flow. | ||||||||
| abstract class DropItem extends XFile { | ||||||||
| /// Security-scoped bookmark bytes for the dropped item (macOS only). | ||||||||
| /// | ||||||||
| /// Use with [DesktopDrop.startAccessingSecurityScopedResource] to gain | ||||||||
| /// temporary access to files outside your sandbox. When empty or null, | ||||||||
| /// you typically don't need to call start/stop (e.g. promise files in | ||||||||
| /// your app's container). | ||||||||
| Uint8List? extraAppleBookmark; | ||||||||
|
|
||||||||
| /// True when this item was delivered via a macOS file promise and was | ||||||||
| /// written into your app's temporary Drops directory. | ||||||||
| /// | ||||||||
| /// In this case, the original source path is not available by design. | ||||||||
| final bool fromPromise; | ||||||||
| DropItem( | ||||||||
| super.path, { | ||||||||
| super.mimeType, | ||||||||
|
|
@@ -12,6 +36,7 @@ abstract class DropItem extends XFile { | |||||||
| super.bytes, | ||||||||
| super.lastModified, | ||||||||
| this.extraAppleBookmark, | ||||||||
| this.fromPromise = false, | ||||||||
| }); | ||||||||
|
|
||||||||
| DropItem.fromData( | ||||||||
|
|
@@ -21,6 +46,8 @@ abstract class DropItem extends XFile { | |||||||
| super.length, | ||||||||
| super.lastModified, | ||||||||
| super.path, | ||||||||
| this.extraAppleBookmark, | ||||||||
| this.fromPromise = false, | ||||||||
| }) : super.fromData(); | ||||||||
| } | ||||||||
|
|
||||||||
|
|
@@ -33,6 +60,7 @@ class DropItemFile extends DropItem { | |||||||
| super.bytes, | ||||||||
| super.lastModified, | ||||||||
| super.extraAppleBookmark, | ||||||||
| super.fromPromise, | ||||||||
| }); | ||||||||
|
|
||||||||
| DropItemFile.fromData( | ||||||||
|
|
@@ -42,9 +70,11 @@ class DropItemFile extends DropItem { | |||||||
| super.length, | ||||||||
| super.lastModified, | ||||||||
| super.path, | ||||||||
|
||||||||
| super.path, | |
| super.path, | |
| super.extraAppleBookmark, |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -32,8 +32,11 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin { | |||||||
| let d = DropTarget(frame: vc.view.bounds, channel: channel) | ||||||||
| d.autoresizingMask = [.width, .height] | ||||||||
|
|
||||||||
| d.registerForDraggedTypes(NSFilePromiseReceiver.readableDraggedTypes.map { NSPasteboard.PasteboardType($0) }) | ||||||||
| d.registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL]) | ||||||||
| // Register for all relevant types (promises, URLs, and legacy filename arrays) | ||||||||
| var types = NSFilePromiseReceiver.readableDraggedTypes.map { NSPasteboard.PasteboardType($0) } | ||||||||
| types.append(.fileURL) // public.file-url | ||||||||
| types.append(NSPasteboard.PasteboardType("NSFilenamesPboardType")) // legacy multi-file array | ||||||||
| d.registerForDraggedTypes(types) | ||||||||
|
|
||||||||
| vc.view.addSubview(d) | ||||||||
|
|
||||||||
|
|
@@ -49,7 +52,7 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin { | |||||||
| let bookmarkByte = map["apple-bookmark"] as! FlutterStandardTypedData | ||||||||
| let bookmark = bookmarkByte.data | ||||||||
|
|
||||||||
| let url = try? URL(resolvingBookmarkData: bookmark, bookmarkDataIsStale: &isStale) | ||||||||
| let url = try? URL(resolvingBookmarkData: bookmark, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale) | ||||||||
| let suc = url?.startAccessingSecurityScopedResource() | ||||||||
| result(suc) | ||||||||
| return | ||||||||
|
|
@@ -60,7 +63,7 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin { | |||||||
| var isStale: Bool = false | ||||||||
| let bookmarkByte = map["apple-bookmark"] as! FlutterStandardTypedData | ||||||||
| let bookmark = bookmarkByte.data | ||||||||
| let url = try? URL(resolvingBookmarkData: bookmark, bookmarkDataIsStale: &isStale) | ||||||||
| let url = try? URL(resolvingBookmarkData: bookmark, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale) | ||||||||
| url?.stopAccessingSecurityScopedResource() | ||||||||
| result(true) | ||||||||
| return | ||||||||
|
|
@@ -76,6 +79,7 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin { | |||||||
|
|
||||||||
| class DropTarget: NSView { | ||||||||
| private let channel: FlutterMethodChannel | ||||||||
| private let itemsLock = NSLock() | ||||||||
|
|
||||||||
| init(frame frameRect: NSRect, channel: FlutterMethodChannel) { | ||||||||
| self.channel = channel | ||||||||
|
|
@@ -100,12 +104,17 @@ class DropTarget: NSView { | |||||||
| channel.invokeMethod("exited", arguments: nil) | ||||||||
| } | ||||||||
|
|
||||||||
| /// Directory URL used for accepting file promises. | ||||||||
| private lazy var destinationURL: URL = { | ||||||||
| let destinationURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("Drops") | ||||||||
| try? FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil) | ||||||||
| return destinationURL | ||||||||
| }() | ||||||||
| /// Create a per-drop destination for promised files (avoids name collisions). | ||||||||
| private func uniqueDropDestination() -> URL { | ||||||||
| let base = FileManager.default.temporaryDirectory.appendingPathComponent("Drops", isDirectory: true) | ||||||||
| let formatter = DateFormatter() | ||||||||
| formatter.locale = Locale(identifier: "en_US_POSIX") | ||||||||
| formatter.dateFormat = "yyyyMMdd_HHmmss_SSS'Z'" | ||||||||
|
||||||||
| formatter.dateFormat = "yyyyMMdd_HHmmss_SSS'Z'" | |
| formatter.dateFormat = "yyyyMMdd_HHmmss_SSS'Z'" | |
| formatter.timeZone = TimeZone(secondsFromGMT: 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DropItem.fromData constructor is missing the extraAppleBookmark parameter in its super() call. This will cause extraAppleBookmark to always be null for items created via fromData, which may not be the intended behavior.