Skip to content
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

macOS: external drag and drop support #111

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/os_macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ package app
import (
"errors"
"image"
"io"
"mime"
"os"
"path/filepath"
"runtime"
"time"
"unicode"
Expand All @@ -18,6 +22,7 @@ import (
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit"

_ "gioui.org/internal/cocoainit"
Expand Down Expand Up @@ -557,6 +562,22 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
})
}

//export gio_onExternalDrop
func gio_onExternalDrop(view C.CFTypeRef, path *C.char) {
fileUrl := C.GoString(path)
w := mustView(view)

fileExtension := filepath.Ext(fileUrl)
mime := mime.TypeByExtension(fileExtension)
StarHack marked this conversation as resolved.
Show resolved Hide resolved

w.w.Event(transfer.DataEvent{
Type: mime,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a new field in the event that includes the filename/URL please?

In my app, I would like to use the filename and do not require access to the file content when the DnD event happens. This kind of logic is impossible when the event only contains the Open function, and it's especially weird since the filename is there, the framework just isn't providing it....

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation triggers an event for every file dropped so providing multiple Files at once probably would be inconsistent as the Open function still only provides access to one file. But I agree that including the (file) URL would make sense, or at least the filename, as Gio users might need them depending on their implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I posted another API idea specifically adding files in the main review comment. Let's see what @eliasnaur has to say about it...

Copy link
Sponsor Contributor

@inkeliz inkeliz Apr 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think nothing prevents you from getting the filepath:

evt  :=  transfer.DataEvent{} // Received on main-loop

file, _ := evt.Open()

var filepath string
if f, ok := file.(*os.File); ok {
     filepath = f.Name()
}

That works because Open() returns os.File in the current implementation. However, that is not guaranteed on other OSes, such as WASM.

Open: func() (io.ReadCloser, error) {
return os.Open(fileUrl)
},
})
}

//export gio_onDraw
func gio_onDraw(view C.CFTypeRef) {
w := mustView(view)
Expand Down
15 changes: 15 additions & 0 deletions app/os_macos.m
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ - (void)mouseMoved:(NSEvent *)event {
- (void)mouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
-(NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender
{
return NSDragOperationCopy;
}
- (void)draggingEnded:(id <NSDraggingInfo>)sender
{
NSPasteboard* pbrd = [sender draggingPasteboard];
NSArray* droppedFiles = [pbrd propertyListForType:NSFilenamesPboardType];

for (NSString* filePath in droppedFiles) {
NSURL* url = [NSURL fileURLWithPath:filePath];
gio_onExternalDrop((__bridge CFTypeRef)self, (char*)[[url path] UTF8String]);
}
}
- (void)scrollWheel:(NSEvent *)event {
CGFloat dx = -event.scrollingDeltaX;
CGFloat dy = -event.scrollingDeltaY;
Expand Down Expand Up @@ -366,6 +380,7 @@ CFTypeRef gio_createView(void) {
@autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame];
[view registerForDraggedTypes: [NSArray arrayWithObjects:NSTIFFPboardType, NSFilenamesPboardType, nil]];
view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
return CFBridgingRetain(view);
Expand Down
18 changes: 11 additions & 7 deletions io/router/pointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,14 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
p.entered = append(p.entered[:0], hits...)
}

func (q *pointerQueue) notifyPotentialTargets(src *pointerHandler, events *handlerEvents, event event.Event) {
for k, tgt := range q.handlers {
if _, ok := firstMimeMatch(src, tgt); ok {
events.Add(k, event)
}
}
}

func (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) {
if p.dataSource != nil {
return
Expand All @@ -814,11 +822,7 @@ func (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) {
// One data source handler per pointer.
p.dataSource = k
// Notify all potential targets.
for k, tgt := range q.handlers {
if _, ok := firstMimeMatch(src, tgt); ok {
events.Add(k, transfer.InitiateEvent{})
}
}
q.notifyPotentialTargets(src, events, transfer.InitiateEvent{})
break
}
}
Expand Down Expand Up @@ -858,9 +862,9 @@ func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerE
transferIdx := len(q.transfers)
events.Add(p.dataTarget, transfer.DataEvent{
Type: src.offeredMime,
Open: func() io.ReadCloser {
Open: func() (io.ReadCloser, error) {
q.transfers[transferIdx] = nil
return src.data
return src.data, nil
},
})
q.transfers = append(q.transfers, src.data)
Expand Down
2 changes: 2 additions & 0 deletions io/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ func (q *Router) Queue(events ...event.Event) bool {
}
case clipboard.Event:
q.cqueue.Push(e, &q.handlers)
case transfer.DataEvent:
q.pointer.queue.notifyPotentialTargets(&pointerHandler{sourceMimes: []string{e.Type}}, &q.handlers, e)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is quite right: as written, notifyPotentialTargets still gives every handler the event. It's made for transfer.InitiateEvent that tells every potential handler a change to change appearance in anticipation of a drop.

}
}
return q.handlers.HadEvents()
Expand Down
2 changes: 1 addition & 1 deletion io/transfer/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ type DataEvent struct {
Type string
// Open returns the transfer data. It is only valid to call Open in the frame
// the DataEvent is received. The caller must close the return value after use.
Open func() io.ReadCloser
Open func() (io.ReadCloser, error)
}

func (DataEvent) ImplementsEvent() {}