Skip to content

Commit a0259de

Browse files
committed
feat(macOS): add context menu functionality for file operations in Finder
1 parent c39a53f commit a0259de

File tree

1 file changed

+249
-95
lines changed

1 file changed

+249
-95
lines changed

wox.core/util/nativecontextmenu/contextmenu_darwin.go

Lines changed: 249 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,103 @@ package nativecontextmenu
77
#import <Cocoa/Cocoa.h>
88
#import <Foundation/Foundation.h>
99
10+
@interface FileMenuHandler : NSObject
11+
@property (strong) NSURL *fileURL;
12+
@property (strong) NSString *filePath;
13+
- (void)openFile:(id)sender;
14+
- (void)showInFinder:(id)sender;
15+
- (void)moveToTrash:(id)sender;
16+
- (void)getInfo:(id)sender;
17+
- (void)renameFile:(id)sender;
18+
- (void)compressFile:(id)sender;
19+
- (void)duplicateFile:(id)sender;
20+
- (void)makeAlias:(id)sender;
21+
- (void)quickLook:(id)sender;
22+
- (void)copyFile:(id)sender;
23+
- (void)copyPath:(id)sender;
24+
@end
25+
26+
@implementation FileMenuHandler
27+
- (void)openFile:(id)sender {
28+
[[NSWorkspace sharedWorkspace] openURL:self.fileURL];
29+
}
30+
31+
- (void)showInFinder:(id)sender {
32+
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[self.fileURL]];
33+
}
34+
35+
- (void)moveToTrash:(id)sender {
36+
[[NSFileManager defaultManager] trashItemAtURL:self.fileURL resultingItemURL:nil error:nil];
37+
}
38+
39+
- (void)getInfo:(id)sender {
40+
// Use AppleScript to show Get Info window
41+
NSString *script = [NSString stringWithFormat:@"tell application \"Finder\" to open information window of (POSIX file \"%@\" as alias)", self.filePath];
42+
NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script];
43+
[appleScript executeAndReturnError:nil];
44+
}
45+
46+
- (void)renameFile:(id)sender {
47+
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[self.fileURL]];
48+
// Trigger rename via AppleScript
49+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
50+
NSString *script = @"tell application \"System Events\" to tell process \"Finder\" to keystroke return";
51+
NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script];
52+
[appleScript executeAndReturnError:nil];
53+
});
54+
}
55+
56+
- (void)compressFile:(id)sender {
57+
NSTask *task = [[NSTask alloc] init];
58+
[task setLaunchPath:@"/usr/bin/ditto"];
59+
[task setArguments:@[@"-c", @"-k", @"--sequesterRsrc", @"--keepParent", self.filePath, [NSString stringWithFormat:@"%@.zip", self.filePath]]];
60+
[task launch];
61+
}
62+
63+
- (void)duplicateFile:(id)sender {
64+
NSFileManager *fm = [NSFileManager defaultManager];
65+
NSString *directory = [self.filePath stringByDeletingLastPathComponent];
66+
NSString *filename = [[self.filePath lastPathComponent] stringByDeletingPathExtension];
67+
NSString *extension = [self.filePath pathExtension];
68+
NSString *newPath = [NSString stringWithFormat:@"%@/%@ copy.%@", directory, filename, extension];
69+
70+
int counter = 1;
71+
while ([fm fileExistsAtPath:newPath]) {
72+
newPath = [NSString stringWithFormat:@"%@/%@ copy %d.%@", directory, filename, counter++, extension];
73+
}
74+
75+
[fm copyItemAtPath:self.filePath toPath:newPath error:nil];
76+
}
77+
78+
- (void)makeAlias:(id)sender {
79+
NSString *directory = [self.filePath stringByDeletingLastPathComponent];
80+
NSString *aliasPath = [NSString stringWithFormat:@"%@/%@ alias", directory, [self.filePath lastPathComponent]];
81+
[[NSFileManager defaultManager] createSymbolicLinkAtPath:aliasPath withDestinationPath:self.filePath error:nil];
82+
}
83+
84+
- (void)quickLook:(id)sender {
85+
[[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[self.fileURL]];
86+
// Trigger Quick Look via keystroke
87+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
88+
NSString *script = @"tell application \"System Events\" to keystroke \" \"";
89+
NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script];
90+
[appleScript executeAndReturnError:nil];
91+
});
92+
}
93+
94+
- (void)copyFile:(id)sender {
95+
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
96+
[pasteboard clearContents];
97+
[pasteboard writeObjects:@[self.fileURL]];
98+
}
99+
100+
- (void)copyPath:(id)sender {
101+
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
102+
[pasteboard clearContents];
103+
[pasteboard setString:self.filePath forType:NSPasteboardTypeString];
104+
}
105+
@end
106+
10107
// ShowContextMenu displays the macOS context menu for a file or folder
11108
// Returns 0 on success, non-zero on error
12109
int ShowContextMenu(const char* path) {
@@ -22,107 +119,164 @@ int ShowContextMenu(const char* path) {
22119
return 2;
23120
}
24121
25-
// Get the mouse location
26-
NSPoint mouseLocation = [NSEvent mouseLocation];
122+
__block int result = 0;
27123
28-
// Find the screen containing the mouse
29-
NSScreen *targetScreen = nil;
30-
for (NSScreen *screen in [NSScreen screens]) {
31-
if (NSPointInRect(mouseLocation, [screen frame])) {
32-
targetScreen = screen;
33-
break;
34-
}
35-
}
124+
// All UI operations must be performed on the main thread
125+
dispatch_sync(dispatch_get_main_queue(), ^{
126+
@autoreleasepool {
127+
// Get the mouse location
128+
NSPoint mouseLocation = [NSEvent mouseLocation];
36129
37-
if (!targetScreen) {
38-
targetScreen = [NSScreen mainScreen];
39-
}
130+
// Find the screen containing the mouse
131+
NSScreen *targetScreen = nil;
132+
for (NSScreen *screen in [NSScreen screens]) {
133+
if (NSPointInRect(mouseLocation, [screen frame])) {
134+
targetScreen = screen;
135+
break;
136+
}
137+
}
138+
139+
if (!targetScreen) {
140+
targetScreen = [NSScreen mainScreen];
141+
}
142+
143+
// Create a minimal window at the mouse position
144+
NSRect windowFrame = NSMakeRect(mouseLocation.x, mouseLocation.y, 1, 1);
145+
NSWindow *window = [[NSWindow alloc] initWithContentRect:windowFrame
146+
styleMask:NSWindowStyleMaskBorderless
147+
backing:NSBackingStoreBuffered
148+
defer:NO];
149+
[window setLevel:NSPopUpMenuWindowLevel];
150+
[window setOpaque:NO];
151+
[window setBackgroundColor:[NSColor clearColor]];
152+
[window makeKeyAndOrderFront:nil];
153+
154+
// Get file URL and attributes
155+
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
156+
NSDictionary *attributes = [fileManager attributesOfItemAtPath:filePath error:nil];
157+
BOOL isDirectory = [[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeDirectory];
158+
BOOL isAppBundle = isDirectory && [filePath hasSuffix:@".app"];
159+
160+
// Create handler
161+
FileMenuHandler *handler = [[FileMenuHandler alloc] init];
162+
handler.fileURL = fileURL;
163+
handler.filePath = filePath;
164+
165+
// Create the context menu
166+
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
167+
[menu setAutoenablesItems:NO];
168+
169+
// Open
170+
NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:@"Open"
171+
action:@selector(openFile:)
172+
keyEquivalent:@""];
173+
[openItem setTarget:handler];
174+
[openItem setEnabled:YES];
175+
[menu addItem:openItem];
176+
177+
// Show Package Contents (for .app bundles)
178+
if (isAppBundle) {
179+
NSMenuItem *showPackageItem = [[NSMenuItem alloc] initWithTitle:@"Show Package Contents"
180+
action:@selector(showInFinder:)
181+
keyEquivalent:@""];
182+
[showPackageItem setTarget:handler];
183+
[showPackageItem setEnabled:YES];
184+
[menu addItem:showPackageItem];
185+
}
186+
187+
[menu addItem:[NSMenuItem separatorItem]];
188+
189+
// Move to Trash
190+
NSMenuItem *trashItem = [[NSMenuItem alloc] initWithTitle:@"Move to Trash"
191+
action:@selector(moveToTrash:)
192+
keyEquivalent:@""];
193+
[trashItem setTarget:handler];
194+
[trashItem setEnabled:YES];
195+
[menu addItem:trashItem];
40196
41-
// Convert mouse location to window coordinates
42-
// macOS uses bottom-left origin, so we need to flip Y coordinate
43-
CGFloat screenHeight = [targetScreen frame].size.height;
44-
NSPoint windowPoint = NSMakePoint(mouseLocation.x, screenHeight - mouseLocation.y);
45-
46-
// Create a minimal window at the mouse position
47-
NSRect windowFrame = NSMakeRect(mouseLocation.x, mouseLocation.y, 1, 1);
48-
NSWindow *window = [[NSWindow alloc] initWithContentRect:windowFrame
49-
styleMask:NSWindowStyleMaskBorderless
50-
backing:NSBackingStoreBuffered
51-
defer:NO];
52-
[window setLevel:NSPopUpMenuWindowLevel];
53-
[window setOpaque:NO];
54-
[window setBackgroundColor:[NSColor clearColor]];
55-
[window makeKeyAndOrderFront:nil];
56-
57-
// Create the context menu
58-
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
59-
[menu setAutoenablesItems:YES];
60-
61-
// Get file URL
62-
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
63-
64-
// Add "Open" menu item
65-
NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:@"Open"
66-
action:@selector(openFile:)
67-
keyEquivalent:@""];
68-
[openItem setRepresentedObject:fileURL];
69-
[openItem setTarget:[[NSWorkspace sharedWorkspace] class]];
70-
[menu addItem:openItem];
71-
72-
// Add "Show in Finder" menu item
73-
NSMenuItem *showInFinderItem = [[NSMenuItem alloc] initWithTitle:@"Show in Finder"
74-
action:@selector(revealInFinder:)
197+
[menu addItem:[NSMenuItem separatorItem]];
198+
199+
// Get Info
200+
NSMenuItem *infoItem = [[NSMenuItem alloc] initWithTitle:@"Get Info"
201+
action:@selector(getInfo:)
75202
keyEquivalent:@""];
76-
[showInFinderItem setRepresentedObject:fileURL];
77-
[showInFinderItem setTarget:[[NSWorkspace sharedWorkspace] class]];
78-
[menu addItem:showInFinderItem];
79-
80-
[menu addItem:[NSMenuItem separatorItem]];
81-
82-
// Add "Get Info" menu item
83-
NSMenuItem *getInfoItem = [[NSMenuItem alloc] initWithTitle:@"Get Info"
84-
action:@selector(showFileInfo:)
85-
keyEquivalent:@""];
86-
[getInfoItem setRepresentedObject:fileURL];
87-
[menu addItem:getInfoItem];
88-
89-
// Add "Quick Look" menu item
90-
NSMenuItem *quickLookItem = [[NSMenuItem alloc] initWithTitle:@"Quick Look"
91-
action:@selector(quickLook:)
92-
keyEquivalent:@""];
93-
[quickLookItem setRepresentedObject:fileURL];
94-
[menu addItem:quickLookItem];
95-
96-
[menu addItem:[NSMenuItem separatorItem]];
97-
98-
// Add "Copy" menu item
99-
NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy"
100-
action:@selector(copyFile:)
101-
keyEquivalent:@""];
102-
[copyItem setRepresentedObject:fileURL];
103-
[menu addItem:copyItem];
104-
105-
// Add "Move to Trash" menu item
106-
NSMenuItem *trashItem = [[NSMenuItem alloc] initWithTitle:@"Move to Trash"
107-
action:@selector(moveToTrash:)
108-
keyEquivalent:@""];
109-
[trashItem setRepresentedObject:fileURL];
110-
[menu addItem:trashItem];
111-
112-
// Display the menu
113-
// Convert screen coordinates to window coordinates
114-
NSPoint menuLocation = NSMakePoint(0, 0);
115-
116-
// Show the menu
117-
[menu popUpMenuPositioningItem:nil atLocation:menuLocation inView:[window contentView]];
118-
119-
// Keep the window alive while menu is shown
120-
// The menu will close automatically when user clicks outside or selects an item
121-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
122-
[window close];
203+
[infoItem setTarget:handler];
204+
[infoItem setEnabled:YES];
205+
[menu addItem:infoItem];
206+
207+
// Rename
208+
NSMenuItem *renameItem = [[NSMenuItem alloc] initWithTitle:@"Rename"
209+
action:@selector(renameFile:)
210+
keyEquivalent:@""];
211+
[renameItem setTarget:handler];
212+
[renameItem setEnabled:YES];
213+
[menu addItem:renameItem];
214+
215+
// Compress
216+
NSString *compressTitle = [NSString stringWithFormat:@"Compress \"%@\"", [filePath lastPathComponent]];
217+
NSMenuItem *compressItem = [[NSMenuItem alloc] initWithTitle:compressTitle
218+
action:@selector(compressFile:)
219+
keyEquivalent:@""];
220+
[compressItem setTarget:handler];
221+
[compressItem setEnabled:YES];
222+
[menu addItem:compressItem];
223+
224+
// Duplicate
225+
NSMenuItem *duplicateItem = [[NSMenuItem alloc] initWithTitle:@"Duplicate"
226+
action:@selector(duplicateFile:)
227+
keyEquivalent:@""];
228+
[duplicateItem setTarget:handler];
229+
[duplicateItem setEnabled:YES];
230+
[menu addItem:duplicateItem];
231+
232+
// Make Alias
233+
NSMenuItem *aliasItem = [[NSMenuItem alloc] initWithTitle:@"Make Alias"
234+
action:@selector(makeAlias:)
235+
keyEquivalent:@""];
236+
[aliasItem setTarget:handler];
237+
[aliasItem setEnabled:YES];
238+
[menu addItem:aliasItem];
239+
240+
// Quick Look
241+
NSMenuItem *quickLookItem = [[NSMenuItem alloc] initWithTitle:@"Quick Look"
242+
action:@selector(quickLook:)
243+
keyEquivalent:@""];
244+
[quickLookItem setTarget:handler];
245+
[quickLookItem setEnabled:YES];
246+
[menu addItem:quickLookItem];
247+
248+
[menu addItem:[NSMenuItem separatorItem]];
249+
250+
// Copy
251+
NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy"
252+
action:@selector(copyFile:)
253+
keyEquivalent:@""];
254+
[copyItem setTarget:handler];
255+
[copyItem setEnabled:YES];
256+
[menu addItem:copyItem];
257+
258+
// Copy Path
259+
NSMenuItem *copyPathItem = [[NSMenuItem alloc] initWithTitle:@"Copy Path"
260+
action:@selector(copyPath:)
261+
keyEquivalent:@""];
262+
[copyPathItem setTarget:handler];
263+
[copyPathItem setEnabled:YES];
264+
[menu addItem:copyPathItem];
265+
266+
// Display the menu at mouse location
267+
NSPoint menuLocation = NSMakePoint(0, 0);
268+
[menu popUpMenuPositioningItem:nil atLocation:menuLocation inView:[window contentView]];
269+
270+
// Keep the window and handler alive while menu is shown
271+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
272+
[window close];
273+
// Keep handler alive
274+
(void)handler;
275+
});
276+
}
123277
});
124278
125-
return 0;
279+
return result;
126280
}
127281
}
128282

0 commit comments

Comments
 (0)