mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
fix(project): paste files copied in Finder on macOS (#250)
The OS clipboard's file list was only wired up for Windows (CF_HDROP), so on macOS the project view never saw files copied in Finder: Paste stayed disabled and ⌘V did nothing. Read and write the general NSPasteboard's file URLs instead, so Finder's copies paste into the project view and the project view's copies paste in Finder. Closes #246 Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4666c4b9a5
commit
200ed6a528
3 files changed
+89
-5
No files matched your search
Generated
+5
@@ -4025,6 +4025,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ide-clipboard"
|
||||
version = "0.1.24"
|
||||
dependencies = [
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ide-core"
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
[package]
|
||||
name = "ide-clipboard"
|
||||
description = "The OS clipboard's file list (Explorer <-> project view): Windows CF_HDROP read/write, no window or message loop"
|
||||
description = "The OS clipboard's file list (Explorer <-> project view): Windows CF_HDROP or macOS NSPasteboard file URLs, no window or message loop"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = { version = "0.6", default-features = false, features = ["std"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSArray", "NSDictionary", "NSEnumerator", "NSObject", "NSString", "NSURL", "NSValue"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSPasteboard"] }
|
||||
@@ -7,9 +7,12 @@
|
||||
//! in full rather than rendered on demand, so no window or message loop is
|
||||
//! needed and the list survives after `CloseClipboard`.
|
||||
//!
|
||||
//! Other platforms keep file lists elsewhere (`public.file-url` on macOS,
|
||||
//! `text/uri-list` on X11) and are not wired up: [`files`] is empty and
|
||||
//! [`set_files`] fails there.
|
||||
//! macOS keeps one `public.file-url` item per file on the general
|
||||
//! `NSPasteboard`; `NSURL` reads and writes them, so Finder's copies paste here
|
||||
//! and ours paste in Finder.
|
||||
//!
|
||||
//! Other platforms (`text/uri-list` on X11) are not wired up: [`files`] is
|
||||
//! empty and [`set_files`] fails there.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -218,7 +221,78 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[cfg(target_os = "macos")]
|
||||
mod imp {
|
||||
use objc2::ClassType;
|
||||
use objc2::rc::{Retained, autoreleasepool};
|
||||
use objc2::runtime::{AnyClass, AnyObject, ProtocolObject};
|
||||
use objc2_app_kit::{
|
||||
NSPasteboard, NSPasteboardReadingOptionKey, NSPasteboardURLReadingFileURLsOnlyKey, NSPasteboardWriting,
|
||||
};
|
||||
use objc2_foundation::{NSArray, NSDictionary, NSNumber, NSURL};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// `NSURL` as the one class to read, limited to file URLs: a web link the
|
||||
/// user copied is not a file to paste.
|
||||
fn read_file_urls<R>(
|
||||
f: impl FnOnce(&NSPasteboard, &NSArray<AnyClass>, &NSDictionary<NSPasteboardReadingOptionKey, AnyObject>) -> R,
|
||||
) -> R {
|
||||
autoreleasepool(|_| {
|
||||
let board = NSPasteboard::generalPasteboard();
|
||||
let classes = NSArray::from_slice(&[NSURL::class()]);
|
||||
let yes = NSNumber::new_bool(true);
|
||||
// SAFETY: the key is an AppKit constant, valid for the process' lifetime.
|
||||
let key = unsafe { NSPasteboardURLReadingFileURLsOnlyKey };
|
||||
let options = NSDictionary::from_slices(&[key], &[yes.as_ref() as &AnyObject]);
|
||||
f(&board, &classes, &options)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_files() -> bool {
|
||||
// SAFETY: the class list holds `NSURL`, which reads from pasteboards,
|
||||
// and the option takes an `NSNumber`.
|
||||
read_file_urls(|board, classes, options| unsafe {
|
||||
board.canReadObjectForClasses_options(classes, Some(options))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn files() -> Vec<PathBuf> {
|
||||
read_file_urls(|board, classes, options| {
|
||||
// SAFETY: as in `has_files`; every object read for `NSURL` is one.
|
||||
let Some(objects) = (unsafe { board.readObjectsForClasses_options(classes, Some(options)) }) else {
|
||||
return Vec::new();
|
||||
};
|
||||
objects
|
||||
.iter()
|
||||
.filter_map(|object| object.downcast::<NSURL>().ok())
|
||||
.filter_map(|url| url.to_file_path())
|
||||
// Finder ends a folder's URL in `/`; the project view names it without.
|
||||
.map(|path| path.components().collect())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_files(paths: &[PathBuf]) -> bool {
|
||||
autoreleasepool(|_| {
|
||||
// Folders get a directory URL (ending in `/`), as Finder writes them.
|
||||
let urls: Vec<Retained<NSURL>> = paths
|
||||
.iter()
|
||||
.filter_map(|p| if p.is_dir() { NSURL::from_directory_path(p) } else { NSURL::from_file_path(p) })
|
||||
.collect();
|
||||
if urls.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let writers: Vec<&ProtocolObject<dyn NSPasteboardWriting>> =
|
||||
urls.iter().map(|url| ProtocolObject::from_ref(&**url)).collect();
|
||||
let board = NSPasteboard::generalPasteboard();
|
||||
// Replacing the list also drops the text the user copied before.
|
||||
board.clearContents();
|
||||
board.writeObjects(&NSArray::from_slice(&writers))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "macos")))]
|
||||
mod imp {
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
Reference in new issue
Block a user