Skip to content

Commit

Permalink
io/transfer,app: [macOS] add support for file opening events
Browse files Browse the repository at this point in the history
Correctly handle macOS file opening events so that an app containing a
binary using gio can be used to open certain files and even be set as
default app for certain file extensions.
This is done via implementing application:openFile in GioAppDelegate.

File events issued this way will be made available to a gio through the
io/transfer package. This package now also contains documentation for
how to register a macOS app as able to support certain file extensions.
  • Loading branch information
lzambarda committed Sep 22, 2023
1 parent 27193ae commit df18024
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 8 deletions.
42 changes: 42 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"
"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 @@ -261,6 +266,10 @@ var viewMap = make(map[C.CFTypeRef]*window)
// launched is closed when applicationDidFinishLaunching is called.
var launched = make(chan struct{})

// openFiles captures all the openFile events happening
// applicationDidFinishLaunching is called.
var openFiles []string

// nextTopLeft is the offset to use for the next window's call to
// cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint
Expand Down Expand Up @@ -844,6 +853,20 @@ func gio_onFinishLaunching() {
close(launched)
}

//export gio_openFile
func gio_openFile(cfile C.CFTypeRef) {
file := nsstringToString(cfile)
if len(viewMap) == 0 {
// Must do this because openFile is called before
// applicationDidFinishLaunching therefore no view is available here.
openFiles = append(openFiles, file)
return
}
for _, w := range viewMap {
sendOpenFileEvent(w, file)
}
}

func newWindow(win *callbacks, options []Option) error {
<-launched
errch := make(chan error)
Expand All @@ -869,10 +892,29 @@ func newWindow(win *callbacks, options []Option) error {
C.makeKeyAndOrderFront(window)
layer := C.layerForView(w.view)
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
// Now send DataTransfer events for any file which has been used to open
// this application from the OS.
for _, file := range openFiles {
sendOpenFileEvent(w, file)
}
})
return <-errch
}

func sendOpenFileEvent(w *window, file string) {
mimeType := mime.TypeByExtension(path.Ext(file))
if mimeType == "" {
mimeType = "application/octet-stream"
}
w.w.Event(transfer.DataEvent{
Type: mimeType,
URI: file,
Open: func() (io.ReadCloser, error) {
return os.Open(file)
},
})
}

func newOSWindow() (*window, error) {
view := C.gio_createView()
if view == 0 {
Expand Down
4 changes: 4 additions & 0 deletions app/os_macos.m
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ - (void)applicationDidHide:(NSNotification *)aNotification {
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onAppShow();
}
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
gio_openFile((__bridge CFTypeRef)filename);
return YES;
}
@end

void gio_main() {
Expand Down
4 changes: 4 additions & 0 deletions app/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,10 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
if handled {
w.setNextFrame(time.Time{})
w.updateAnimation(d)
} else {
// Currently this can only happen with open file OS directives on
// macOS.
w.out <- e2
}
return handled
}
Expand Down
4 changes: 2 additions & 2 deletions io/router/pointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -872,9 +872,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
8 changes: 6 additions & 2 deletions io/router/pointer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -982,8 +982,12 @@ func TestTransfer(t *testing.T) {
if got, want := dataEvent.Type, "file"; got != want {
t.Fatalf("got %s; want %s", got, want)
}
if got, want := dataEvent.Open(), ofr; got != want {
t.Fatalf("got %v; want %v", got, want)
gotReader, gotErr := dataEvent.Open()
if gotErr != nil {
t.Fatalf("got %s, expected nil", gotErr)
}
if gotReader != ofr {
t.Fatalf("got %v; want %v", gotReader, ofr)
}

// Drag and drop complete.
Expand Down
14 changes: 11 additions & 3 deletions io/transfer/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
// with the source. When a drag gesture completes, a CancelEvent is sent
// to the source and all potential targets.
//
// DataEvent is also sent when the application is asked by the operating system
// to open one or more files.
//
// Note that the RequestEvent is sent to the source upon drop.
package transfer

Expand Down Expand Up @@ -100,10 +103,15 @@ func (CancelEvent) ImplementsEvent() {}
// DataEvent is sent to the target receiving the transfer.
type DataEvent struct {
// Type is the MIME type of Data.
// Type will be "application/octet-stream" for unknown data types.
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
// URI is the identifier of the resource being transferred. It can be set if
// the DataEvent concerns a file or an online resource.
URI 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, error)
}

func (DataEvent) ImplementsEvent() {}
6 changes: 5 additions & 1 deletion widget/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ func ExampleDraggable_Layout() {
for _, ev := range gtx.Events(&drop) {
switch e := ev.(type) {
case transfer.DataEvent:
data := e.Open()
data, err := e.Open()
if err != nil {
fmt.Println("DataEvent Open error:", err)
break
}
fmt.Println(data.(offer).Data)
}
}
Expand Down

0 comments on commit df18024

Please sign in to comment.