From 94e1ec91adb2969504edd440a498ce256ab91b9f Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Fri, 23 Jun 2023 17:29:49 -0500 Subject: [PATCH 01/17] Revert "Merge branch 'v3-alpha_linux' into v3-alpha" This reverts commit b317efaf2c1f253fe17c8c7a7ae7413faf59e664, reversing changes made to 29b9c5200fd69f08a4814a49ff4d0fbc2d6cffbc. --- .../webview/request_linux_purego.go | 96 --- .../webview/responsewriter_linux_purego.go | 175 ---- .../assetserver/webview/webkit2_36+_purego.go | 94 --- .../assetserver/webview/webkit2_40+_purego.go | 74 -- .../webview/webkit2_legacy_purego.go | 36 - v3/STATUS.md | 112 +-- v3/go.mod | 3 - v3/go.sum | 2 - v3/pkg/application/application.go | 4 +- v3/pkg/application/application_linux.go | 268 ------ .../application/application_linux_purego.go | 253 ------ v3/pkg/application/clipboard_linux.go | 33 - v3/pkg/application/dialogs_linux.go | 300 ------- v3/pkg/application/dialogs_linux_purego.go | 220 ----- v3/pkg/application/mainthread_linux.go | 51 -- v3/pkg/application/mainthread_linux_purego.go | 30 - v3/pkg/application/menu_linux.go | 200 ----- v3/pkg/application/menu_linux_purego.go | 179 ---- v3/pkg/application/menuitem.go | 4 +- v3/pkg/application/menuitem_linux.go | 407 --------- v3/pkg/application/menuitem_linux_purego.go | 403 --------- v3/pkg/application/messageprocessor_call.go | 7 +- v3/pkg/application/messageprocessor_dialog.go | 7 +- v3/pkg/application/options_linux.go | 6 - v3/pkg/application/screen_linux.go | 94 --- v3/pkg/application/screen_linux_purego.go | 92 -- v3/pkg/application/systemtray_darwin.go | 4 - v3/pkg/application/systemtray_linux.go | 90 -- v3/pkg/application/webview_window.go | 34 - v3/pkg/application/webview_window_linux.go | 797 ------------------ .../webview_window_linux_purego.go | 764 ----------------- 31 files changed, 69 insertions(+), 4770 deletions(-) delete mode 100644 v2/pkg/assetserver/webview/request_linux_purego.go delete mode 100644 v2/pkg/assetserver/webview/responsewriter_linux_purego.go delete mode 100644 v2/pkg/assetserver/webview/webkit2_36+_purego.go delete mode 100644 v2/pkg/assetserver/webview/webkit2_40+_purego.go delete mode 100644 v2/pkg/assetserver/webview/webkit2_legacy_purego.go delete mode 100644 v3/pkg/application/application_linux.go delete mode 100644 v3/pkg/application/application_linux_purego.go delete mode 100644 v3/pkg/application/clipboard_linux.go delete mode 100644 v3/pkg/application/dialogs_linux.go delete mode 100644 v3/pkg/application/dialogs_linux_purego.go delete mode 100644 v3/pkg/application/mainthread_linux.go delete mode 100644 v3/pkg/application/mainthread_linux_purego.go delete mode 100644 v3/pkg/application/menu_linux.go delete mode 100644 v3/pkg/application/menu_linux_purego.go delete mode 100644 v3/pkg/application/menuitem_linux.go delete mode 100644 v3/pkg/application/menuitem_linux_purego.go delete mode 100644 v3/pkg/application/options_linux.go delete mode 100644 v3/pkg/application/screen_linux.go delete mode 100644 v3/pkg/application/screen_linux_purego.go delete mode 100644 v3/pkg/application/systemtray_linux.go delete mode 100644 v3/pkg/application/webview_window_linux.go delete mode 100644 v3/pkg/application/webview_window_linux_purego.go diff --git a/v2/pkg/assetserver/webview/request_linux_purego.go b/v2/pkg/assetserver/webview/request_linux_purego.go deleted file mode 100644 index 03e3a5ca0..000000000 --- a/v2/pkg/assetserver/webview/request_linux_purego.go +++ /dev/null @@ -1,96 +0,0 @@ -//go:build linux && purego -// +build linux,purego - -package webview - -import ( - "fmt" - "io" - "net/http" - - "github.com/ebitengine/purego" -) - -// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest` -// -// Please make sure to call Release() when finished using the request. -func NewRequest(webKitURISchemeRequest uintptr) Request { - webkitReq := webKitURISchemeRequest - req := &request{req: webkitReq} - req.AddRef() - return req -} - -var _ Request = &request{} - -type request struct { - req uintptr - - header http.Header - body io.ReadCloser - rw *responseWriter -} - -func (r *request) AddRef() error { - var objectRef func(uintptr) - purego.RegisterLibFunc(&objectRef, gtk, "g_object_ref") - objectRef(r.req) - return nil -} - -func (r *request) Release() error { - var objectUnref func(uintptr) - purego.RegisterLibFunc(&objectUnref, gtk, "g_object_unref") - objectUnref(r.req) - return nil -} - -func (r *request) URL() (string, error) { - var getUri func(uintptr) string - purego.RegisterLibFunc(&getUri, webkit, "webkit_uri_scheme_request_get_uri") - return getUri(r.req), nil -} - -func (r *request) Method() (string, error) { - return webkit_uri_scheme_request_get_http_method(r.req), nil -} - -func (r *request) Header() (http.Header, error) { - if r.header != nil { - return r.header, nil - } - - r.header = webkit_uri_scheme_request_get_http_headers(r.req) - return r.header, nil -} - -func (r *request) Body() (io.ReadCloser, error) { - if r.body != nil { - return r.body, nil - } - - // WebKit2GTK has currently no support for request bodies. - r.body = http.NoBody - - return r.body, nil -} - -func (r *request) Response() ResponseWriter { - fmt.Println("r.Response()") - if r.rw != nil { - return r.rw - } - - r.rw = &responseWriter{req: r.req} - return r.rw -} - -func (r *request) Close() error { - var err error - if r.body != nil { - err = r.body.Close() - } - r.Response().Finish() - r.Release() - return err -} diff --git a/v2/pkg/assetserver/webview/responsewriter_linux_purego.go b/v2/pkg/assetserver/webview/responsewriter_linux_purego.go deleted file mode 100644 index 8f5f1a57f..000000000 --- a/v2/pkg/assetserver/webview/responsewriter_linux_purego.go +++ /dev/null @@ -1,175 +0,0 @@ -//go:build linux && purego -// +build linux,purego - -package webview - -import ( - "fmt" - "io" - "net/http" - "os" - "strconv" - "syscall" - - "github.com/ebitengine/purego" -) - -const ( - gtk3 = "libgtk-3.so" - gtk4 = "libgtk-4.so" -) - -var ( - gtk uintptr - webkit uintptr - version int -) - -func init() { - var err error - // gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) - // if err == nil { - // version = 4 - // return - // } - // log.Println("Failed to open GTK4: Falling back to GTK3") - gtk, err = purego.Dlopen(gtk3, purego.RTLD_NOW|purego.RTLD_GLOBAL) - if err != nil { - panic(err) - } - version = 3 - - var webkit4 string = "libwebkit2gtk-4.1.so" - webkit, err = purego.Dlopen(webkit4, purego.RTLD_NOW|purego.RTLD_GLOBAL) - if err != nil { - panic(err) - } -} - -type responseWriter struct { - req uintptr - - header http.Header - wroteHeader bool - finished bool - - w io.WriteCloser - wErr error -} - -func (rw *responseWriter) Header() http.Header { - if rw.header == nil { - rw.header = http.Header{} - } - return rw.header -} - -func (rw *responseWriter) Write(buf []byte) (int, error) { - if rw.finished { - return 0, errResponseFinished - } - - rw.WriteHeader(http.StatusOK) - if rw.wErr != nil { - return 0, rw.wErr - } - return rw.w.Write(buf) -} - -func (rw *responseWriter) WriteHeader(code int) { - // TODO? Is this ever called? I don't think so! - if rw.wroteHeader || rw.finished { - return - } - rw.wroteHeader = true - - contentLength := int64(-1) - if sLen := rw.Header().Get(HeaderContentLength); sLen != "" { - if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 { - contentLength = pLen - } - } - fmt.Println("content_length", contentLength) - // We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the - // read FD is given to the InputStream and will be closed there. - // Furthermore we especially don't want to have the FD_CLOEXEC - rFD, w, err := pipe() - if err != nil { - rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err)) - return - } - rw.w = w - - var newStream func(int, bool) uintptr - purego.RegisterLibFunc(&newStream, gtk, "g_unix_input_stream_new") - var unRef func(uintptr) - purego.RegisterLibFunc(&unRef, gtk, "g_object_unref") - stream := newStream(rFD, true) - - /* var reqFinish func(uintptr, uintptr, uintptr, uintptr, int64) int - purego.RegisterLibFunc(&reqFinish, webkit, "webkit_uri_scheme_request_finish") - - header := rw.Header() - defer unRef(stream) - if err := reqFinish(rw.req, code, header, stream, contentLength); err != nil { - rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) - } - */ - if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil { - rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) - return - } -} - -func (rw *responseWriter) Finish() { - if !rw.wroteHeader { - rw.WriteHeader(http.StatusNotImplemented) - } - - if rw.finished { - return - } - rw.finished = true - if rw.w != nil { - rw.w.Close() - } -} - -func (rw *responseWriter) finishWithError(code int, err error) { - if rw.w != nil { - rw.w.Close() - rw.w = &nopCloser{io.Discard} - } - rw.wErr = err - - var newLiteral func(uint32, string, int, string) uintptr // is this correct? - purego.RegisterLibFunc(&newLiteral, gtk, "g_error_new_literal") - var newQuark func(string) uintptr - purego.RegisterLibFunc(&newQuark, gtk, "g_quark_from_string") - var freeError func(uintptr) - purego.RegisterLibFunc(&freeError, gtk, "g_error_free") - var finishError func(uintptr, uintptr) - purego.RegisterLibFunc(&finishError, webkit, "webkit_uri_scheme_request_finish_error") - - msg := string(err.Error()) - //gquark := newQuark(msg) - gerr := newLiteral(1, msg, code, msg) - finishError(rw.req, gerr) - freeError(gerr) -} - -type nopCloser struct { - io.Writer -} - -func (nopCloser) Close() error { return nil } - -func pipe() (r int, w *os.File, err error) { - var p [2]int - e := syscall.Pipe2(p[0:], 0) - if e != nil { - return 0, nil, fmt.Errorf("pipe2: %s", e) - } - - return p[0], os.NewFile(uintptr(p[1]), "|1"), nil -} diff --git a/v2/pkg/assetserver/webview/webkit2_36+_purego.go b/v2/pkg/assetserver/webview/webkit2_36+_purego.go deleted file mode 100644 index 2386868c3..000000000 --- a/v2/pkg/assetserver/webview/webkit2_36+_purego.go +++ /dev/null @@ -1,94 +0,0 @@ -//go:build linux && (webkit2_36 || webkit2_40) && purego - -package webview - -import ( - "net/http" - "strings" - - "github.com/ebitengine/purego" -) - -func webkit_uri_scheme_request_get_http_method(req uintptr) string { - var getMethod func(uintptr) string - purego.RegisterLibFunc(&getMethod, gtk, "webkit_uri_scheme_request_get_http_method") - return strings.ToUpper(getMethod(req)) -} - -func webkit_uri_scheme_request_get_http_headers(req uintptr) http.Header { - var getHeaders func(uintptr) uintptr - purego.RegisterLibFunc(&getUri, webkit, "webkit_uri_scheme_request_get_http_headers") - - hdrs := getHeaders(req) - - var headersIterInit func(uintptr, uintptr) uintptr - purego.RegisterLibFunc(&headersIterInit, gtk, "soup_message_headers_iter_init") - - // TODO: How do we get a struct? - /* - typedef struct { - SoupMessageHeaders *hdrs; - int index_common; - int index_uncommon; - } SoupMessageHeadersIterReal; - */ - iter := make([]byte, 12) - headersIterInit(&iter, hdrs) - - var iterNext func(uintptr, *string, *string) int - purego.RegisterLibFunc(&iterNext, gtk, "soup_message_headers_iter_next") - - var name string - var value string - h := http.Header{} - - for iterNext(&iter, &name, &value) != 0 { - h.Add(name, value) - } - - return h -} - -func webkit_uri_scheme_request_finish(req uintptr, code int, header http.Header, stream uintptr, streamLength int64) error { - - var newResponse func(uintptr, int64) string - purego.RegisterLibFunc(&newResponse, webkit, "webkit_uri_scheme_response_new") - var unRef func(uintptr) - purego.RegisterLibFunc(&unRef, gtk, "g_object_unref") - - resp := newResponse(stream, streamLength) - defer unRef(resp) - - var setStatus func(uintptr, int, string) - purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_status") - - setStatus(resp, code, cReason) - - var setContentType func(uintptr, string) - purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_content_type") - - setContentType(resp, header.Get(HeaderContentType)) - - soup := gtk - var soupHeadersNew func(int) uintptr - purego.RegisterLibFunc(&unRef, soup, "soup_message_headers_new") - var soupHeadersAppend func(uintptr, string, string) - purego.RegisterLibFunc(&unRef, soup, "soup_message_headers_append") - - hdrs := soupHeadersNew(SOUP_MESSAGE_HEADERS_RESPONSE) - for name, values := range header { - for _, value := range values { - soupHeadersAppend(hdrs, name, value) - } - } - - var setHttpHeaders func(uintptr, uintptr) - purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_http_headers") - - setHttpHeaders(resp, hdrs) - var finishWithResponse func(uintptr, uintptr) - purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_request_finish_with_response") - finishWithResponse(req, resp) - - return nil -} diff --git a/v2/pkg/assetserver/webview/webkit2_40+_purego.go b/v2/pkg/assetserver/webview/webkit2_40+_purego.go deleted file mode 100644 index 1088be25e..000000000 --- a/v2/pkg/assetserver/webview/webkit2_40+_purego.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build linux && webkit2_40 && purego - -package webview - -import ( - "fmt" - "io" - "net/http" - "unsafe" -) - -func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser { - stream := C.webkit_uri_scheme_request_get_http_body(req) - if stream == nil { - return http.NoBody - } - return &webkitRequestBody{stream: stream} -} - -type webkitRequestBody struct { - stream *C.GInputStream - closed bool -} - -// Read implements io.Reader -func (r *webkitRequestBody) Read(p []byte) (int, error) { - if r.closed { - return 0, io.ErrClosedPipe - } - - var content unsafe.Pointer - var contentLen int - if p != nil { - content = unsafe.Pointer(&p[0]) - contentLen = len(p) - } - - var n C.gsize - var gErr *C.GError - res := C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr) - if res == 0 { - return 0, formatGError("stream read failed", gErr) - } else if n == 0 { - return 0, io.EOF - } - return int(n), nil -} - -func (r *webkitRequestBody) Close() error { - if r.closed { - return nil - } - r.closed = true - - // https://docs.gtk.org/gio/method.InputStream.close.html - // Streams will be automatically closed when the last reference is dropped, but you might want to call this function - // to make sure resources are released as early as possible. - var err error - var gErr *C.GError - if C.g_input_stream_close(r.stream, nil, &gErr) == 0 { - err = formatGError("stream close failed", gErr) - } - C.g_object_unref(C.gpointer(r.stream)) - r.stream = nil - return err -} - -func formatGError(msg string, gErr *C.GError, args ...any) error { - if gErr != nil && gErr.message != nil { - msg += ": " + C.GoString(gErr.message) - C.g_error_free(gErr) - } - return fmt.Errorf(msg, args...) -} diff --git a/v2/pkg/assetserver/webview/webkit2_legacy_purego.go b/v2/pkg/assetserver/webview/webkit2_legacy_purego.go deleted file mode 100644 index 2e88864c8..000000000 --- a/v2/pkg/assetserver/webview/webkit2_legacy_purego.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build linux && !(webkit2_36 || webkit2_40) && purego - -package webview - -import ( - "fmt" - "io" - "net/http" - - "github.com/ebitengine/purego" -) - -const Webkit2MinMinorVersion = 0 - -func webkit_uri_scheme_request_get_http_method(_ uintptr) string { - return http.MethodGet -} - -func webkit_uri_scheme_request_get_http_headers(_ uintptr) http.Header { - return http.Header{} -} - -func webkit_uri_scheme_request_get_http_body(_ uintptr) io.ReadCloser { - return http.NoBody -} - -func webkit_uri_scheme_request_finish(req uintptr, code int, header http.Header, stream uintptr, streamLength int64) error { - if code != http.StatusOK { - return fmt.Errorf("StatusCodes not supported: %d - %s", code, http.StatusText(code)) - } - - var requestFinish func(uintptr, uintptr, int64, string) - purego.RegisterLibFunc(&requestFinish, webkit, "webkit_uri_scheme_request_finish") - requestFinish(req, stream, streamLength, header.Get(HeaderContentType)) - return nil -} diff --git a/v3/STATUS.md b/v3/STATUS.md index 528080052..1bfb6d08a 100644 --- a/v3/STATUS.md +++ b/v3/STATUS.md @@ -14,21 +14,21 @@ Status of features in v3. Incomplete - please add as you see fit. Application interface methods -| Method | Windows | Linux | Mac | Notes | -|---------------------------------------------------------------|---------|-------|-----|------------------------------| -| run() error | Y | Y | Y | | -| destroy() | | Y | Y | | -| setApplicationMenu(menu *Menu) | Y | | Y | | -| name() string | | | Y | | -| getCurrentWindowID() uint | Y | Y | Y | | -| showAboutDialog(name string, description string, icon []byte) | | Y | Y | [linux] No icon possible yet | -| setIcon(icon []byte) | - | | Y | | -| on(id uint) | | | Y | | -| dispatchOnMainThread(fn func()) | Y | Y | Y | | -| hide() | Y | | Y | | -| show() | Y | | Y | | -| getPrimaryScreen() (*Screen, error) | | Y | Y | | -| getScreens() ([]*Screen, error) | | Y | Y | | +| Method | Windows | Linux | Mac | Notes | +|---------------------------------------------------------------|---------|-------|-----|-------| +| run() error | Y | | Y | | +| destroy() | | | Y | | +| setApplicationMenu(menu *Menu) | Y | | Y | | +| name() string | | | Y | | +| getCurrentWindowID() uint | Y | | Y | | +| showAboutDialog(name string, description string, icon []byte) | | | Y | | +| setIcon(icon []byte) | - | | Y | | +| on(id uint) | | | Y | | +| dispatchOnMainThread(fn func()) | Y | | Y | | +| hide() | Y | | Y | | +| show() | Y | | Y | | +| getPrimaryScreen() (*Screen, error) | | | Y | | +| getScreens() ([]*Screen, error) | | | Y | | ## Webview Window @@ -90,7 +90,7 @@ Webview Window Interface Methods | Feature | Windows | Linux | Mac | Notes | |---------|---------|-------|-----|-------| -| Quit | Y | Y | Y | | +| Quit | Y | | Y | | | Hide | Y | | Y | | | Show | Y | | Y | | @@ -132,9 +132,9 @@ explicitly set with `--default-contextmenu: show`. | Feature | Windows | Linux | Mac | Notes | |------------|---------|-------|-----|-------| -| GetAll | Y | Y | Y | | -| GetPrimary | Y | Y | Y | | -| GetCurrent | Y | Y | Y | | +| GetAll | Y | | Y | | +| GetPrimary | Y | | Y | | +| GetCurrent | Y | | Y | | ### Window @@ -180,37 +180,39 @@ U = Untested A 'Y' in the table below indicates that the option has been tested and is applied when the window is created. An 'X' indicates that the option is not supported by the platform. -| Feature | Windows | Linux | Mac | Notes | -|---------------------------------|---------|-------|-----|---------------------------------------------------| -| Name | | | | | -| Title | Y | | | | -| Width | Y | Y | | | -| Height | Y | Y | | | -| AlwaysOnTop | Y | Y | | | -| URL | Y | | | | -| DisableResize | Y | Y | | | -| Frameless | Y | Y | | | -| MinWidth | Y | Y | | | -| MinHeight | Y | Y | | | -| MaxWidth | Y | Y | | | -| MaxHeight | Y | Y | | | -| StartState | Y | | | | -| Mac | - | - | | | -| BackgroundType | | | | Acrylic seems to work but the others don't | -| BackgroundColour | Y | Y | | | -| HTML | Y | Y | | | -| JS | Y | Y | | | -| CSS | Y | Y | | | -| X | Y | Y | | | -| Y | Y | Y | | | -| HideOnClose | Y | Y | | | -| FullscreenButtonEnabled | | ? | | [linux] How is this different from DisableResize? | -| Hidden | Y | | | | -| EnableFraudulentWebsiteWarnings | | | | | -| Zoom | | Y | | | -| EnableDragAndDrop | Y | Y | | | -| Windows | Y | - | - | | -| Focused | Y | | | | +| Feature | Windows | Linux | Mac | Notes | +|---------------------------------|---------|-------|-----|--------------------------------------------| +| AlwaysOnTop | Y | | | | +| BackgroundColour | Y | | | | +| BackgroundType | | | | Acrylic seems to work but the others don't | +| CSS | Y | | | | +| DevToolsEnabled | Y | | Y | | +| DisableResize | Y | | | | +| EnableDragAndDrop | | | | | +| EnableFraudulentWebsiteWarnings | | | | | +| Focused | Y | | | | +| Frameless | Y | | | | +| FullscreenButtonEnabled | Y | | | | +| Height | Y | | | | +| Hidden | Y | | | | +| HTML | Y | | | | +| JS | Y | | | | +| Mac | - | - | | | +| MaxHeight | Y | | | | +| MaxWidth | Y | | | | +| MinHeight | Y | | | | +| MinWidth | Y | | | | +| Name | Y | | | | +| OpenInspectorOnStartup | | | | | +| StartState | Y | | | | +| Title | Y | | | | +| URL | Y | | | | +| Width | Y | | | | +| Windows | Y | - | - | | +| X | Y | | | | +| Y | Y | | | | +| Zoom | | | | | +| ZoomControlEnabled | | | | | ### Log @@ -220,7 +222,7 @@ To log or not to log? System logger vs custom logger. | Event | Windows | Linux | Mac | Notes | |--------------------------|---------|-------|-----|-------| -| Default Application Menu | Y | Y | Y | | +| Default Application Menu | Y | | Y | | ## Tray Menus @@ -293,10 +295,10 @@ Built-in plugin support: | Plugin | Windows | Linux | Mac | Notes | |-----------------|---------|-------|-----|-------| | Browser | Y | | Y | | -| KV Store | Y | Y | Y | | -| Log | Y | Y | Y | | +| KV Store | Y | | Y | | +| Log | Y | | Y | | | Single Instance | Y | | Y | | -| SQLite | Y | Y | Y | | +| SQLite | Y | | Y | | | Start at login | | | Y | | | Server | | | | | @@ -381,4 +383,4 @@ Built-in plugin support: # Beta Release TODO -- [ ] Make better looking examples \ No newline at end of file +- [ ] Make better looking examples diff --git a/v3/go.mod b/v3/go.mod index 44e890a8b..ec8262c08 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -3,7 +3,6 @@ module github.com/wailsapp/wails/v3 go 1.19 require ( - github.com/ebitengine/purego v0.3.2 github.com/bep/debounce v1.2.1 github.com/go-ole/go-ole v1.2.6 github.com/go-task/task/v3 v3.20.0 @@ -76,5 +75,3 @@ require ( ) replace github.com/wailsapp/wails/v2 => ../v2 - -replace github.com/ebitengine/purego v0.3.2 => github.com/TotallyGamerJet/purego v0.2.0-alpha.0.20230404174033-5655abccca7e diff --git a/v3/go.sum b/v3/go.sum index b9f9f1b10..715d5133d 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -11,8 +11,6 @@ github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzX github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.1 h1:a9Fqx6vQrHQ4CyiaLhktfTTelwGotmFWy8MNhyaohw8= -github.com/TotallyGamerJet/purego v0.2.0-alpha.0.20230404174033-5655abccca7e h1:wQ7ot+e0mwJYkbomtIX9tU0dOV9lFTmwAgUGqvQTIUg= -github.com/TotallyGamerJet/purego v0.2.0-alpha.0.20230404174033-5655abccca7e/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index afb5898cb..4c8c3490d 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -333,9 +333,7 @@ func (a *App) error(message string, args ...any) { func (a *App) NewWebviewWindowWithOptions(windowOptions WebviewWindowOptions) *WebviewWindow { newWindow := NewWindow(windowOptions) id := newWindow.id - if a.windows == nil { - a.windows = make(map[uint]*WebviewWindow) - } + a.windowsLock.Lock() a.windows[id] = newWindow a.windowsLock.Unlock() diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go deleted file mode 100644 index 1bb5a3be5..000000000 --- a/v3/pkg/application/application_linux.go +++ /dev/null @@ -1,268 +0,0 @@ -//go:build linux && !purego - -package application - -/* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 - -#include -#include -#include -#include -#include -#include - -typedef struct App { - void *app; -} App; - -extern void processApplicationEvent(uint); - -extern void activateLinux(gpointer data); - -static void activate (GtkApplication* app, gpointer data) { - // FIXME: should likely emit a WAILS specific code - // events.Mac.EventApplicationDidFinishLaunching == 1032 - //processApplicationEvent(1032); - - activateLinux(data); -} - -static GtkApplication* init(char* name) { - return gtk_application_new(name, G_APPLICATION_DEFAULT_FLAGS); -} - -static int run(void *app, void *data) { - g_signal_connect (app, "activate", G_CALLBACK (activate), data); - g_application_hold(app); // allows it to run without a window - int status = g_application_run (G_APPLICATION (app), 0, NULL); - g_application_release(app); - g_object_unref (app); - return status; -} - -*/ -import "C" -import ( - "fmt" - "log" - "os" - "strings" - "sync" - "unsafe" - - "github.com/wailsapp/wails/v3/pkg/events" -) - -func init() { - // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings - _ = os.Setenv("GDK_BACKEND", "x11") -} - -type linuxApp struct { - application unsafe.Pointer - applicationMenu unsafe.Pointer - parent *App - - startupActions []func() - - // Native -> uint - windows map[*C.GtkWindow]uint - windowsLock sync.Mutex -} - -func getNativeApplication() *linuxApp { - return globalApplication.impl.(*linuxApp) -} - -func (m *linuxApp) hide() { - windows := C.gtk_application_get_windows((*C.GtkApplication)(m.application)) - for { - fmt.Println("hiding", windows.data) - C.gtk_widget_hide((*C.GtkWidget)(windows.data)) - windows = windows.next - if windows == nil { - return - } - } -} - -func (m *linuxApp) show() { - windows := C.gtk_application_get_windows((*C.GtkApplication)(m.application)) - for { - fmt.Println("hiding", windows.data) - C.gtk_widget_show_all((*C.GtkWidget)(windows.data)) - windows = windows.next - if windows == nil { - return - } - } -} - -func (m *linuxApp) on(eventID uint) { - log.Println("linuxApp.on()", eventID) - // TODO: Setup signal handling as appropriate - // Note: GTK signals seem to be strings! -} - -func (m *linuxApp) setIcon(icon []byte) { - /* // FIXME: WIP - loader := C.gdk_pixbuf_loader_new() - - if loader == nil { - return - } - - loaded := C.gdk_pixbuf_loader_write(loader, (*C.guchar)(&icon[0]), (C.gsize)(len(icon)), 0) - - if loaded == C.bool(1) && C.gdk_pixbuf_loader_close(loader, 0) { - pixbuf := C.gdk_pixbuf_loader_get_pixbuf(loader) - if pixbuf != nil { - ww := m.parent.CurrentWindow() - window := ww.impl.window - C.gtk_window_set_icon(window, pixbuf) - } - } - - C.g_object_unref(loader) - */ -} - -func (m *linuxApp) name() string { - // appName := C.getAppName() - // defer C.free(unsafe.Pointer(appName)) - // return C.GoString(appName) - return "" -} - -func (m *linuxApp) getCurrentWindowID() uint { - // TODO: Add extra metadata to window - window := (*C.GtkWindow)(C.gtk_application_get_active_window((*C.GtkApplication)(m.application))) - if window == nil { - return uint(1) - } - m.windowsLock.Lock() - defer m.windowsLock.Unlock() - identifier, ok := m.windows[window] - if ok { - return identifier - } - return uint(1) -} - -func (m *linuxApp) setApplicationMenu(menu *Menu) { - if menu == nil { - // Create a default menu - menu = defaultApplicationMenu() - } - globalApplication.dispatchOnMainThread(func() { - fmt.Println("setApplicationMenu") - - menu.Update() - m.applicationMenu = (menu.impl).(*linuxMenu).native - }) -} - -func (m *linuxApp) run() error { - - // Add a hook to the ApplicationDidFinishLaunching event - // FIXME: add Wails specific events - i.e. Shouldn't platform specific ones be translated to Wails events? - m.parent.On(events.Mac.ApplicationDidFinishLaunching, func() { - // Do we need to do anything now? - fmt.Println("events.Mac.ApplicationDidFinishLaunching received!") - }) - - var app C.App - app.app = unsafe.Pointer(m) - C.run(m.application, m.application) - return nil -} - -func (m *linuxApp) destroy() { - C.g_application_quit((*C.GApplication)(m.application)) -} - -// register our window to our parent mapping -func (m *linuxApp) registerWindow(window *C.GtkWindow, id uint) { - m.windowsLock.Lock() - m.windows[window] = id - m.windowsLock.Unlock() -} - -func newPlatformApp(parent *App) *linuxApp { - name := strings.ToLower(strings.Replace(parent.options.Name, " ", "", -1)) - if name == "" { - name = "undefined" - } - nameC := C.CString(fmt.Sprintf("org.wails.%s", name)) - app := &linuxApp{ - parent: parent, - application: unsafe.Pointer(C.init(nameC)), - // name: fmt.Sprintf("org.wails.%s", name), - windows: map[*C.GtkWindow]uint{}, - } - C.free(unsafe.Pointer(nameC)) - return app -} - -// executeStartupActions is called by `activateLinux` below to execute -// code which needs to be run after the 'activate' signal is received -func (m *linuxApp) executeStartupActions() { - for _, fn := range m.startupActions { - fn() - } -} - -//export activateLinux -func activateLinux(data unsafe.Pointer) { - getNativeApplication().executeStartupActions() -} - -//export processApplicationEvent -func processApplicationEvent(eventID C.uint) { - // TODO: add translation to Wails events - // currently reusing Mac specific values - applicationEvents <- uint(eventID) -} - -//export processWindowEvent -func processWindowEvent(windowID C.uint, eventID C.uint) { - windowEvents <- &WindowEvent{ - WindowID: uint(windowID), - EventID: uint(eventID), - } -} - -//export processMessage -func processMessage(windowID C.uint, message *C.char) { - windowMessageBuffer <- &windowMessage{ - windowId: uint(windowID), - message: C.GoString(message), - } -} - -//export processDragItems -func processDragItems(windowID C.uint, arr **C.char, length C.int) { - var filenames []string - // Convert the C array to a Go slice - goSlice := (*[1 << 30]*C.char)(unsafe.Pointer(arr))[:length:length] - for _, str := range goSlice { - filenames = append(filenames, C.GoString(str)) - } - windowDragAndDropBuffer <- &dragAndDropMessage{ - windowId: uint(windowID), - filenames: filenames, - } -} - -//export processMenuItemClick -func processMenuItemClick(menuID C.uint) { - menuItemClicked <- uint(menuID) -} - -func setIcon(icon []byte) { - if icon == nil { - return - } - //C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon))) -} diff --git a/v3/pkg/application/application_linux_purego.go b/v3/pkg/application/application_linux_purego.go deleted file mode 100644 index 6585ad59e..000000000 --- a/v3/pkg/application/application_linux_purego.go +++ /dev/null @@ -1,253 +0,0 @@ -//go:build linux && purego - -package application - -import ( - "fmt" - "log" - "os" - "strings" - "sync" - - "github.com/ebitengine/purego" - "github.com/wailsapp/wails/v2/pkg/assetserver/webview" -) - -const ( - gtk3 = "libgtk-3.so" - gtk4 = "libgtk-4.so" -) - -var ( - gtk uintptr - version int - webkit uintptr -) - -func init() { - // needed for GTK4 to function - _ = os.Setenv("GDK_BACKEND", "x11") - var err error - /* - gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) - if err == nil { - version = 4 - return - } - - log.Println("Failed to open GTK4: Falling back to GTK3") - */ - gtk, err = purego.Dlopen(gtk3, purego.RTLD_NOW|purego.RTLD_GLOBAL) - if err != nil { - panic(err) - } - version = 3 - - var webkit4 string = "libwebkit2gtk-4.1.so" - webkit, err = purego.Dlopen(webkit4, purego.RTLD_NOW|purego.RTLD_GLOBAL) - if err != nil { - panic(err) - } -} - -type linuxApp struct { - appName string - application uintptr - applicationMenu uintptr - parent *App - - // Native -> uint - windows map[uintptr]uint - windowsLock sync.Mutex -} - -func getNativeApplication() *linuxApp { - return globalApplication.impl.(*linuxApp) -} - -func (m *linuxApp) hide() { - // C.hide() -} - -func (m *linuxApp) show() { - // C.show() -} - -func (m *linuxApp) on(eventID uint) { - log.Println("linuxApp.on()", eventID) - - // TODO: Setup signal handling as appropriate - // Note: GTK signals seem to be strings! -} - -func (m *linuxApp) setIcon(icon []byte) { - // C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon))) -} - -func (m *linuxApp) name() string { - return m.appName -} - -func (m *linuxApp) getCurrentWindowID() uint { - var getCurrentWindow func(uintptr) uintptr - purego.RegisterLibFunc(&getCurrentWindow, gtk, "gtk_application_get_active_window") - window := getCurrentWindow(m.application) - if window == 0 { - return 1 - } - m.windowsLock.Lock() - defer m.windowsLock.Unlock() - if identifier, ok := m.windows[window]; ok { - return identifier - } - - return 1 -} - -func (m *linuxApp) setApplicationMenu(menu *Menu) { - if menu == nil { - // Create a default menu - menu = defaultApplicationMenu() - } - globalApplication.dispatchOnMainThread(func() { - menu.Update() - m.applicationMenu = (menu.impl).(*linuxMenu).native - }) -} - -func (m *linuxApp) activate() { - fmt.Println("linuxApp.activated!", m.application) - var hold func(uintptr) - purego.RegisterLibFunc(&hold, gtk, "g_application_hold") - - hold(m.application) - - // time.Sleep(50 * time.Millisecond) - // m.parent.activate() -} - -func (m *linuxApp) run() error { - // Add a hook to the ApplicationDidFinishLaunching event - // FIXME: add Wails specific events - i.e. Shouldn't platform specific ones be translated to Wails events? - /* m.parent.On(events.Mac.ApplicationDidFinishLaunching, func() { - // Do we need to do anything now? - fmt.Println("ApplicationDidFinishLaunching!") - }) - */ - m.parent.OnWindowCreation(func(window *WebviewWindow) { - fmt.Println("OnWindowCreation: ", window) - - }) - - var g_signal_connect func(uintptr, string, uintptr, uintptr, bool, int) int - purego.RegisterLibFunc(&g_signal_connect, gtk, "g_signal_connect_data") - g_signal_connect(m.application, "activate", purego.NewCallback(m.activate), m.application, false, 0) - - var run func(uintptr, int, []string) int - purego.RegisterLibFunc(&run, gtk, "g_application_run") - - // FIXME: Convert status to 'error' if needed - status := run(m.application, 0, []string{}) - fmt.Println("status", status) - - var release func(uintptr) - purego.RegisterLibFunc(&release, gtk, "g_application_release") - release(m.application) - - purego.RegisterLibFunc(&release, gtk, "g_object_unref") - release(m.application) - - return nil -} - -func (m *linuxApp) destroy() { - var quit func(uintptr) - purego.RegisterLibFunc(&quit, gtk, "g_application_quit") - quit(m.application) -} - -func (m *linuxApp) registerWindow(address uintptr, window uint) { - m.windowsLock.Lock() - m.windows[address] = window - m.windowsLock.Unlock() -} - -func newPlatformApp(parent *App) *linuxApp { - name := strings.ToLower(parent.options.Name) - if name == "" { - name = "undefined" - } - identifier := fmt.Sprintf("org.wails.%s", strings.Replace(name, " ", "-", -1)) - - var gtkNew func(string, uint) uintptr - purego.RegisterLibFunc(>kNew, gtk, "gtk_application_new") - app := &linuxApp{ - appName: identifier, - parent: parent, - application: gtkNew(identifier, 0), - windows: map[uintptr]uint{}, - } - return app -} - -func processApplicationEvent(eventID uint) { - // TODO: add translation to Wails events - // currently reusing Mac specific values - applicationEvents <- eventID -} - -func processWindowEvent(windowID uint, eventID uint) { - windowEvents <- &WindowEvent{ - WindowID: windowID, - EventID: eventID, - } -} - -func processMessage(windowID uint, message string) { - windowMessageBuffer <- &windowMessage{ - windowId: windowID, - message: message, - } -} - -func processURLRequest(windowID uint, wkUrlSchemeTask uintptr) { - fmt.Println("processURLRequest", windowID, wkUrlSchemeTask) - webviewRequests <- &webViewAssetRequest{ - Request: webview.NewRequest(wkUrlSchemeTask), - windowId: windowID, - windowName: globalApplication.getWindowForID(windowID).Name(), - } -} - -func processDragItems(windowID uint, arr []string, length int) { - windowDragAndDropBuffer <- &dragAndDropMessage{ - windowId: windowID, - filenames: arr, - } -} - -func processMenuItemClick(menuID uint) { - menuItemClicked <- menuID -} - -func setIcon(icon []byte) { - if icon == nil { - return - } - fmt.Println("setIcon") - /* - GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); - if (!loader) - { - return; - } - if (gdk_pixbuf_loader_write(loader, buf, len, NULL) && gdk_pixbuf_loader_close(loader, NULL)) - { - GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader); - if (pixbuf) - { - gtk_window_set_icon(window, pixbuf); - } - } - g_object_unref(loader);*/ -} diff --git a/v3/pkg/application/clipboard_linux.go b/v3/pkg/application/clipboard_linux.go deleted file mode 100644 index cfbdf4df1..000000000 --- a/v3/pkg/application/clipboard_linux.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build linux - -package application - -import ( - "sync" -) - -var clipboardLock sync.RWMutex - -type linuxClipboard struct{} - -func (m linuxClipboard) setText(text string) bool { - clipboardLock.Lock() - defer clipboardLock.Unlock() - // cText := C.CString(text) - // success := C.setClipboardText(cText) - // C.free(unsafe.Pointer(cText)) - success := false - return bool(success) -} - -func (m linuxClipboard) text() string { - clipboardLock.RLock() - defer clipboardLock.RUnlock() - // clipboardText := C.getClipboardText() - // result := C.GoString(clipboardText) - return "" -} - -func newClipboardImpl() *linuxClipboard { - return &linuxClipboard{} -} diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go deleted file mode 100644 index 85733bdf1..000000000 --- a/v3/pkg/application/dialogs_linux.go +++ /dev/null @@ -1,300 +0,0 @@ -//go:build linux - -package application - -/* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 - -#include -#include -#include - -static GtkWidget* new_about_dialog(GtkWindow *parent, const gchar *msg) { - // gtk_message_dialog_new is variadic! Can't call from cgo - GtkWidget *dialog; - dialog = gtk_message_dialog_new( - parent, - GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, - GTK_MESSAGE_INFO, - GTK_BUTTONS_CLOSE, - msg); - - g_signal_connect_swapped (dialog, - "response", - G_CALLBACK (gtk_widget_destroy), - dialog); - return dialog; -}; - -*/ -import "C" -import ( - "fmt" - "unsafe" -) - -const AlertStyleWarning = C.int(0) -const AlertStyleInformational = C.int(1) -const AlertStyleCritical = C.int(2) - -var alertTypeMap = map[DialogType]C.int{ - WarningDialog: AlertStyleWarning, - InfoDialog: AlertStyleInformational, - ErrorDialog: AlertStyleCritical, - QuestionDialog: AlertStyleInformational, -} - -func setWindowIcon(window *C.GtkWindow, icon []byte) { - fmt.Println("setWindowIcon", len(icon)) - loader := C.gdk_pixbuf_loader_new() - if loader == nil { - return - } - written := C.gdk_pixbuf_loader_write( - loader, - (*C.uchar)(&icon[0]), - C.ulong(len(icon)), - nil) - if written == 0 { - fmt.Println("failed to write icon") - return - } - C.gdk_pixbuf_loader_close(loader, nil) - pixbuf := C.gdk_pixbuf_loader_get_pixbuf(loader) - if pixbuf != nil { - fmt.Println("gtk_window_set_icon", window) - C.gtk_window_set_icon((*C.GtkWindow)(window), pixbuf) - } - C.g_object_unref(C.gpointer(loader)) -} - -func (m *linuxApp) showAboutDialog(title string, message string, icon []byte) { - globalApplication.dispatchOnMainThread(func() { - parent := C.gtk_application_get_active_window((*C.GtkApplication)(m.application)) - cMsg := C.CString(message) - cTitle := C.CString(title) - defer C.free(unsafe.Pointer(cMsg)) - defer C.free(unsafe.Pointer(cTitle)) - dialog := C.new_about_dialog(parent, cMsg) - C.gtk_window_set_title( - (*C.GtkWindow)(unsafe.Pointer(dialog)), - cTitle) - // setWindowIcon((*C.GtkWindow)(dialog), icon) - C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dialog))) - - }) -} - -type linuxDialog struct { - dialog *MessageDialog - - //nsDialog unsafe.Pointer -} - -func (m *linuxDialog) show() { - globalApplication.dispatchOnMainThread(func() { - - // Mac can only have 4 Buttons on a dialog - if len(m.dialog.Buttons) > 4 { - m.dialog.Buttons = m.dialog.Buttons[:4] - } - - // if m.nsDialog != nil { - // //C.releaseDialog(m.nsDialog) - // } - // var title *C.char - // if m.dialog.Title != "" { - // title = C.CString(m.dialog.Title) - // } - // var message *C.char - // if m.dialog.Message != "" { - // message = C.CString(m.dialog.Message) - // } - // var iconData unsafe.Pointer - // var iconLength C.int - // if m.dialog.Icon != nil { - // iconData = unsafe.Pointer(&m.dialog.Icon[0]) - // iconLength = C.int(len(m.dialog.Icon)) - // } else { - // // if it's an error, use the application Icon - // if m.dialog.DialogType == ErrorDialog { - // iconData = unsafe.Pointer(&globalApplication.options.Icon[0]) - // iconLength = C.int(len(globalApplication.options.Icon)) - // } - // } - - // alertType, ok := alertTypeMap[m.dialog.DialogType] - // if !ok { - // alertType = AlertStyleInformational - // } - - // m.nsDialog = C.createAlert(alertType, title, message, iconData, iconLength) - - // Reverse the Buttons so that the default is on the right - reversedButtons := make([]*Button, len(m.dialog.Buttons)) - var count = 0 - for i := len(m.dialog.Buttons) - 1; i >= 0; i-- { - //button := m.dialog.Buttons[i] - //C.alertAddButton(m.nsDialog, C.CString(button.Label), C.bool(button.IsDefault), C.bool(button.IsCancel)) - reversedButtons[count] = m.dialog.Buttons[i] - count++ - } - - buttonPressed := int(0) //C.dialogRunModal(m.nsDialog)) - if len(m.dialog.Buttons) > buttonPressed { - button := reversedButtons[buttonPressed] - if button.callback != nil { - button.callback() - } - } - }) - -} - -func newDialogImpl(d *MessageDialog) *linuxDialog { - return &linuxDialog{ - dialog: d, - } -} - -type linuxOpenFileDialog struct { - dialog *OpenFileDialog -} - -func newOpenFileDialogImpl(d *OpenFileDialog) *linuxOpenFileDialog { - return &linuxOpenFileDialog{ - dialog: d, - } -} - -func toCString(s string) *C.char { - if s == "" { - return nil - } - return C.CString(s) -} - -func (m *linuxOpenFileDialog) show() ([]string, error) { - openFileResponses[m.dialog.id] = make(chan string) - // nsWindow := unsafe.Pointer(nil) - if m.dialog.window != nil { - // get NSWindow from window - //nsWindow = m.dialog.window.impl.(*macosWebviewWindow).nsWindow - } - - // Massage filter patterns into macOS format - // We iterate all filter patterns, tidy them up and then join them with a semicolon - // This should produce a single string of extensions like "png;jpg;gif" - // var filterPatterns string - // if len(m.dialog.filters) > 0 { - // var allPatterns []string - // for _, filter := range m.dialog.filters { - // patternComponents := strings.Split(filter.Pattern, ";") - // for i, component := range patternComponents { - // filterPattern := strings.TrimSpace(component) - // filterPattern = strings.TrimPrefix(filterPattern, "*.") - // patternComponents[i] = filterPattern - // } - // allPatterns = append(allPatterns, strings.Join(patternComponents, ";")) - // } - // filterPatterns = strings.Join(allPatterns, ";") - // } - - // C.showOpenFileDialog(C.uint(m.dialog.id), - // C.bool(m.dialog.canChooseFiles), - // C.bool(m.dialog.canChooseDirectories), - // C.bool(m.dialog.canCreateDirectories), - // C.bool(m.dialog.showHiddenFiles), - // C.bool(m.dialog.allowsMultipleSelection), - // C.bool(m.dialog.resolvesAliases), - // C.bool(m.dialog.hideExtension), - // C.bool(m.dialog.treatsFilePackagesAsDirectories), - // C.bool(m.dialog.allowsOtherFileTypes), - // toCString(filterPatterns), - // C.uint(len(filterPatterns)), - // toCString(m.dialog.message), - // toCString(m.dialog.directory), - // toCString(m.dialog.buttonText), - // nsWindow) - var result []string - for filename := range openFileResponses[m.dialog.id] { - result = append(result, filename) - } - return result, nil -} - -//export openFileDialogCallback -func openFileDialogCallback(cid C.uint, cpath *C.char) { - path := C.GoString(cpath) - id := uint(cid) - channel, ok := openFileResponses[id] - if ok { - channel <- path - } else { - panic("No channel found for open file dialog") - } -} - -//export openFileDialogCallbackEnd -func openFileDialogCallbackEnd(cid C.uint) { - id := uint(cid) - channel, ok := openFileResponses[id] - if ok { - close(channel) - delete(openFileResponses, id) - freeDialogID(id) - } else { - panic("No channel found for open file dialog") - } -} - -type linuxSaveFileDialog struct { - dialog *SaveFileDialog -} - -func newSaveFileDialogImpl(d *SaveFileDialog) *linuxSaveFileDialog { - return &linuxSaveFileDialog{ - dialog: d, - } -} - -func (m *linuxSaveFileDialog) show() (string, error) { - saveFileResponses[m.dialog.id] = make(chan string) - // nsWindow := unsafe.Pointer(nil) - if m.dialog.window != nil { - // get NSWindow from window - // nsWindow = m.dialog.window.impl.(*linuxWebviewWindow).nsWindow - } - - // C.showSaveFileDialog(C.uint(m.dialog.id), - // C.bool(m.dialog.canCreateDirectories), - // C.bool(m.dialog.showHiddenFiles), - // C.bool(m.dialog.canSelectHiddenExtension), - // C.bool(m.dialog.hideExtension), - // C.bool(m.dialog.treatsFilePackagesAsDirectories), - // C.bool(m.dialog.allowOtherFileTypes), - // toCString(m.dialog.message), - // toCString(m.dialog.directory), - // toCString(m.dialog.buttonText), - // toCString(m.dialog.filename), - // nsWindow) - return <-saveFileResponses[m.dialog.id], nil -} - -//export saveFileDialogCallback -func saveFileDialogCallback(cid C.uint, cpath *C.char) { - // Covert the path to a string - path := C.GoString(cpath) - id := uint(cid) - // put response on channel - channel, ok := saveFileResponses[id] - if ok { - channel <- path - close(channel) - delete(saveFileResponses, id) - freeDialogID(id) - - } else { - panic("No channel found for save file dialog") - } -} diff --git a/v3/pkg/application/dialogs_linux_purego.go b/v3/pkg/application/dialogs_linux_purego.go deleted file mode 100644 index 757b4f3d3..000000000 --- a/v3/pkg/application/dialogs_linux_purego.go +++ /dev/null @@ -1,220 +0,0 @@ -//go:build linux && purego - -package application - -const AlertStyleWarning = 0 -const AlertStyleInformational = 1 -const AlertStyleCritical = 2 - -var alertTypeMap = map[DialogType]int{ - WarningDialog: AlertStyleWarning, - InfoDialog: AlertStyleInformational, - ErrorDialog: AlertStyleCritical, - QuestionDialog: AlertStyleInformational, -} - -func (m *linuxApp) showAboutDialog(title string, message string, icon []byte) { - // var iconData unsafe.Pointer - // if icon != nil { - // iconData = unsafe.Pointer(&icon[0]) - // } - //C.showAboutBox(C.CString(title), C.CString(message), iconData, C.int(len(icon))) -} - -type linuxDialog struct { - dialog *MessageDialog - - //nsDialog unsafe.Pointer -} - -func (m *linuxDialog) show() { - globalApplication.dispatchOnMainThread(func() { - - // Mac can only have 4 Buttons on a dialog - if len(m.dialog.Buttons) > 4 { - m.dialog.Buttons = m.dialog.Buttons[:4] - } - - // if m.nsDialog != nil { - // //C.releaseDialog(m.nsDialog) - // } - // var title *C.char - // if m.dialog.Title != "" { - // title = C.CString(m.dialog.Title) - // } - // var message *C.char - // if m.dialog.Message != "" { - // message = C.CString(m.dialog.Message) - // } - // var iconData unsafe.Pointer - // var iconLength C.int - // if m.dialog.Icon != nil { - // iconData = unsafe.Pointer(&m.dialog.Icon[0]) - // iconLength = C.int(len(m.dialog.Icon)) - // } else { - // // if it's an error, use the application Icon - // if m.dialog.DialogType == ErrorDialog { - // iconData = unsafe.Pointer(&globalApplication.options.Icon[0]) - // iconLength = C.int(len(globalApplication.options.Icon)) - // } - // } - - // alertType, ok := alertTypeMap[m.dialog.DialogType] - // if !ok { - // alertType = AlertStyleInformational - // } - - // m.nsDialog = C.createAlert(alertType, title, message, iconData, iconLength) - - // Reverse the Buttons so that the default is on the right - reversedButtons := make([]*Button, len(m.dialog.Buttons)) - var count = 0 - for i := len(m.dialog.Buttons) - 1; i >= 0; i-- { - //button := m.dialog.Buttons[i] - //C.alertAddButton(m.nsDialog, C.CString(button.Label), C.bool(button.IsDefault), C.bool(button.IsCancel)) - reversedButtons[count] = m.dialog.Buttons[i] - count++ - } - - buttonPressed := int(0) //C.dialogRunModal(m.nsDialog)) - if len(m.dialog.Buttons) > buttonPressed { - button := reversedButtons[buttonPressed] - if button.callback != nil { - button.callback() - } - } - }) - -} - -func newDialogImpl(d *MessageDialog) *linuxDialog { - return &linuxDialog{ - dialog: d, - } -} - -type linuxOpenFileDialog struct { - dialog *OpenFileDialog -} - -func newOpenFileDialogImpl(d *OpenFileDialog) *linuxOpenFileDialog { - return &linuxOpenFileDialog{ - dialog: d, - } -} - -func (m *linuxOpenFileDialog) show() ([]string, error) { - openFileResponses[m.dialog.id] = make(chan string) - // nsWindow := unsafe.Pointer(nil) - if m.dialog.window != nil { - // get NSWindow from window - //nsWindow = m.dialog.window.impl.(*macosWebviewWindow).nsWindow - } - - // Massage filter patterns into macOS format - // We iterate all filter patterns, tidy them up and then join them with a semicolon - // This should produce a single string of extensions like "png;jpg;gif" - // var filterPatterns string - // if len(m.dialog.filters) > 0 { - // var allPatterns []string - // for _, filter := range m.dialog.filters { - // patternComponents := strings.Split(filter.Pattern, ";") - // for i, component := range patternComponents { - // filterPattern := strings.TrimSpace(component) - // filterPattern = strings.TrimPrefix(filterPattern, "*.") - // patternComponents[i] = filterPattern - // } - // allPatterns = append(allPatterns, strings.Join(patternComponents, ";")) - // } - // filterPatterns = strings.Join(allPatterns, ";") - // } - - // C.showOpenFileDialog(C.uint(m.dialog.id), - // C.bool(m.dialog.canChooseFiles), - // C.bool(m.dialog.canChooseDirectories), - // C.bool(m.dialog.canCreateDirectories), - // C.bool(m.dialog.showHiddenFiles), - // C.bool(m.dialog.allowsMultipleSelection), - // C.bool(m.dialog.resolvesAliases), - // C.bool(m.dialog.hideExtension), - // C.bool(m.dialog.treatsFilePackagesAsDirectories), - // C.bool(m.dialog.allowsOtherFileTypes), - // toCString(filterPatterns), - // C.uint(len(filterPatterns)), - // toCString(m.dialog.message), - // toCString(m.dialog.directory), - // toCString(m.dialog.buttonText), - // nsWindow) - var result []string - for filename := range openFileResponses[m.dialog.id] { - result = append(result, filename) - } - return result, nil -} - -func openFileDialogCallback(id uint, path string) { - channel, ok := openFileResponses[id] - if ok { - channel <- path - } else { - panic("No channel found for open file dialog") - } -} - -func openFileDialogCallbackEnd(id uint) { - channel, ok := openFileResponses[id] - if ok { - close(channel) - delete(openFileResponses, id) - freeDialogID(id) - } else { - panic("No channel found for open file dialog") - } -} - -type linuxSaveFileDialog struct { - dialog *SaveFileDialog -} - -func newSaveFileDialogImpl(d *SaveFileDialog) *linuxSaveFileDialog { - return &linuxSaveFileDialog{ - dialog: d, - } -} - -func (m *linuxSaveFileDialog) show() (string, error) { - saveFileResponses[m.dialog.id] = make(chan string) - // nsWindow := unsafe.Pointer(nil) - if m.dialog.window != nil { - // get NSWindow from window - // nsWindow = m.dialog.window.impl.(*linuxWebviewWindow).nsWindow - } - - // C.showSaveFileDialog(C.uint(m.dialog.id), - // C.bool(m.dialog.canCreateDirectories), - // C.bool(m.dialog.showHiddenFiles), - // C.bool(m.dialog.canSelectHiddenExtension), - // C.bool(m.dialog.hideExtension), - // C.bool(m.dialog.treatsFilePackagesAsDirectories), - // C.bool(m.dialog.allowOtherFileTypes), - // toCString(m.dialog.message), - // toCString(m.dialog.directory), - // toCString(m.dialog.buttonText), - // toCString(m.dialog.filename), - // nsWindow) - return <-saveFileResponses[m.dialog.id], nil -} - -func saveFileDialogCallback(cid uint, path string) { - // put response on channel - channel, ok := saveFileResponses[cid] - if ok { - channel <- path - close(channel) - delete(saveFileResponses, cid) - freeDialogID(cid) - - } else { - panic("No channel found for save file dialog") - } -} diff --git a/v3/pkg/application/mainthread_linux.go b/v3/pkg/application/mainthread_linux.go deleted file mode 100644 index 670f01f9f..000000000 --- a/v3/pkg/application/mainthread_linux.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build linux - -package application - -/* -#cgo linux pkg-config: gtk+-3.0 - -#include -#include "gtk/gtk.h" - -typedef struct CallbackID -{ - unsigned int value; -} CallbackID; - -extern void dispatchOnMainThreadCallback(unsigned int); - -static gboolean dispatchCallback(gpointer data) { - struct CallbackID *args = data; - unsigned int cid = args->value; - dispatchOnMainThreadCallback(cid); - free(args); - - return G_SOURCE_REMOVE; -}; - -static void dispatchOnMainThread(unsigned int id) { - CallbackID *args = malloc(sizeof(CallbackID)); - args->value = id; - g_idle_add((GSourceFunc)dispatchCallback, (gpointer)args); -} - -*/ -import "C" - -func (m *linuxApp) dispatchOnMainThread(id uint) { - C.dispatchOnMainThread(C.uint(id)) -} - -//export dispatchOnMainThreadCallback -func dispatchOnMainThreadCallback(callbackID C.uint) { - mainThreadFunctionStoreLock.RLock() - id := uint(callbackID) - fn := mainThreadFunctionStore[id] - if fn == nil { - Fatal("dispatchCallback called with invalid id: %v", id) - } - delete(mainThreadFunctionStore, id) - mainThreadFunctionStoreLock.RUnlock() - fn() -} diff --git a/v3/pkg/application/mainthread_linux_purego.go b/v3/pkg/application/mainthread_linux_purego.go deleted file mode 100644 index fb69c96e3..000000000 --- a/v3/pkg/application/mainthread_linux_purego.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build linux && purego - -package application - -import "github.com/ebitengine/purego" - -const ( - G_SOURCE_REMOVE = 0 -) - -func (m *linuxApp) dispatchOnMainThread(id uint) { - var dispatch func(uintptr) - purego.RegisterLibFunc(&dispatch, gtk, "g_idle_add") - dispatch(purego.NewCallback(func(uintptr) int { - dispatchOnMainThreadCallback(id) - return G_SOURCE_REMOVE - })) -} - -func dispatchOnMainThreadCallback(callbackID uint) { - mainThreadFunctionStoreLock.RLock() - id := uint(callbackID) - fn := mainThreadFunctionStore[id] - if fn == nil { - Fatal("dispatchCallback called with invalid id: %v", id) - } - delete(mainThreadFunctionStore, id) - mainThreadFunctionStoreLock.RUnlock() - fn() -} diff --git a/v3/pkg/application/menu_linux.go b/v3/pkg/application/menu_linux.go deleted file mode 100644 index e0c0118a9..000000000 --- a/v3/pkg/application/menu_linux.go +++ /dev/null @@ -1,200 +0,0 @@ -//go:build linux - -package application - -/* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 - -#include -#include - -void handleClick(void*); -*/ -import "C" -import ( - "fmt" - "unsafe" -) - -var ( - gtkSignalHandlers map[*C.GtkWidget]C.gulong - gtkSignalToMenuItem map[*C.GtkWidget]*MenuItem -) - -func init() { - gtkSignalHandlers = map[*C.GtkWidget]C.gulong{} - gtkSignalToMenuItem = map[*C.GtkWidget]*MenuItem{} -} - -//export handleClick -func handleClick(idPtr unsafe.Pointer) { - id := (*C.GtkWidget)(idPtr) - item, ok := gtkSignalToMenuItem[id] - if !ok { - return - } - - //impl := (item.impl).(*linuxMenuItem) - - switch item.itemType { - case text, checkbox: - processMenuItemClick(C.uint(item.id)) - case radio: - menuItem := (item.impl).(*linuxMenuItem) - if menuItem.isChecked() { - processMenuItemClick(C.uint(item.id)) - } - } -} - -type linuxMenu struct { - menu *Menu - native unsafe.Pointer -} - -func newMenuImpl(menu *Menu) *linuxMenu { - result := &linuxMenu{ - menu: menu, - native: unsafe.Pointer(C.gtk_menu_bar_new()), - } - return result -} - -func (m *linuxMenu) update() { - // fmt.Println("linuxMenu.update()") - // if m.native != nil { - // C.gtk_widget_destroy((*C.GtkWidget)(m.native)) - // m.native = unsafe.Pointer(C.gtk_menu_new()) - // } - m.processMenu(m.menu) -} - -func (m *linuxMenu) processMenu(menu *Menu) { - if menu.impl == nil { - menu.impl = &linuxMenu{ - menu: menu, - native: unsafe.Pointer(C.gtk_menu_new()), - } - } - var currentRadioGroup *C.GSList - - for _, item := range menu.items { - // drop the group if we have run out of radio items - if item.itemType != radio { - currentRadioGroup = nil - } - - switch item.itemType { - case submenu: - menuItem := newMenuItemImpl(item) - item.impl = menuItem - m.processMenu(item.submenu) - m.addSubMenuToItem(item.submenu, item) - m.addMenuItem(menu, item) - case text, checkbox: - menuItem := newMenuItemImpl(item) - item.impl = menuItem - m.addMenuItem(menu, item) - case radio: - menuItem := newRadioItemImpl(item, currentRadioGroup) - item.impl = menuItem - m.addMenuItem(menu, item) - currentRadioGroup = C.gtk_radio_menu_item_get_group((*C.GtkRadioMenuItem)(menuItem.native)) - case separator: - m.addMenuSeparator(menu) - } - - } - - for _, item := range menu.items { - if item.callback != nil { - m.attachHandler(item) - } - } - -} - -func (m *linuxMenu) attachHandler(item *MenuItem) { - signal := C.CString("activate") - defer C.free(unsafe.Pointer(signal)) - - impl := (item.impl).(*linuxMenuItem) - widget := impl.native - flags := C.GConnectFlags(0) - handlerId := C.g_signal_connect_object( - C.gpointer(widget), - signal, - C.GCallback(C.handleClick), - C.gpointer(widget), - flags) - - id := (*C.GtkWidget)(widget) - gtkSignalToMenuItem[id] = item - gtkSignalHandlers[id] = handlerId - impl.handlerId = handlerId -} - -func (m *linuxMenu) addSubMenuToItem(menu *Menu, item *MenuItem) { - if menu.impl == nil { - menu.impl = &linuxMenu{ - menu: menu, - native: unsafe.Pointer(C.gtk_menu_new()), - } - } - - C.gtk_menu_item_set_submenu( - (*C.GtkMenuItem)((item.impl).(*linuxMenuItem).native), - (*C.GtkWidget)((menu.impl).(*linuxMenu).native)) - - if item.role == ServicesMenu { - // FIXME: what does this mean? - } -} - -func (m *linuxMenu) addMenuItem(parent *Menu, menu *MenuItem) { - // fmt.Println("addMenuIteam", fmt.Sprintf("%+v", parent), fmt.Sprintf("%+v", menu)) - C.gtk_menu_shell_append( - (*C.GtkMenuShell)((parent.impl).(*linuxMenu).native), - (*C.GtkWidget)((menu.impl).(*linuxMenuItem).native), - ) - /* - C.gtk_menu_item_set_submenu( - (*C.struct__GtkMenuItem)((menu.impl).(*linuxMenuItem).native), - (*C.struct__GtkWidget)((parent.impl).(*linuxMenu).native), - ) - */ -} - -func (m *linuxMenu) addMenuSeparator(menu *Menu) { - // fmt.Println("addMenuSeparator", fmt.Sprintf("%+v", menu)) - sep := C.gtk_separator_menu_item_new() - native := (menu.impl).(*linuxMenu).native - C.gtk_menu_shell_append((*C.GtkMenuShell)(native), sep) -} - -func (m *linuxMenu) addServicesMenu(menu *Menu) { - fmt.Println("addServicesMenu - not implemented") - //C.addServicesMenu(unsafe.Pointer(menu.impl.(*linuxMenu).nsMenu)) -} - -func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { - impl := newMenuImpl(&Menu{label: name}) - menu := &Menu{ - label: name, - items: items, - impl: impl, - } - impl.menu = menu - return menu -} - -func defaultApplicationMenu() *Menu { - menu := NewMenu() - menu.AddRole(AppMenu) - menu.AddRole(FileMenu) - menu.AddRole(EditMenu) - menu.AddRole(ViewMenu) - menu.AddRole(WindowMenu) - menu.AddRole(HelpMenu) - return menu -} diff --git a/v3/pkg/application/menu_linux_purego.go b/v3/pkg/application/menu_linux_purego.go deleted file mode 100644 index 601d09b5f..000000000 --- a/v3/pkg/application/menu_linux_purego.go +++ /dev/null @@ -1,179 +0,0 @@ -//go:build linux && purego - -package application - -import ( - "fmt" - - "github.com/ebitengine/purego" -) - -type linuxMenu struct { - menu *Menu - native uintptr -} - -func newMenuImpl(menu *Menu) *linuxMenu { - var newMenuBar func() uintptr - purego.RegisterLibFunc(&newMenuBar, gtk, "gtk_menu_bar_new") - result := &linuxMenu{ - menu: menu, - native: newMenuBar(), - } - return result -} - -func (m *linuxMenu) update() { - m.processMenu(m.menu) -} - -func (m *linuxMenu) processMenu(menu *Menu) { - var newMenu func() uintptr - purego.RegisterLibFunc(&newMenu, gtk, "gtk_menu_new") - if menu.impl == nil { - menu.impl = &linuxMenu{ - menu: menu, - native: newMenu(), - } - } - var currentRadioGroup uintptr - - for _, item := range menu.items { - // drop the group if we have run out of radio items - if item.itemType != radio { - currentRadioGroup = 0 - } - - switch item.itemType { - case submenu: - menuItem := newMenuItemImpl(item) - item.impl = menuItem - m.processMenu(item.submenu) - m.addSubMenuToItem(item.submenu, item) - m.addMenuItem(menu, item) - case text, checkbox: - menuItem := newMenuItemImpl(item) - item.impl = menuItem - m.addMenuItem(menu, item) - case radio: - menuItem := newRadioItemImpl(item, currentRadioGroup) - item.impl = menuItem - m.addMenuItem(menu, item) - - var radioGetGroup func(uintptr) uintptr - purego.RegisterLibFunc(&radioGetGroup, gtk, "gtk_radio_menu_item_get_group") - - currentRadioGroup = radioGetGroup(menuItem.native) - case separator: - m.addMenuSeparator(menu) - } - - } - - for _, item := range menu.items { - if item.callback != nil { - m.attachHandler(item) - } - } - -} - -func (m *linuxMenu) attachHandler(item *MenuItem) { - impl := (item.impl).(*linuxMenuItem) - widget := impl.native - flags := 0 - - var handleClick = func() { - item := item - switch item.itemType { - case text, checkbox: - processMenuItemClick(item.id) - case radio: - menuItem := (item.impl).(*linuxMenuItem) - if menuItem.isChecked() { - processMenuItemClick(item.id) - } - default: - fmt.Println("handleClick", item.itemType, item.id) - } - } - - var signalConnectObject func(uintptr, string, uintptr, uintptr, int) uint - purego.RegisterLibFunc(&signalConnectObject, gtk, "g_signal_connect_object") - handlerId := signalConnectObject( - widget, - "activate", - purego.NewCallback(handleClick), - widget, - flags) - - impl.handlerId = handlerId -} - -func (m *linuxMenu) addSubMenuToItem(menu *Menu, item *MenuItem) { - var newMenu func() uintptr - purego.RegisterLibFunc(&newMenu, gtk, "gtk_menu_new") - if menu.impl == nil { - menu.impl = &linuxMenu{ - menu: menu, - native: newMenu(), -N } - } - var itemSetSubmenu func(uintptr, uintptr) - purego.RegisterLibFunc(&itemSetSubmenu, gtk, "gtk_menu_item_set_submenu") - - itemSetSubmenu( - (item.impl).(*linuxMenuItem).native, - (menu.impl).(*linuxMenu).native) - - if item.role == ServicesMenu { - // FIXME: what does this mean? - } -} - -func (m *linuxMenu) addMenuItem(parent *Menu, menu *MenuItem) { - var shellAppend func(uintptr, uintptr) - purego.RegisterLibFunc(&shellAppend, gtk, "gtk_menu_shell_append") - shellAppend( - (parent.impl).(*linuxMenu).native, - (menu.impl).(*linuxMenuItem).native, - ) -} - -func (m *linuxMenu) addMenuSeparator(menu *Menu) { - var newSeparator func() uintptr - purego.RegisterLibFunc(&newSeparator, gtk, "gtk_separator_menu_item_new") - var shellAppend func(uintptr, uintptr) - purego.RegisterLibFunc(&shellAppend, gtk, "gtk_menu_shell_append") - - sep := newSeparator() - native := (menu.impl).(*linuxMenu).native - shellAppend(native, sep) -} - -func (m *linuxMenu) addServicesMenu(menu *Menu) { - fmt.Println("addServicesMenu - not implemented") - //C.addServicesMenu(unsafe.Pointer(menu.impl.(*linuxMenu).nsMenu)) -} - -func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { - impl := newMenuImpl(&Menu{label: name}) - menu := &Menu{ - label: name, - items: items, - impl: impl, - } - impl.menu = menu - return menu -} - -func defaultApplicationMenu() *Menu { - menu := NewMenu() - menu.AddRole(AppMenu) - menu.AddRole(FileMenu) - menu.AddRole(EditMenu) - menu.AddRole(ViewMenu) - menu.AddRole(WindowMenu) - menu.AddRole(HelpMenu) - return menu -} diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index 74ed4ada4..e30750781 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -203,9 +203,7 @@ func (m *MenuItem) handleClick() { if m.itemType == radio { for _, member := range m.radioGroupMembers { member.checked = false - if member.impl != nil { - member.impl.setChecked(false) - } + member.impl.setChecked(false) } m.checked = true ctx.withChecked(true) diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go deleted file mode 100644 index d67a02729..000000000 --- a/v3/pkg/application/menuitem_linux.go +++ /dev/null @@ -1,407 +0,0 @@ -//go:build linux - -package application - -import ( - "fmt" - "runtime" - "unsafe" -) - -/* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 - -#include -#include "gtk/gtk.h" - - - -*/ -import "C" - -type linuxMenuItem struct { - menuItem *MenuItem - native unsafe.Pointer - handlerId C.gulong -} - -func (l linuxMenuItem) setTooltip(tooltip string) { - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - - value := C.CString(tooltip) - C.gtk_widget_set_tooltip_text( - (*C.GtkWidget)(l.native), - value) - C.free(unsafe.Pointer(value)) - }) -} - -func (l linuxMenuItem) blockSignal() { - if l.handlerId != 0 { - C.g_signal_handler_block(C.gpointer(l.native), l.handlerId) - } -} - -func (l linuxMenuItem) unBlockSignal() { - if l.handlerId != 0 { - C.g_signal_handler_unblock(C.gpointer(l.native), l.handlerId) - } -} - -func (l linuxMenuItem) setLabel(s string) { - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - value := C.CString(s) - C.gtk_menu_item_set_label( - (*C.GtkMenuItem)(l.native), - value) - C.free(unsafe.Pointer(value)) - - }) -} - -func (l linuxMenuItem) isChecked() bool { - if C.gtk_check_menu_item_get_active((*C.GtkCheckMenuItem)(l.native)) == C.int(1) { - return true - } - return false -} - -func (l linuxMenuItem) setDisabled(disabled bool) { - - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - - value := C.int(1) - if disabled { - value = C.int(0) - } - C.gtk_widget_set_sensitive( - (*C.GtkWidget)(l.native), - value) - }) -} - -func (l linuxMenuItem) setChecked(checked bool) { - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - - value := C.int(0) - if checked { - value = C.int(1) - } - - C.gtk_check_menu_item_set_active( - (*C.GtkCheckMenuItem)(l.native), - value) - }) -} - -func (l linuxMenuItem) setAccelerator(accelerator *accelerator) { - fmt.Println("setAccelerator", accelerator) - // Set the keyboard shortcut of the menu item - // var modifier C.int - // var key *C.char - if accelerator != nil { - // modifier = C.int(toMacModifier(accelerator.Modifiers)) - // key = C.CString(accelerator.Key) - } - - // Convert the key to a string - // C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) -} - -func newMenuItemImpl(item *MenuItem) *linuxMenuItem { - result := &linuxMenuItem{ - menuItem: item, - } - cLabel := C.CString(item.label) - switch item.itemType { - case text: - result.native = unsafe.Pointer(C.gtk_menu_item_new_with_label(cLabel)) - - case checkbox: - result.native = unsafe.Pointer(C.gtk_check_menu_item_new_with_label(cLabel)) - result.setChecked(item.checked) - if item.itemType == checkbox || item.itemType == radio { - // C.setMenuItemChecked(result.nsMenuItem, C.bool(item.checked)) - } - if item.accelerator != nil { - result.setAccelerator(item.accelerator) - } - case radio: - panic("Shouldn't get here with a radio item") - - case submenu: - result.native = unsafe.Pointer(C.gtk_menu_item_new_with_label(cLabel)) - - default: - panic("WTF") - } - result.setDisabled(result.menuItem.disabled) - - C.free(unsafe.Pointer(cLabel)) - return result -} - -func newRadioItemImpl(item *MenuItem, group *C.GSList) *linuxMenuItem { - cLabel := C.CString(item.label) - defer C.free(unsafe.Pointer(cLabel)) - result := &linuxMenuItem{ - menuItem: item, - native: unsafe.Pointer(C.gtk_radio_menu_item_new_with_label(group, cLabel)), - } - result.setChecked(item.checked) - result.setDisabled(result.menuItem.disabled) - return result -} - -func newSpeechMenu() *MenuItem { - speechMenu := NewMenu() - speechMenu.Add("Start Speaking"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+."). - OnClick(func(ctx *Context) { - // C.startSpeaking() - }) - speechMenu.Add("Stop Speaking"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+,"). - OnClick(func(ctx *Context) { - // C.stopSpeaking() - }) - subMenu := newSubMenuItem("Speech") - subMenu.submenu = speechMenu - return subMenu -} - -func newHideMenuItem() *MenuItem { - return newMenuItem("Hide " + globalApplication.options.Name). - SetAccelerator("CmdOrCtrl+h"). - OnClick(func(ctx *Context) { - // C.hideApplication() - }) -} - -func newHideOthersMenuItem() *MenuItem { - return newMenuItem("Hide Others"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+h"). - OnClick(func(ctx *Context) { - // C.hideOthers() - }) -} - -func newUnhideMenuItem() *MenuItem { - return newMenuItem("Show All"). - OnClick(func(ctx *Context) { - // C.showAll() - }) -} - -func newUndoMenuItem() *MenuItem { - return newMenuItem("Undo"). - SetAccelerator("CmdOrCtrl+z"). - OnClick(func(ctx *Context) { - // C.undo() - }) -} - -// newRedoMenuItem creates a new menu item for redoing the last action -func newRedoMenuItem() *MenuItem { - return newMenuItem("Redo"). - SetAccelerator("CmdOrCtrl+Shift+z"). - OnClick(func(ctx *Context) { - // C.redo() - }) -} - -func newCutMenuItem() *MenuItem { - return newMenuItem("Cut"). - SetAccelerator("CmdOrCtrl+x"). - OnClick(func(ctx *Context) { - // C.cut() - }) -} - -func newCopyMenuItem() *MenuItem { - return newMenuItem("Copy"). - SetAccelerator("CmdOrCtrl+c"). - OnClick(func(ctx *Context) { - // C.copy() - }) -} - -func newPasteMenuItem() *MenuItem { - return newMenuItem("Paste"). - SetAccelerator("CmdOrCtrl+v"). - OnClick(func(ctx *Context) { - // C.paste() - }) -} - -func newPasteAndMatchStyleMenuItem() *MenuItem { - return newMenuItem("Paste and Match Style"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+v"). - OnClick(func(ctx *Context) { - // C.pasteAndMatchStyle() - }) -} - -func newDeleteMenuItem() *MenuItem { - return newMenuItem("Delete"). - SetAccelerator("backspace"). - OnClick(func(ctx *Context) { - // C.delete() - }) -} - -func newQuitMenuItem() *MenuItem { - return newMenuItem("Quit " + globalApplication.options.Name). - SetAccelerator("CmdOrCtrl+q"). - OnClick(func(ctx *Context) { - globalApplication.Quit() - }) -} - -func newSelectAllMenuItem() *MenuItem { - return newMenuItem("Select All"). - SetAccelerator("CmdOrCtrl+a"). - OnClick(func(ctx *Context) { - // C.selectAll() - }) -} - -func newAboutMenuItem() *MenuItem { - return newMenuItem("About " + globalApplication.options.Name). - OnClick(func(ctx *Context) { - globalApplication.ShowAboutDialog() - }) -} - -func newCloseMenuItem() *MenuItem { - return newMenuItem("Close"). - SetAccelerator("CmdOrCtrl+w"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Close() - } - }) -} - -func newReloadMenuItem() *MenuItem { - return newMenuItem("Reload"). - SetAccelerator("CmdOrCtrl+r"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Reload() - } - }) -} - -func newForceReloadMenuItem() *MenuItem { - return newMenuItem("Force Reload"). - SetAccelerator("CmdOrCtrl+Shift+r"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ForceReload() - } - }) -} - -func newToggleFullscreenMenuItem() *MenuItem { - result := newMenuItem("Toggle Full Screen"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ToggleFullscreen() - } - }) - if runtime.GOOS == "darwin" { - result.SetAccelerator("Ctrl+Command+F") - } else { - result.SetAccelerator("F11") - } - return result -} - -func newToggleDevToolsMenuItem() *MenuItem { - return newMenuItem("Toggle Developer Tools"). - SetAccelerator("Alt+Command+I"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ToggleDevTools() - } - }) -} - -func newZoomResetMenuItem() *MenuItem { - // reset zoom menu item - return newMenuItem("Actual Size"). - SetAccelerator("CmdOrCtrl+0"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ZoomReset() - } - }) -} - -func newZoomInMenuItem() *MenuItem { - return newMenuItem("Zoom In"). - SetAccelerator("CmdOrCtrl+plus"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ZoomIn() - } - }) -} - -func newZoomOutMenuItem() *MenuItem { - return newMenuItem("Zoom Out"). - SetAccelerator("CmdOrCtrl+-"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ZoomOut() - } - }) -} - -func newMinimizeMenuItem() *MenuItem { - return newMenuItem("Minimize"). - SetAccelerator("CmdOrCtrl+M"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Minimise() - } - }) -} - -func newZoomMenuItem() *MenuItem { - return newMenuItem("Zoom"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Zoom() - } - }) -} - -func newFullScreenMenuItem() *MenuItem { - return newMenuItem("Fullscreen"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Fullscreen() - } - }) -} diff --git a/v3/pkg/application/menuitem_linux_purego.go b/v3/pkg/application/menuitem_linux_purego.go deleted file mode 100644 index be51f09e8..000000000 --- a/v3/pkg/application/menuitem_linux_purego.go +++ /dev/null @@ -1,403 +0,0 @@ -//go:build linux && purego - -package application - -import ( - "fmt" - "runtime" - - "github.com/ebitengine/purego" -) - -type linuxMenuItem struct { - menuItem *MenuItem - native uintptr - handlerId uint -} - -func (l linuxMenuItem) setTooltip(tooltip string) { - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - var setToolTip func(uintptr, string) - purego.RegisterLibFunc(&setToolTip, gtk, "gtk_widget_set_tooltip_text") - - setToolTip(l.native, tooltip) - }) -} - -func (l linuxMenuItem) blockSignal() { - var block func(uintptr, uint) - purego.RegisterLibFunc(&block, gtk, "g_signal_handler_block") - - if l.handlerId != 0 { - block(l.native, l.handlerId) - } -} - -func (l linuxMenuItem) unBlockSignal() { - var unblock func(uintptr, uint) - purego.RegisterLibFunc(&unblock, gtk, "g_signal_handler_unblock") - - if l.handlerId != 0 { - unblock(l.native, l.handlerId) - } -} - -func (l linuxMenuItem) setLabel(s string) { - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - var setLabel func(uintptr, string) - purego.RegisterLibFunc(&setLabel, gtk, "gtk_menu_item_set_label") - setLabel(l.native, s) - }) -} - -func (l linuxMenuItem) isChecked() bool { - var getActive func(uintptr) int - purego.RegisterLibFunc(&getActive, gtk, "gtk_check_menu_item_get_active") - - if getActive(l.native) == 1 { - return true - } - return false -} - -func (l linuxMenuItem) setDisabled(disabled bool) { - - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - - var setSensitive func(uintptr, int) - purego.RegisterLibFunc(&setSensitive, gtk, "gtk_widget_set_sensitive") - - value := 1 - if disabled { - value = 0 - } - setSensitive(l.native, value) - }) -} - -func (l linuxMenuItem) setChecked(checked bool) { - globalApplication.dispatchOnMainThread(func() { - l.blockSignal() - defer l.unBlockSignal() - - var setActive func(uintptr, int) - purego.RegisterLibFunc(&setActive, gtk, "gtk_check_menu_item_set_active") - - value := 0 - if checked { - value = 1 - } - setActive(l.native, value) - }) -} - -func (l linuxMenuItem) setAccelerator(accelerator *accelerator) { - fmt.Println("setAccelerator", accelerator) - // Set the keyboard shortcut of the menu item - // var modifier C.int - // var key *C.char - if accelerator != nil { - // modifier = C.int(toMacModifier(accelerator.Modifiers)) - // key = C.CString(accelerator.Key) - } - - // Convert the key to a string - // C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) -} - -func newMenuItemImpl(item *MenuItem) *linuxMenuItem { - result := &linuxMenuItem{ - menuItem: item, - } - var newWithLabel func(string) uintptr - purego.RegisterLibFunc(&newWithLabel, gtk, "gtk_menu_item_new_with_label") - var newCBWithLabel func(string) uintptr - purego.RegisterLibFunc(&newCBWithLabel, gtk, "gtk_check_menu_item_new_with_label") - - switch item.itemType { - case text: - result.native = newWithLabel(item.label) - - case checkbox: - result.native = newCBWithLabel(item.label) - result.setChecked(item.checked) - if item.accelerator != nil { - result.setAccelerator(item.accelerator) - } - case radio: - panic("Shouldn't get here with a radio item") - - case submenu: - result.native = newWithLabel(item.label) - - default: - panic("WTF") - } - result.setDisabled(result.menuItem.disabled) - - return result -} - -func newRadioItemImpl(item *MenuItem, group uintptr) *linuxMenuItem { - var newWithLabel func(uintptr, string) uintptr - purego.RegisterLibFunc(&newWithLabel, gtk, "gtk_radio_menu_item_new_with_label") - - result := &linuxMenuItem{ - menuItem: item, - native: newWithLabel(group, item.label), - } - result.setChecked(item.checked) - result.setDisabled(result.menuItem.disabled) - return result -} - -func newSpeechMenu() *MenuItem { - speechMenu := NewMenu() - speechMenu.Add("Start Speaking"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+."). - OnClick(func(ctx *Context) { - // C.startSpeaking() - }) - speechMenu.Add("Stop Speaking"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+,"). - OnClick(func(ctx *Context) { - // C.stopSpeaking() - }) - subMenu := newSubMenuItem("Speech") - subMenu.submenu = speechMenu - return subMenu -} - -func newHideMenuItem() *MenuItem { - return newMenuItem("Hide " + globalApplication.options.Name). - SetAccelerator("CmdOrCtrl+h"). - OnClick(func(ctx *Context) { - // C.hideApplication() - }) -} - -func newHideOthersMenuItem() *MenuItem { - return newMenuItem("Hide Others"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+h"). - OnClick(func(ctx *Context) { - // C.hideOthers() - }) -} - -func newUnhideMenuItem() *MenuItem { - return newMenuItem("Show All"). - OnClick(func(ctx *Context) { - // C.showAll() - }) -} - -func newUndoMenuItem() *MenuItem { - return newMenuItem("Undo"). - SetAccelerator("CmdOrCtrl+z"). - OnClick(func(ctx *Context) { - // C.undo() - }) -} - -// newRedoMenuItem creates a new menu item for redoing the last action -func newRedoMenuItem() *MenuItem { - return newMenuItem("Redo"). - SetAccelerator("CmdOrCtrl+Shift+z"). - OnClick(func(ctx *Context) { - // C.redo() - }) -} - -func newCutMenuItem() *MenuItem { - return newMenuItem("Cut"). - SetAccelerator("CmdOrCtrl+x"). - OnClick(func(ctx *Context) { - // C.cut() - }) -} - -func newCopyMenuItem() *MenuItem { - return newMenuItem("Copy"). - SetAccelerator("CmdOrCtrl+c"). - OnClick(func(ctx *Context) { - // C.copy() - }) -} - -func newPasteMenuItem() *MenuItem { - return newMenuItem("Paste"). - SetAccelerator("CmdOrCtrl+v"). - OnClick(func(ctx *Context) { - // C.paste() - }) -} - -func newPasteAndMatchStyleMenuItem() *MenuItem { - return newMenuItem("Paste and Match Style"). - SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+v"). - OnClick(func(ctx *Context) { - // C.pasteAndMatchStyle() - }) -} - -func newDeleteMenuItem() *MenuItem { - return newMenuItem("Delete"). - SetAccelerator("backspace"). - OnClick(func(ctx *Context) { - // C.delete() - }) -} - -func newQuitMenuItem() *MenuItem { - return newMenuItem("Quit " + globalApplication.options.Name). - SetAccelerator("CmdOrCtrl+q"). - OnClick(func(ctx *Context) { - globalApplication.Quit() - }) -} - -func newSelectAllMenuItem() *MenuItem { - return newMenuItem("Select All"). - SetAccelerator("CmdOrCtrl+a"). - OnClick(func(ctx *Context) { - // C.selectAll() - }) -} - -func newAboutMenuItem() *MenuItem { - return newMenuItem("About " + globalApplication.options.Name). - OnClick(func(ctx *Context) { - globalApplication.ShowAboutDialog() - }) -} - -func newCloseMenuItem() *MenuItem { - return newMenuItem("Close"). - SetAccelerator("CmdOrCtrl+w"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Close() - } - }) -} - -func newReloadMenuItem() *MenuItem { - return newMenuItem("Reload"). - SetAccelerator("CmdOrCtrl+r"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Reload() - } - }) -} - -func newForceReloadMenuItem() *MenuItem { - return newMenuItem("Force Reload"). - SetAccelerator("CmdOrCtrl+Shift+r"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ForceReload() - } - }) -} - -func newToggleFullscreenMenuItem() *MenuItem { - result := newMenuItem("Toggle Full Screen"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ToggleFullscreen() - } - }) - if runtime.GOOS == "darwin" { - result.SetAccelerator("Ctrl+Command+F") - } else { - result.SetAccelerator("F11") - } - return result -} - -func newToggleDevToolsMenuItem() *MenuItem { - return newMenuItem("Toggle Developer Tools"). - SetAccelerator("Alt+Command+I"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ToggleDevTools() - } - }) -} - -func newZoomResetMenuItem() *MenuItem { - // reset zoom menu item - return newMenuItem("Actual Size"). - SetAccelerator("CmdOrCtrl+0"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ZoomReset() - } - }) -} - -func newZoomInMenuItem() *MenuItem { - return newMenuItem("Zoom In"). - SetAccelerator("CmdOrCtrl+plus"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ZoomIn() - } - }) -} - -func newZoomOutMenuItem() *MenuItem { - return newMenuItem("Zoom Out"). - SetAccelerator("CmdOrCtrl+-"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.ZoomOut() - } - }) -} - -func newMinimizeMenuItem() *MenuItem { - return newMenuItem("Minimize"). - SetAccelerator("CmdOrCtrl+M"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Minimise() - } - }) -} - -func newZoomMenuItem() *MenuItem { - return newMenuItem("Zoom"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Zoom() - } - }) -} - -func newFullScreenMenuItem() *MenuItem { - return newMenuItem("Fullscreen"). - OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() - if currentWindow != nil { - currentWindow.Fullscreen() - } - }) -} diff --git a/v3/pkg/application/messageprocessor_call.go b/v3/pkg/application/messageprocessor_call.go index 0e717994b..51dcc9423 100644 --- a/v3/pkg/application/messageprocessor_call.go +++ b/v3/pkg/application/messageprocessor_call.go @@ -4,16 +4,19 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" ) func (m *MessageProcessor) callErrorCallback(window *WebviewWindow, message string, callID *string, err error) { errorMsg := fmt.Sprintf(message, err) m.Error(errorMsg) - window.CallError(callID, errorMsg) + msg := "_wails.callErrorCallback('" + *callID + "', " + strconv.Quote(errorMsg) + ");" + window.ExecJS(msg) } func (m *MessageProcessor) callCallback(window *WebviewWindow, callID *string, result string, isJSON bool) { - window.CallResponse(callID, result) + msg := fmt.Sprintf("_wails.callCallback('%s', %s, %v);", *callID, strconv.Quote(result), isJSON) + window.ExecJS(msg) } func (m *MessageProcessor) processCallMethod(method string, rw http.ResponseWriter, _ *http.Request, window *WebviewWindow, params QueryParams) { diff --git a/v3/pkg/application/messageprocessor_dialog.go b/v3/pkg/application/messageprocessor_dialog.go index 0810d8138..83389c9a3 100644 --- a/v3/pkg/application/messageprocessor_dialog.go +++ b/v3/pkg/application/messageprocessor_dialog.go @@ -5,16 +5,19 @@ import ( "fmt" "net/http" "runtime" + "strconv" ) func (m *MessageProcessor) dialogErrorCallback(window *WebviewWindow, message string, dialogID *string, err error) { errorMsg := fmt.Sprintf(message, err) m.Error(errorMsg) - window.DialogError(dialogID, errorMsg) + msg := "_wails.dialogErrorCallback('" + *dialogID + "', " + strconv.Quote(errorMsg) + ");" + window.ExecJS(msg) } func (m *MessageProcessor) dialogCallback(window *WebviewWindow, dialogID *string, result string, isJSON bool) { - window.DialogResponse(dialogID, result) + msg := fmt.Sprintf("_wails.dialogCallback('%s', %s, %v);", *dialogID, strconv.Quote(result), isJSON) + window.ExecJS(msg) } func (m *MessageProcessor) processDialogMethod(method string, rw http.ResponseWriter, r *http.Request, window *WebviewWindow, params QueryParams) { diff --git a/v3/pkg/application/options_linux.go b/v3/pkg/application/options_linux.go deleted file mode 100644 index bf3d0c308..000000000 --- a/v3/pkg/application/options_linux.go +++ /dev/null @@ -1,6 +0,0 @@ -package application - -// LinuxWindow contains macOS specific options -type LinuxWindow struct { - ShowApplicationMenu bool -} diff --git a/v3/pkg/application/screen_linux.go b/v3/pkg/application/screen_linux.go deleted file mode 100644 index 585f7db75..000000000 --- a/v3/pkg/application/screen_linux.go +++ /dev/null @@ -1,94 +0,0 @@ -//go:build linux - -package application - -/* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 - -#include -#include -#include -#include - -typedef struct Screen { - const char* id; - const char* name; - int p_width; - int p_height; - int width; - int height; - int x; - int y; - int w_width; - int w_height; - int w_x; - int w_y; - float scale; - double rotation; - bool isPrimary; -} Screen; - - -int GetNumScreens(){ - return 0; -} - -*/ -import "C" -import ( - "fmt" - "sync" - "unsafe" -) - -func (m *linuxApp) getPrimaryScreen() (*Screen, error) { - return nil, fmt.Errorf("not implemented") -} - -func (m *linuxApp) getScreenByIndex(display *C.struct__GdkDisplay, index int) *Screen { - monitor := C.gdk_display_get_monitor(display, C.int(index)) - - // TODO: Do we need to update Screen to contain current info? - // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) - - var geometry C.GdkRectangle - C.gdk_monitor_get_geometry(monitor, &geometry) - primary := false - if C.gdk_monitor_is_primary(monitor) == 1 { - primary = true - } - - return &Screen{ - IsPrimary: primary, - Scale: 1.0, - X: int(geometry.x), - Y: int(geometry.y), - Size: Size{ - Height: int(geometry.height), - Width: int(geometry.width), - }, - } -} - -func (m *linuxApp) getScreens() ([]*Screen, error) { - var wg sync.WaitGroup - var screens []*Screen - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - window := C.gtk_application_get_active_window((*C.GtkApplication)(m.application)) - display := C.gdk_window_get_display((*C.GdkWindow)(unsafe.Pointer(window))) - count := C.gdk_display_get_n_monitors(display) - for i := 0; i < int(count); i++ { - screens = append(screens, - m.getScreenByIndex(display, i), - ) - } - wg.Done() - }) - wg.Wait() - return screens, nil -} - -func getScreenForWindow(window *linuxWebviewWindow) (*Screen, error) { - return window.getScreen() -} diff --git a/v3/pkg/application/screen_linux_purego.go b/v3/pkg/application/screen_linux_purego.go deleted file mode 100644 index 6f7add617..000000000 --- a/v3/pkg/application/screen_linux_purego.go +++ /dev/null @@ -1,92 +0,0 @@ -//go:build linux && purego - -package application - -import ( - "fmt" - "sync" - "unsafe" - - "github.com/ebitengine/purego" -) - -func (m *linuxApp) getPrimaryScreen() (*Screen, error) { - return nil, fmt.Errorf("not implemented") -} - -func (m *linuxApp) getScreenByIndex(display uintptr, index int) *Screen { - fmt.Println("getScreenByIndex") - var getMonitor func(uintptr, int) uintptr - purego.RegisterLibFunc(&getMonitor, gtk, "gdk_display_get_monitor") - - monitor := getMonitor(display, index) - - // TODO: Do we need to update Screen to contain current info? - // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) - - var getGeometry func(uintptr, uintptr) - purego.RegisterLibFunc(&getGeometry, gtk, "gdk_monitor_get_geometry") - - //var geometry C.GdkRectangle - /* - struct GdkRectangle { - int x; - int y; - int width; - int height; - } - */ - geometry := make([]byte, 16) - getGeometry(monitor, uintptr(unsafe.Pointer(&geometry[0]))) - fmt.Println("geometry: %v\n", geometry) - - var isPrimary func(uintptr) int - purego.RegisterLibFunc(&isPrimary, gtk, "gdk_monitor_is_primary") - - primary := false - if isPrimary(monitor) == 1 { - primary = true - } - - return &Screen{ - IsPrimary: primary, - Scale: 1.0, - X: 0, //int(geometry.x), - Y: 0, //int(geometry.y), - Size: Size{ - Height: 1024, //int(geometry.height), - Width: 1024, //int(geometry.width), - }, - } -} - -func (m *linuxApp) getScreens() ([]*Screen, error) { - fmt.Println("getScreens") - var wg sync.WaitGroup - var screens []*Screen - wg.Add(1) - - var getWindow func(uintptr) uintptr - purego.RegisterLibFunc(&getWindow, gtk, "gtk_application_get_active_window") - var getDisplay func(uintptr) uintptr - purego.RegisterLibFunc(&getDisplay, gtk, "gdk_window_get_display") - var getMonitorCount func(uintptr) int - purego.RegisterLibFunc(&getMonitorCount, gtk, "getNMonitors") - globalApplication.dispatchOnMainThread(func() { - window := getWindow(m.application) - display := getDisplay(window) - count := getMonitorCount(display) - for i := 0; i < int(count); i++ { - screens = append(screens, - m.getScreenByIndex(display, i), - ) - } - wg.Done() - }) - wg.Wait() - return screens, nil -} - -func getScreenForWindow(window *linuxWebviewWindow) (*Screen, error) { - return window.getScreen() -} diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index 5e68e312c..fdf125a90 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -130,10 +130,6 @@ func (s *macosSystemTray) setIcon(icon []byte) { }) } -func (s *macosSystemTray) setDarkModeIcon(icon []byte) { - s.setIcon(icon) -} - func (s *macosSystemTray) setTemplateIcon(icon []byte) { s.icon = icon s.isTemplateIcon = true diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go deleted file mode 100644 index 8cc74460a..000000000 --- a/v3/pkg/application/systemtray_linux.go +++ /dev/null @@ -1,90 +0,0 @@ -//go:build linux - -package application - -type linuxSystemTray struct { - id uint - label string - icon []byte - menu *Menu - - iconPosition int - isTemplateIcon bool -} - -func (s *linuxSystemTray) setIconPosition(position int) { - s.iconPosition = position -} - -func (s *linuxSystemTray) setMenu(menu *Menu) { - s.menu = menu -} - -func (s *linuxSystemTray) run() { - globalApplication.dispatchOnMainThread(func() { - // if s.nsStatusItem != nil { - // Fatal("System tray '%d' already running", s.id) - // } - // s.nsStatusItem = unsafe.Pointer(C.systemTrayNew()) - if s.label != "" { - // C.systemTraySetLabel(s.nsStatusItem, C.CString(s.label)) - } - if s.icon != nil { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&s.icon[0]), C.int(len(s.icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) - } - if s.menu != nil { - s.menu.Update() - // Convert impl to macosMenu object - // s.nsMenu = (s.menu.impl).(*macosMenu).nsMenu - // C.systemTraySetMenu(s.nsStatusItem, s.nsMenu) - } - - }) -} - -func (s *linuxSystemTray) setIcon(icon []byte) { - s.icon = icon - globalApplication.dispatchOnMainThread(func() { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) - }) -} - -func (s *linuxSystemTray) setDarkModeIcon(icon []byte) { - s.icon = icon - globalApplication.dispatchOnMainThread(func() { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) - }) -} - -func (s *linuxSystemTray) setTemplateIcon(icon []byte) { - s.icon = icon - s.isTemplateIcon = true - globalApplication.dispatchOnMainThread(func() { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) - }) -} - -func newSystemTrayImpl(s *SystemTray) systemTrayImpl { - return &linuxSystemTray{ - id: s.id, - label: s.label, - icon: s.icon, - menu: s.menu, - iconPosition: s.iconPosition, - isTemplateIcon: s.isTemplateIcon, - } -} - -func (s *linuxSystemTray) setLabel(label string) { - s.label = label - // C.systemTraySetLabel(s.nsStatusItem, C.CString(label)) -} - -func (s *linuxSystemTray) destroy() { - // Remove the status item from the status bar and its associated menu - // C.systemTrayDestroy(s.nsStatusItem) -} diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 857787f27..1fdf8777d 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -1,7 +1,6 @@ package application import ( - "encoding/json" "errors" "fmt" "github.com/samber/lo" @@ -150,39 +149,6 @@ func (w *WebviewWindow) addCancellationFunction(canceller func()) { w.cancellers = append(w.cancellers, canceller) } -// formatJS ensures the 'data' provided marshals to valid json or panics -func (w *WebviewWindow) formatJS(f string, callID string, data string) string { - j, err := json.Marshal(data) - if err != nil { - panic(err) - } - return fmt.Sprintf(f, callID, j) -} - -func (w *WebviewWindow) CallError(callID *string, result string) { - if w.impl != nil { - w.impl.execJS(w.formatJS("_wails.callErrorCallback('%s', %s);", *callID, result)) - } -} - -func (w *WebviewWindow) CallResponse(callID *string, result string) { - if w.impl != nil { - w.impl.execJS(w.formatJS("_wails.callCallback('%s', %s, true);", *callID, result)) - } -} - -func (w *WebviewWindow) DialogError(dialogID *string, result string) { - if w.impl != nil { - w.impl.execJS(w.formatJS("_wails.dialogErrorCallback('%s', %s);", *dialogID, result)) - } -} - -func (w *WebviewWindow) DialogResponse(dialogID *string, result string) { - if w.impl != nil { - w.impl.execJS(w.formatJS("_wails.dialogCallback('%s', %s, true);", *dialogID, result)) - } -} - // SetTitle sets the title of the window func (w *WebviewWindow) SetTitle(title string) *WebviewWindow { w.options.Title = title diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go deleted file mode 100644 index 7cc9843f0..000000000 --- a/v3/pkg/application/webview_window_linux.go +++ /dev/null @@ -1,797 +0,0 @@ -//go:build linux && !purego - -package application - -/* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 - -#include -#include -#include -#include -#include -#include - - -// exported below -extern gboolean buttonEvent(GtkWidget *widget, GdkEventButton *event, gpointer user_data); - extern void processRequest(void *request, gpointer user_data); -extern void onDragNDrop( - void *target, - GdkDragContext* context, - gint x, - gint y, - gpointer seldata, - guint info, - guint time, - gpointer data); -// exported below (end) - -static void signal_connect(GtkWidget *widget, char *event, void *cb, void* data) { - // g_signal_connect is a macro and can't be called directly - g_signal_connect(widget, event, cb, data); -} -*/ -import "C" - -import ( - "fmt" - "net/url" - "strings" - "sync" - "unsafe" - - "github.com/wailsapp/wails/v2/pkg/assetserver/webview" - "github.com/wailsapp/wails/v2/pkg/menu" - "github.com/wailsapp/wails/v3/pkg/events" -) - -var showDevTools = func(window unsafe.Pointer) {} - -func gtkBool(input bool) C.gboolean { - if input { - return C.gboolean(1) - } - return C.gboolean(0) -} - -type dragInfo struct { - XRoot int - YRoot int - DragTime int - MouseButton uint -} - -type linuxWebviewWindow struct { - id uint - application unsafe.Pointer - window unsafe.Pointer - webview unsafe.Pointer - parent *WebviewWindow - menubar *C.GtkWidget - vbox *C.GtkWidget - menu *menu.Menu - accels *C.GtkAccelGroup - lastWidth int - lastHeight int - drag dragInfo -} - -var ( - registered bool = false // avoid 'already registered message' about 'wails://' -) - -//export buttonEvent -func buttonEvent(_ *C.GtkWidget, event *C.GdkEventButton, data unsafe.Pointer) C.gboolean { - // Constants (defined here to be easier to use with ) - GdkButtonPress := C.GDK_BUTTON_PRESS // 4 - Gdk2ButtonPress := C.GDK_2BUTTON_PRESS // 5 for double-click - GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7 - - windowId := uint(*((*C.uint)(data))) - window := globalApplication.getWindowForID(windowId) - if window == nil { - return C.gboolean(0) - } - lw, ok := (window.impl).(*linuxWebviewWindow) - if !ok { - return C.gboolean(0) - } - - if event == nil { - return C.gboolean(0) - } - if event.button == 3 { - return C.gboolean(0) - } - - switch int(event._type) { - case GdkButtonPress: - lw.startDrag(uint(event.button), int(event.x_root), int(event.y_root)) - case Gdk2ButtonPress: - fmt.Printf("%d - button %d - double-clicked\n", windowId, int(event.button)) - case GdkButtonRelease: - lw.endDrag(uint(event.button), int(event.x_root), int(event.y_root)) - } - - return C.gboolean(0) -} - -func (w *linuxWebviewWindow) startDrag(button uint, x, y int) { - fmt.Println("startDrag ", button, x, y) - w.drag.XRoot = x - w.drag.YRoot = y -} - -func (w *linuxWebviewWindow) endDrag(button uint, x, y int) { - fmt.Println("endDrag", button, x, y) -} - -//export onDragNDrop -func onDragNDrop(target unsafe.Pointer, context *C.GdkDragContext, x C.gint, y C.gint, seldata unsafe.Pointer, info C.guint, time C.guint, data unsafe.Pointer) { - fmt.Println("target", target, info) - var length C.gint - selection := unsafe.Pointer(C.gtk_selection_data_get_data_with_length((*C.GtkSelectionData)(seldata), &length)) - extracted := C.g_uri_list_extract_uris((*C.char)(selection)) - defer C.g_strfreev(extracted) - - uris := unsafe.Slice( - (**C.char)(unsafe.Pointer(extracted)), - int(length)) - - var filenames []string - for _, uri := range uris { - if uri == nil { - break - } - filenames = append(filenames, strings.TrimPrefix(C.GoString(uri), "file://")) - } - windowDragAndDropBuffer <- &dragAndDropMessage{ - windowId: uint(*((*C.uint)(data))), - filenames: filenames, - } - C.gtk_drag_finish(context, C.true, C.false, time) -} - -//export processRequest -func processRequest(request unsafe.Pointer, data unsafe.Pointer) { - windowId := uint(*((*C.uint)(data))) - webviewRequests <- &webViewAssetRequest{ - Request: webview.NewRequest(request), - windowId: windowId, - windowName: globalApplication.getWindowForID(windowId).Name(), - } -} - -func (w *linuxWebviewWindow) enableDND() { - dnd := C.CString("text/uri-list") - defer C.free(unsafe.Pointer(dnd)) - targetentry := C.gtk_target_entry_new(dnd, 0, C.guint(w.parent.id)) - defer C.gtk_target_entry_free(targetentry) - C.gtk_drag_dest_set((*C.GtkWidget)(w.webview), C.GTK_DEST_DEFAULT_DROP, targetentry, 1, C.GDK_ACTION_COPY) - event := C.CString("drag-data-received") - defer C.free(unsafe.Pointer(event)) - id := C.uint(w.parent.id) - C.signal_connect((*C.GtkWidget)(unsafe.Pointer(w.webview)), event, C.onDragNDrop, unsafe.Pointer(C.gpointer(&id))) -} - -func (w *linuxWebviewWindow) newWebview(gpuPolicy int) unsafe.Pointer { - manager := C.webkit_user_content_manager_new() - external := C.CString("external") - C.webkit_user_content_manager_register_script_message_handler(manager, external) - - C.free(unsafe.Pointer(external)) - webview := C.webkit_web_view_new_with_user_content_manager(manager) - id := C.uint(w.parent.id) - if !registered { - wails := C.CString("wails") - C.webkit_web_context_register_uri_scheme( - C.webkit_web_context_get_default(), - wails, - C.WebKitURISchemeRequestCallback(C.processRequest), - C.gpointer(&id), - nil) - registered = true - C.free(unsafe.Pointer(wails)) - } - settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(unsafe.Pointer(webview))) - wails_io := C.CString("wails.io") - empty := C.CString("") - defer C.free(unsafe.Pointer(wails_io)) - defer C.free(unsafe.Pointer(empty)) - C.webkit_settings_set_user_agent_with_application_details(settings, wails_io, empty) - - switch gpuPolicy { - case 0: - C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS) - break - case 1: - C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND) - break - case 2: - C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER) - break - default: - C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND) - } - return unsafe.Pointer(webview) -} - -func (w *linuxWebviewWindow) connectSignals() { - event := C.CString("delete-event") - defer C.free(unsafe.Pointer(event)) - - // Window close handler - - if w.parent.options.HideOnClose { - C.signal_connect((*C.GtkWidget)(w.window), event, C.gtk_widget_hide_on_delete, C.NULL) - } else { - - // C.signal_connect((*C.GtkWidget)(window), event, C.close_button_pressed, w.parent.id) - } - /* - event = C.CString("load-changed") - defer C.free(unsafe.Pointer(event)) - C.signal_connect(webview, event, C.webviewLoadChanged, unsafe.Pointer(&w.parent.id)) - */ - id := C.uint(w.parent.id) - event = C.CString("button-press-event") - C.signal_connect((*C.GtkWidget)(unsafe.Pointer(w.webview)), event, C.buttonEvent, unsafe.Pointer(&id)) - C.free(unsafe.Pointer(event)) - event = C.CString("button-release-event") - defer C.free(unsafe.Pointer(event)) - C.signal_connect((*C.GtkWidget)(unsafe.Pointer(w.webview)), event, C.buttonEvent, unsafe.Pointer(&id)) -} - -func (w *linuxWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) { - // Create the menu - thisMenu := newMenuImpl(menu) - thisMenu.update() - fmt.Println("linux.openContextMenu()") - /* void - gtk_menu_popup_at_rect ( - GtkMenu* menu, - GdkWindow* rect_window, - const GdkRectangle* rect, - GdkGravity rect_anchor, - GdkGravity menu_anchor, - const GdkEvent* trigger_event - ) - */ -} - -func (w *linuxWebviewWindow) getZoom() float64 { - return float64(C.webkit_web_view_get_zoom_level((*C.WebKitWebView)(w.webview))) -} - -func (w *linuxWebviewWindow) setZoom(zoom float64) { - C.webkit_web_view_set_zoom_level((*C.WebKitWebView)(w.webview), C.double(zoom)) -} - -func (w *linuxWebviewWindow) setFrameless(frameless bool) { - if frameless { - C.gtk_window_set_decorated((*C.GtkWindow)(w.window), C.gboolean(0)) - } else { - C.gtk_window_set_decorated((*C.GtkWindow)(w.window), C.gboolean(1)) - // TODO: Deal with transparency for the titlebar if possible - // Perhaps we just make it undecorated and add a menu bar inside? - } -} - -func (w *linuxWebviewWindow) getScreen() (*Screen, error) { - mx, my, width, height, scale := w.getCurrentMonitorGeometry() - return &Screen{ - ID: fmt.Sprintf("%d", w.id), // A unique identifier for the display - Name: w.parent.Name(), // The name of the display - Scale: float32(scale), // The scale factor of the display - X: mx, // The x-coordinate of the top-left corner of the rectangle - Y: my, // The y-coordinate of the top-left corner of the rectangle - Size: Size{Width: width, Height: height}, // The size of the display - Bounds: Rect{}, // The bounds of the display - WorkArea: Rect{}, // The work area of the display - IsPrimary: false, // Whether this is the primary display - Rotation: 0.0, // The rotation of the display - }, nil -} - -func (w *linuxWebviewWindow) show() { - globalApplication.dispatchOnMainThread(func() { - C.gtk_widget_show_all((*C.GtkWidget)(w.window)) - }) -} - -func (w *linuxWebviewWindow) hide() { - C.gtk_widget_hide((*C.GtkWidget)(w.window)) -} - -func (w *linuxWebviewWindow) isNormal() bool { - return !w.isMinimised() && !w.isMaximised() && !w.isFullscreen() -} - -func (w *linuxWebviewWindow) isVisible() bool { - if C.gtk_widget_is_visible((*C.GtkWidget)(w.window)) == 1 { - return true - } - return false -} - -func (w *linuxWebviewWindow) setFullscreenButtonEnabled(enabled bool) { - // C.setFullscreenButtonEnabled(w.nsWindow, C.bool(enabled)) - fmt.Println("setFullscreenButtonEnabled - not implemented") -} - -func (w *linuxWebviewWindow) disableSizeConstraints() { - x, y, width, height, scale := w.getCurrentMonitorGeometry() - w.setMinMaxSize(x, y, width*scale, height*scale) -} - -func (w *linuxWebviewWindow) unfullscreen() { - fmt.Println("unfullscreen") - globalApplication.dispatchOnMainThread(func() { - C.gtk_window_unfullscreen((*C.GtkWindow)(w.window)) - w.unmaximise() - }) -} - -func (w *linuxWebviewWindow) fullscreen() { - w.maximise() - w.lastWidth, w.lastHeight = w.size() - globalApplication.dispatchOnMainThread(func() { - x, y, width, height, scale := w.getCurrentMonitorGeometry() - if x == -1 && y == -1 && width == -1 && height == -1 { - return - } - w.setMinMaxSize(0, 0, width*scale, height*scale) - w.setSize(width*scale, height*scale) - C.gtk_window_fullscreen((*C.GtkWindow)(w.window)) - w.setRelativePosition(0, 0) - }) -} - -func (w *linuxWebviewWindow) setEnabled(enabled bool) { - globalApplication.dispatchOnMainThread(func() { - C.gtk_widget_set_sensitive((*C.GtkWidget)(w.window), C.gboolean(enabled)) - }) -} - -func (w *linuxWebviewWindow) unminimise() { - C.gtk_window_present((*C.GtkWindow)(w.window)) - // gtk_window_unminimize ((*C.GtkWindow)(w.window)) /// gtk4 -} - -func (w *linuxWebviewWindow) unmaximise() { - C.gtk_window_unmaximize((*C.GtkWindow)(w.window)) -} - -func (w *linuxWebviewWindow) maximise() { - C.gtk_window_maximize((*C.GtkWindow)(w.window)) -} - -func (w *linuxWebviewWindow) minimise() { - C.gtk_window_iconify((*C.GtkWindow)(w.window)) -} - -func (w *linuxWebviewWindow) on(eventID uint) { - // Don't think this is correct! - // GTK Events are strings - fmt.Println("on()", eventID) - //C.registerListener(C.uint(eventID)) -} - -func (w *linuxWebviewWindow) zoom() { - w.zoomIn() -} - -func (w *linuxWebviewWindow) windowZoom() { - w.zoom() // FIXME> This should be removed -} - -func (w *linuxWebviewWindow) close() { - C.gtk_window_close((*C.GtkWindow)(w.window)) - if !w.parent.options.HideOnClose { - globalApplication.deleteWindowByID(w.parent.id) - } -} - -func (w *linuxWebviewWindow) zoomIn() { - lvl := C.webkit_web_view_get_zoom_level((*C.WebKitWebView)(w.webview)) - C.webkit_web_view_set_zoom_level((*C.WebKitWebView)(w.webview), lvl+0.5) -} - -func (w *linuxWebviewWindow) zoomOut() { - lvl := C.webkit_web_view_get_zoom_level((*C.WebKitWebView)(w.webview)) - C.webkit_web_view_set_zoom_level((*C.WebKitWebView)(w.webview), lvl-0.5) -} - -func (w *linuxWebviewWindow) zoomReset() { - C.webkit_web_view_set_zoom_level((*C.WebKitWebView)(w.webview), 0.0) -} - -func (w *linuxWebviewWindow) reload() { - // TODO: This should be a constant somewhere I feel - uri := C.CString("wails://") - C.webkit_web_view_load_uri((*C.WebKitWebView)(w.window), uri) - C.free(unsafe.Pointer(uri)) -} - -func (w *linuxWebviewWindow) forceReload() { - w.reload() -} - -func (w linuxWebviewWindow) getCurrentMonitor() *C.GdkMonitor { - // Get the monitor that the window is currently on - display := C.gtk_widget_get_display((*C.GtkWidget)(w.window)) - gdk_window := C.gtk_widget_get_window((*C.GtkWidget)(w.window)) - if gdk_window == nil { - return nil - } - return C.gdk_display_get_monitor_at_window(display, gdk_window) -} - -func (w linuxWebviewWindow) getCurrentMonitorGeometry() (x int, y int, width int, height int, scale int) { - monitor := w.getCurrentMonitor() - if monitor == nil { - return -1, -1, -1, -1, 1 - } - var result C.GdkRectangle - C.gdk_monitor_get_geometry(monitor, &result) - scale = int(C.gdk_monitor_get_scale_factor(monitor)) - return int(result.x), int(result.y), int(result.width), int(result.height), scale -} - -func (w *linuxWebviewWindow) center() { - globalApplication.dispatchOnMainThread(func() { - x, y, width, height, _ := w.getCurrentMonitorGeometry() - if x == -1 && y == -1 && width == -1 && height == -1 { - return - } - - var windowWidth C.int - var windowHeight C.int - C.gtk_window_get_size((*C.GtkWindow)(w.window), &windowWidth, &windowHeight) - - newX := C.int(((width - int(windowWidth)) / 2) + x) - newY := C.int(((height - int(windowHeight)) / 2) + y) - - // Place the window at the center of the monitor - C.gtk_window_move((*C.GtkWindow)(w.window), newX, newY) - }) -} - -func (w *linuxWebviewWindow) isMinimised() bool { - gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(w.window)) - state := C.gdk_window_get_state(gdkwindow) - return state&C.GDK_WINDOW_STATE_ICONIFIED > 0 -} - -func (w *linuxWebviewWindow) isMaximised() bool { - return w.syncMainThreadReturningBool(func() bool { - gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(w.window)) - state := C.gdk_window_get_state(gdkwindow) - return state&C.GDK_WINDOW_STATE_MAXIMIZED > 0 && state&C.GDK_WINDOW_STATE_FULLSCREEN == 0 - }) -} - -func (w *linuxWebviewWindow) isFullscreen() bool { - return w.syncMainThreadReturningBool(func() bool { - gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(w.window)) - state := C.gdk_window_get_state(gdkwindow) - return state&C.GDK_WINDOW_STATE_FULLSCREEN > 0 - }) -} - -func (w *linuxWebviewWindow) syncMainThreadReturningBool(fn func() bool) bool { - var wg sync.WaitGroup - wg.Add(1) - var result bool - globalApplication.dispatchOnMainThread(func() { - result = fn() - wg.Done() - }) - wg.Wait() - return result -} - -func (w *linuxWebviewWindow) restore() { - // restore window to normal size - // FIXME: never called! - remove from webviewImpl interface -} - -func (w *linuxWebviewWindow) execJS(js string) { - value := C.CString(js) - C.webkit_web_view_evaluate_javascript((*C.WebKitWebView)(w.webview), - value, - C.long(len(js)), - nil, - C.CString(""), - nil, - nil, - nil) - C.free(unsafe.Pointer(value)) -} - -func (w *linuxWebviewWindow) setURL(uri string) { - if uri != "" { - url, err := url.Parse(uri) - if err == nil && url.Scheme == "" && url.Host == "" { - // TODO handle this in a central location, the scheme and host might be platform dependant. - url.Scheme = "wails" - url.Host = "wails" - uri = url.String() - } - } - target := C.CString(uri) - C.webkit_web_view_load_uri((*C.WebKitWebView)(w.webview), target) - C.free(unsafe.Pointer(target)) -} - -func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { - C.gtk_window_set_keep_above((*C.GtkWindow)(w.window), gtkBool(alwaysOnTop)) -} - -func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { - // (*C.struct__GtkWidget)(m.native) - //var menubar *C.struct__GtkWidget - return &linuxWebviewWindow{ - application: getNativeApplication().application, - parent: parent, - // menubar: menubar, - } -} - -func (w *linuxWebviewWindow) setTitle(title string) { - if !w.parent.options.Frameless { - cTitle := C.CString(title) - C.gtk_window_set_title((*C.GtkWindow)(w.window), cTitle) - C.free(unsafe.Pointer(cTitle)) - } -} - -func (w *linuxWebviewWindow) setSize(width, height int) { - C.gtk_window_resize((*C.GtkWindow)(w.window), C.gint(width), C.gint(height)) -} - -func (w *linuxWebviewWindow) setMinMaxSize(minWidth, minHeight, maxWidth, maxHeight int) { - fmt.Println("setMinMaxSize", minWidth, minHeight, maxWidth, maxHeight) - if minWidth == 0 { - minWidth = -1 - } - if minHeight == 0 { - minHeight = -1 - } - if maxWidth == 0 { - maxWidth = -1 - } - if maxHeight == 0 { - maxHeight = -1 - } - size := C.GdkGeometry{ - min_width: C.int(minWidth), - min_height: C.int(minHeight), - max_width: C.int(maxWidth), - max_height: C.int(maxHeight), - } - C.gtk_window_set_geometry_hints((*C.GtkWindow)(w.window), nil, &size, C.GDK_HINT_MAX_SIZE|C.GDK_HINT_MIN_SIZE) -} - -func (w *linuxWebviewWindow) setMinSize(width, height int) { - w.setMinMaxSize(width, height, w.parent.options.MaxWidth, w.parent.options.MaxHeight) -} - -func (w *linuxWebviewWindow) setMaxSize(width, height int) { - w.setMinMaxSize(w.parent.options.MinWidth, w.parent.options.MinHeight, width, height) -} - -func (w *linuxWebviewWindow) setResizable(resizable bool) { - if resizable { - C.gtk_window_set_resizable((*C.GtkWindow)(w.window), 1) - } else { - C.gtk_window_set_resizable((*C.GtkWindow)(w.window), 0) - } -} - -func (w *linuxWebviewWindow) toggleDevTools() { - settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(w.webview)) - enabled := C.webkit_settings_get_enable_developer_extras(settings) - if enabled == C.int(0) { - enabled = C.int(1) - } else { - enabled = C.int(0) - } - C.webkit_settings_set_enable_developer_extras(settings, enabled) -} - -func (w *linuxWebviewWindow) size() (int, int) { - var width, height C.int - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - C.gtk_window_get_size((*C.GtkWindow)(w.window), &width, &height) - wg.Done() - }) - wg.Wait() - return int(width), int(height) -} - -func (w *linuxWebviewWindow) setRelativePosition(x, y int) { - mx, my, _, _, _ := w.getCurrentMonitorGeometry() - globalApplication.dispatchOnMainThread(func() { - C.gtk_window_move((*C.GtkWindow)(w.window), C.int(x+mx), C.int(y+my)) - }) -} - -func (w *linuxWebviewWindow) width() int { - width, _ := w.size() - return width -} - -func (w *linuxWebviewWindow) height() int { - _, height := w.size() - return height -} - -func (w *linuxWebviewWindow) absolutePosition() (int, int) { - var x, y C.int - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - C.gtk_window_get_position((*C.GtkWindow)(w.window), &x, &y) - wg.Done() - }) - wg.Wait() - return int(x), int(y) -} - -func (w *linuxWebviewWindow) run() { - for eventId := range w.parent.eventListeners { - w.on(eventId) - } - - app := getNativeApplication() - menu := app.applicationMenu - - globalApplication.dispatchOnMainThread(func() { - w.window = unsafe.Pointer(C.gtk_application_window_new((*C.GtkApplication)(w.application))) - app.registerWindow((*C.GtkWindow)(w.window), w.parent.id) // record our mapping - C.g_object_ref_sink(C.gpointer(w.window)) - w.webview = w.newWebview(1) - w.connectSignals() - if w.parent.options.EnableDragAndDrop { - w.enableDND() - } - w.vbox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0) - C.gtk_container_add((*C.GtkContainer)(w.window), w.vbox) - if menu != nil { - C.gtk_box_pack_start((*C.GtkBox)(unsafe.Pointer(w.vbox)), (*C.GtkWidget)(menu), 0, 0, 0) - } - C.gtk_box_pack_start((*C.GtkBox)(unsafe.Pointer(w.vbox)), (*C.GtkWidget)(w.webview), 1, 1, 0) - - w.setTitle(w.parent.options.Title) - w.setAlwaysOnTop(w.parent.options.AlwaysOnTop) - w.setResizable(!w.parent.options.DisableResize) - // only set min/max size if actually set - if w.parent.options.MinWidth != 0 && - w.parent.options.MinHeight != 0 && - w.parent.options.MaxWidth != 0 && - w.parent.options.MaxHeight != 0 { - w.setMinMaxSize( - w.parent.options.MinWidth, - w.parent.options.MinHeight, - w.parent.options.MaxWidth, - w.parent.options.MaxHeight, - ) - } - w.setSize(w.parent.options.Width, w.parent.options.Height) - w.setZoom(w.parent.options.Zoom) - w.setBackgroundColour(w.parent.options.BackgroundColour) - w.setFrameless(w.parent.options.Frameless) - - if w.parent.options.X != 0 || w.parent.options.Y != 0 { - w.setRelativePosition(w.parent.options.X, w.parent.options.Y) - } else { - fmt.Println("attempting to set in the center") - w.center() - } - switch w.parent.options.StartState { - case WindowStateMaximised: - w.maximise() - case WindowStateMinimised: - w.minimise() - case WindowStateFullscreen: - w.fullscreen() - } - - if w.parent.options.URL != "" { - w.setURL(w.parent.options.URL) - } - // We need to wait for the HTML to load before we can execute the javascript - // FIXME: What event is this? DomReady? - w.parent.On(events.Mac.WebViewDidFinishNavigation, func(_ *WindowEventContext) { - if w.parent.options.JS != "" { - w.execJS(w.parent.options.JS) - } - if w.parent.options.CSS != "" { - js := fmt.Sprintf("(function() { var style = document.createElement('style'); style.appendChild(document.createTextNode('%s')); document.head.appendChild(style); })();", w.parent.options.CSS) - fmt.Println(js) - w.execJS(js) - } - }) - if w.parent.options.HTML != "" { - w.setHTML(w.parent.options.HTML) - } - if !w.parent.options.Hidden { - w.show() - if w.parent.options.X != 0 || w.parent.options.Y != 0 { - w.setRelativePosition(w.parent.options.X, w.parent.options.Y) - } else { - fmt.Println("attempting to set in the center") - w.center() - } - } - }) -} - -func (w *linuxWebviewWindow) setTransparent() { - screen := C.gtk_widget_get_screen((*C.GtkWidget)(w.window)) - visual := C.gdk_screen_get_rgba_visual(screen) - - if visual != nil && C.gdk_screen_is_composited(screen) == C.int(1) { - C.gtk_widget_set_app_paintable((*C.GtkWidget)(w.window), C.gboolean(1)) - C.gtk_widget_set_visual((*C.GtkWidget)(w.window), visual) - } -} - -func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { - if colour.Alpha != 0 { - w.setTransparent() - } - rgba := C.GdkRGBA{C.double(colour.Red) / 255.0, C.double(colour.Green) / 255.0, C.double(colour.Blue) / 255.0, C.double(colour.Alpha) / 255.0} - fmt.Println(unsafe.Pointer(&rgba)) - C.webkit_web_view_set_background_color((*C.WebKitWebView)(w.webview), &rgba) -} - -func (w *linuxWebviewWindow) relativePosition() (int, int) { - var x, y C.int - var wg sync.WaitGroup - wg.Add(1) - go globalApplication.dispatchOnMainThread(func() { - C.gtk_window_get_position((*C.GtkWindow)(w.window), &x, &y) - - // The position must be relative to the screen it is on - // We need to get the screen it is on - screen := C.gtk_widget_get_screen((*C.GtkWidget)(w.window)) - monitor := C.gdk_screen_get_monitor_at_window(screen, (*C.GdkWindow)(w.window)) - geometry := C.GdkRectangle{} - C.gdk_screen_get_monitor_geometry(screen, monitor, &geometry) - x = x - geometry.x - y = y - geometry.y - - // TODO: Scale based on DPI - - wg.Done() - }) - wg.Wait() - return int(x), int(y) -} - -func (w *linuxWebviewWindow) destroy() { - C.gtk_window_close((*C.GtkWindow)(w.window)) -} - -func (w *linuxWebviewWindow) setHTML(html string) { - cHTML := C.CString(html) - uri := C.CString("wails://") - empty := C.CString("") - defer C.free(unsafe.Pointer(cHTML)) - defer C.free(unsafe.Pointer(uri)) - defer C.free(unsafe.Pointer(empty)) - C.webkit_web_view_load_alternate_html( - (*C.WebKitWebView)(w.webview), - cHTML, - uri, - empty) -} - -func (w *linuxWebviewWindow) nativeWindowHandle() uintptr { - return uintptr(w.window) -} diff --git a/v3/pkg/application/webview_window_linux_purego.go b/v3/pkg/application/webview_window_linux_purego.go deleted file mode 100644 index fa9ff9f57..000000000 --- a/v3/pkg/application/webview_window_linux_purego.go +++ /dev/null @@ -1,764 +0,0 @@ -//go:build linux && purego - -package application - -import ( - "fmt" - "net/url" - "sync" - "unsafe" - - "github.com/ebitengine/purego" - "github.com/wailsapp/wails/v2/pkg/menu" - "github.com/wailsapp/wails/v3/pkg/events" -) - -var ( - registered bool = false // avoid 'already registered message' -) - -const ( - // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkwindow.h#L121 - GDK_HINT_MIN_SIZE = 1 << 1 - GDK_HINT_MAX_SIZE = 1 << 2 - // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkevents.h#L512 - GDK_WINDOW_STATE_ICONIFIED = 1 << 1 - GDK_WINDOW_STATE_MAXIMIZED = 1 << 2 - GDK_WINDOW_STATE_FULLSCREEN = 1 << 4 -) - -type GdkGeometry struct { - minWidth int32 - minHeight int32 - maxWidth int32 - maxHeight int32 - baseWidth int32 - baseHeight int32 - widthInc int32 - heightInc int32 - padding int32 - minAspect float64 - maxAspect float64 - GdkGravity int32 -} - -type linuxWebviewWindow struct { - application uintptr - window uintptr - webview uintptr - parent *WebviewWindow - menubar uintptr - vbox uintptr - menu *menu.Menu - accels uintptr - minWidth, minHeight, maxWidth, maxHeight int -} - -func (w *linuxWebviewWindow) newWebview(gpuPolicy int) uintptr { - var newContentMgr func() uintptr - purego.RegisterLibFunc( - &newContentMgr, - webkit, - "webkit_user_content_manager_new") - var registerScriptMessageHandler func(uintptr, string) - purego.RegisterLibFunc(®isterScriptMessageHandler, webkit, "webkit_user_content_manager_register_script_message_handler") - var newWebview func(uintptr) uintptr - purego.RegisterLibFunc(&newWebview, webkit, "webkit_web_view_new_with_user_content_manager") - - manager := newContentMgr() - registerScriptMessageHandler(manager, "external") - webview := newWebview(manager) - if !registered { - var registerUriScheme func(uintptr, string, uintptr, uintptr, uintptr) - purego.RegisterLibFunc(®isterUriScheme, webkit, "webkit_web_context_register_uri_scheme") - cb := purego.NewCallback(func(request uintptr) { - processURLRequest(w.parent.id, request) - }) - var defaultContext func() uintptr - purego.RegisterLibFunc(&defaultContext, webkit, "webkit_web_context_get_default") - registerUriScheme(defaultContext(), "wails", cb, 0, 0) - } - - var g_signal_connect func(uintptr, string, uintptr, uintptr, bool, int) int - purego.RegisterLibFunc(&g_signal_connect, gtk, "g_signal_connect_data") - - loadChanged := purego.NewCallback(func(window uintptr) { - //fmt.Println("loadChanged", window) - }) - if g_signal_connect(webview, "load-changed", loadChanged, 0, false, 0) == 0 { - fmt.Println("failed to connect 'load-changed' event") - } - - if g_signal_connect(webview, "button-press-event", purego.NewCallback(w.buttonPress), 0, false, 0) == 0 { - fmt.Println("failed to connect 'button-press-event") - } - if g_signal_connect(webview, "button-release-event", purego.NewCallback(w.buttonRelease), 0, false, 0) == 0 { - fmt.Println("failed to connect 'button-release-event") - } - - handleDelete := purego.NewCallback(func(uintptr) { - w.close() - if !w.parent.options.HideOnClose { - fmt.Println("Need to do more!") - } - }) - g_signal_connect(w.window, "delete-event", handleDelete, 0, false, 0) - - var getSettings func(uintptr) uintptr - purego.RegisterLibFunc(&getSettings, webkit, "webkit_web_view_get_settings") - var setSettings func(uintptr, uintptr) - purego.RegisterLibFunc(&setSettings, webkit, "webkit_web_view_set_settings") - var setUserAgent func(uintptr, string, string) - purego.RegisterLibFunc(&setUserAgent, webkit, "webkit_settings_set_user_agent_with_application_details") - settings := getSettings(webview) - setUserAgent(settings, "wails.io", "") - - var setHWAccel func(uintptr, int) - purego.RegisterLibFunc(&setHWAccel, webkit, "webkit_settings_set_hardware_acceleration_policy") - - setHWAccel(settings, gpuPolicy) - setSettings(webview, settings) - - return webview -} - -func (w *linuxWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) { - // Create the menu - thisMenu := newMenuImpl(menu) - thisMenu.update() - fmt.Println("linux.openContextMenu()") - //C.windowShowMenu(w.nsWindow, thisMenu.nsMenu, C.int(data.X), C.int(data.Y)) -} - -func (w *linuxWebviewWindow) getZoom() float64 { - var getZoom func(uintptr) float32 - purego.RegisterLibFunc(&getZoom, webkit, "webkit_web_view_get_zoom_level") - return float64(getZoom(w.webview)) -} - -func (w *linuxWebviewWindow) setZoom(zoom float64) { - var setZoom func(uintptr, float64) - purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") - setZoom(w.webview, zoom) -} - -func (w *linuxWebviewWindow) setFrameless(frameless bool) { - var setDecorated func(uintptr, int) - purego.RegisterLibFunc(&setDecorated, gtk, "gtk_window_set_decorated") - decorated := 1 - if frameless { - decorated = 0 - } - setDecorated(w.window, decorated) - if !frameless { - // TODO: Deal with transparency for the titlebar if possible - // Perhaps we just make it undecorated and add a menu bar inside? - } -} - -func (w *linuxWebviewWindow) getScreen() (*Screen, error) { - return getScreenForWindow(w) -} - -func (w *linuxWebviewWindow) show() { - var widgetShow func(uintptr) - purego.RegisterLibFunc(&widgetShow, gtk, "gtk_widget_show_all") - globalApplication.dispatchOnMainThread(func() { - widgetShow(w.window) - }) -} - -func (w *linuxWebviewWindow) hide() { - var widgetHide func(uintptr) - purego.RegisterLibFunc(&widgetHide, gtk, "gtk_widget_hide") - widgetHide(w.window) -} - -func (w *linuxWebviewWindow) setFullscreenButtonEnabled(enabled bool) { - // C.setFullscreenButtonEnabled(w.nsWindow, C.bool(enabled)) - fmt.Println("setFullscreenButtonEnabled - not implemented") - -} - -func (w *linuxWebviewWindow) disableSizeConstraints() { - x, y, width, height, scale := w.getCurrentMonitorGeometry() - w.setMinMaxSize(x, y, width*scale, height*scale) -} - -func (w *linuxWebviewWindow) unfullscreen() { - var unfullScreen func(uintptr) - purego.RegisterLibFunc(&unfullScreen, gtk, "gtk_window_unfullscreen") - - globalApplication.dispatchOnMainThread(func() { - unfullScreen(w.window) - w.unmaximise() - }) -} - -func (w *linuxWebviewWindow) setEnabled(enabled bool) { - var gtkWidgetSensitive func(uintptr, int) - purego.RegisterLibFunc(>kWidgetSensitive, gtk, "gtk_widget_set_sensitive") - globalApplication.dispatchOnMainThread(func() { - gtkWidgetSensitive(w.window, boolToInt(enabled)) - }) -} - -func (w *linuxWebviewWindow) fullscreen() { - var fullScreen func(uintptr) - purego.RegisterLibFunc(&fullScreen, gtk, "gtk_window_fullscreen") - - globalApplication.dispatchOnMainThread(func() { - w.maximise() - // w.lastWidth, w.lastHeight = w.size() // do we need this? - - x, y, width, height, scale := w.getCurrentMonitorGeometry() - if x == -1 && y == -1 && width == -1 && height == -1 { - return - } - w.setMinMaxSize(0, 0, width*scale, height*scale) - w.setSize(width*scale, height*scale) - w.setRelativePosition(0, 0) - fullScreen(w.window) - }) -} - -func (w *linuxWebviewWindow) unminimise() { - var present func(uintptr) - purego.RegisterLibFunc(&present, gtk, "gtk_window_present") - present(w.window) -} - -func (w *linuxWebviewWindow) unmaximise() { - var unmaximize func(uintptr) - purego.RegisterLibFunc(&unmaximize, gtk, "gtk_window_unmaximize") - unmaximize(w.window) -} - -func (w *linuxWebviewWindow) maximise() { - var maximize func(uintptr) - purego.RegisterLibFunc(&maximize, gtk, "gtk_window_maximize") - maximize(w.window) -} - -func (w *linuxWebviewWindow) minimise() { - var iconify func(uintptr) - purego.RegisterLibFunc(&iconify, gtk, "gtk_window_iconify") - iconify(w.window) -} - -func (w *linuxWebviewWindow) on(eventID uint) { - // Don't think this is correct! - // GTK Events are strings - fmt.Println("on()", eventID) - //C.registerListener(C.uint(eventID)) -} - -func (w *linuxWebviewWindow) zoom() { - w.zoomIn() -} - -func (w *linuxWebviewWindow) windowZoom() { - w.zoom() -} - -func (w *linuxWebviewWindow) close() { - var close func(uintptr) - purego.RegisterLibFunc(&close, gtk, "gtk_window_close") - close(w.window) -} - -func (w *linuxWebviewWindow) zoomIn() { - var getZoom func(uintptr) float32 - purego.RegisterLibFunc(&getZoom, webkit, "webkit_web_view_get_zoom_level") - var setZoom func(uintptr, float32) - purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") - lvl := getZoom(w.webview) - setZoom(w.webview, lvl+0.5) -} - -func (w *linuxWebviewWindow) zoomOut() { - var getZoom func(uintptr) float32 - purego.RegisterLibFunc(&getZoom, webkit, "webkit_web_view_get_zoom_level") - var setZoom func(uintptr, float32) - purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") - lvl := getZoom(w.webview) - setZoom(w.webview, lvl-0.5) -} - -func (w *linuxWebviewWindow) zoomReset() { - var setZoom func(uintptr, float32) - purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") - setZoom(w.webview, 0.0) -} - -func (w *linuxWebviewWindow) toggleDevTools() { - var getSettings func(uintptr) uintptr - purego.RegisterLibFunc(&getSettings, webkit, "webkit_web_view_get_settings") - var isEnabled func(uintptr) bool - purego.RegisterLibFunc(&isEnabled, webkit, "webkit_settings_get_enable_developer_extras") - var enableDev func(uintptr, bool) - purego.RegisterLibFunc(&enableDev, webkit, "webkit_settings_set_enable_developer_extras") - settings := getSettings(w.webview) - enabled := isEnabled(settings) - enableDev(settings, !enabled) -} - -func (w *linuxWebviewWindow) reload() { - var reload func(uintptr) - purego.RegisterLibFunc(&reload, webkit, "webkit_web_view_reload") - reload(w.webview) -} - -func (w *linuxWebviewWindow) forceReload() { - var reload func(uintptr) - purego.RegisterLibFunc(&reload, webkit, "webkit_web_view_reload_bypass_cache") - reload(w.webview) -} - -func (w linuxWebviewWindow) getCurrentMonitor() uintptr { - var getDisplay func(uintptr) uintptr - purego.RegisterLibFunc(&getDisplay, gtk, "gtk_widget_get_display") - var getWindow func(uintptr) uintptr - purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") - var getMonitor func(uintptr, uintptr) uintptr - purego.RegisterLibFunc(&getMonitor, gtk, "gdk_display_get_monitor_at_window") - - display := getDisplay(w.window) - window := getWindow(w.window) - if window == 0 { - return 0 - } - return getMonitor(display, window) -} - -func (w linuxWebviewWindow) getCurrentMonitorGeometry() (x int, y int, width int, height int, scale int) { - var getGeometry func(uintptr, uintptr) - purego.RegisterLibFunc(&getGeometry, gtk, "gdk_monitor_get_geometry") - var getScaleFactor func(uintptr) int - purego.RegisterLibFunc(&getScaleFactor, gtk, "gdk_monitor_get_scale_factor") - - monitor := w.getCurrentMonitor() - if monitor == 0 { - return -1, -1, -1, -1, 1 - } - result := struct { - x int32 - y int32 - width int32 - height int32 - }{} - getGeometry(monitor, uintptr(unsafe.Pointer(&result))) - scale = getScaleFactor(monitor) - return int(result.x), int(result.y), int(result.width), int(result.height), scale -} - -func (w *linuxWebviewWindow) center() { - fmt.Println("attempting to set in the center") - - x, y, width, height, _ := w.getCurrentMonitorGeometry() - if x == -1 && y == -1 && width == -1 && height == -1 { - return - } - - windowWidth, windowHeight := w.size() - - newX := ((width - int(windowWidth)) / 2) + x - newY := ((height - int(windowHeight)) / 2) + y - - w.setRelativePosition(newX, newY) -} - -func (w *linuxWebviewWindow) isMinimised() bool { - var getWindow func(uintptr) uintptr - purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") - var getWindowState func(uintptr) int - purego.RegisterLibFunc(&getWindowState, gtk, "gdk_window_get_state") - - return w.syncMainThreadReturningBool(func() bool { - state := getWindowState(getWindow(w.window)) - return state&GDK_WINDOW_STATE_ICONIFIED > 0 - }) -} - -func (w *linuxWebviewWindow) isMaximised() bool { - var getWindow func(uintptr) uintptr - purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") - var getWindowState func(uintptr) int - purego.RegisterLibFunc(&getWindowState, gtk, "gdk_window_get_state") - - return w.syncMainThreadReturningBool(func() bool { - state := getWindowState(getWindow(w.window)) - return state&GDK_WINDOW_STATE_MAXIMIZED > 0 && state&GDK_WINDOW_STATE_FULLSCREEN == 0 - }) -} - -func (w *linuxWebviewWindow) isFullscreen() bool { - var getWindow func(uintptr) uintptr - purego.RegisterLibFunc(&getWindow, gtk, "gtk_widget_get_window") - var getWindowState func(uintptr) int - purego.RegisterLibFunc(&getWindowState, gtk, "gdk_window_get_state") - - return w.syncMainThreadReturningBool(func() bool { - state := getWindowState(getWindow(w.window)) - return state&GDK_WINDOW_STATE_FULLSCREEN > 0 - }) -} - -func (w *linuxWebviewWindow) syncMainThreadReturningBool(fn func() bool) bool { - var wg sync.WaitGroup - wg.Add(1) - var result bool - globalApplication.dispatchOnMainThread(func() { - result = fn() - wg.Done() - }) - wg.Wait() - return result -} - -func (w *linuxWebviewWindow) restore() { - // restore window to normal size - fmt.Println("restore") -} - -func (w *linuxWebviewWindow) execJS(js string) { - var evalJS func(uintptr, string, int, uintptr, string, uintptr, uintptr, uintptr) - purego.RegisterLibFunc(&evalJS, webkit, "webkit_web_view_evaluate_javascript") - evalJS(w.webview, js, len(js), 0, "", 0, 0, 0) -} - -func (w *linuxWebviewWindow) setURL(uri string) { - fmt.Println("setURL", uri) - var loadUri func(uintptr, string) - purego.RegisterLibFunc(&loadUri, webkit, "webkit_web_view_load_uri") - - url, err := url.Parse(uri) - if url != nil && err == nil && url.Scheme == "" && url.Host == "" { - // TODO handle this in a central location, the scheme and host might be platform dependant - url.Scheme = "wails" - url.Host = "wails" - uri = url.String() - loadUri(w.webview, uri) - } -} - -func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { - var keepAbove func(uintptr, bool) - purego.RegisterLibFunc(&keepAbove, gtk, "gtk_window_set_keep_above") - keepAbove(w.window, alwaysOnTop) -} - -func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { - return &linuxWebviewWindow{ - application: getNativeApplication().application, - parent: parent, - } -} - -func (w *linuxWebviewWindow) setTitle(title string) { - if !w.parent.options.Frameless { - var setTitle func(uintptr, string) - purego.RegisterLibFunc(&setTitle, gtk, "gtk_window_set_title") - setTitle(w.window, title) - } -} - -func (w *linuxWebviewWindow) setSize(width, height int) { - var setSize func(uintptr, int, int) - purego.RegisterLibFunc(&setSize, gtk, "gtk_window_set_default_size") - setSize(w.window, width, height) -} - -func (w *linuxWebviewWindow) setMinMaxSize(minWidth, minHeight, maxWidth, maxHeight int) { - fmt.Println("setMinMaxSize", minWidth, minHeight, maxWidth, maxHeight) - if minWidth == 0 { - minWidth = -1 - } - if minHeight == 0 { - minHeight = -1 - } - if maxWidth == 0 { - maxWidth = -1 - } - if maxHeight == 0 { - maxHeight = -1 - } - size := GdkGeometry{ - minWidth: int32(minWidth), - minHeight: int32(minHeight), - maxWidth: int32(maxWidth), - maxHeight: int32(maxHeight), - } - - var setHints func(uintptr, uintptr, uintptr, int) - purego.RegisterLibFunc(&setHints, gtk, "gtk_window_set_geometry_hints") - setHints(w.window, 0, uintptr(unsafe.Pointer(&size)), GDK_HINT_MIN_SIZE|GDK_HINT_MAX_SIZE) -} - -func (w *linuxWebviewWindow) setMinSize(width, height int) { - w.setMinMaxSize(width, height, w.parent.options.MaxWidth, w.parent.options.MaxHeight) -} - -func (w *linuxWebviewWindow) setMaxSize(width, height int) { - w.setMinMaxSize(w.parent.options.MinWidth, w.parent.options.MinHeight, width, height) -} - -func (w *linuxWebviewWindow) setResizable(resizable bool) { - var setResizable func(uintptr, int) - purego.RegisterLibFunc(&setResizable, gtk, "gtk_window_set_resizable") - globalApplication.dispatchOnMainThread(func() { - if resizable { - setResizable(w.window, 1) - } else { - setResizable(w.window, 0) - } - }) -} - -func (w *linuxWebviewWindow) size() (int, int) { - var width, height int - var windowGetSize func(uintptr, *int, *int) - purego.RegisterLibFunc(&windowGetSize, gtk, "gtk_window_get_size") - - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - windowGetSize(w.window, &width, &height) - wg.Done() - }) - wg.Wait() - return width, height -} - -func (w *linuxWebviewWindow) setRelativePosition(x, y int) { - var windowMove func(uintptr, int, int) - purego.RegisterLibFunc(&windowMove, gtk, "gtk_window_move") - mx, my, _, _, _ := w.getCurrentMonitorGeometry() - fmt.Println("setRelativePosition", mx, my) - globalApplication.dispatchOnMainThread(func() { - windowMove(w.window, x+mx, y+my) - }) -} - -func (w *linuxWebviewWindow) width() int { - width, _ := w.size() - return width -} - -func (w *linuxWebviewWindow) height() int { - _, height := w.size() - return height -} - -func (w *linuxWebviewWindow) buttonPress(widget uintptr, event uintptr, user_data uintptr) { - GdkEventButton := (*byte)(unsafe.Pointer(event)) - fmt.Println("buttonpress", w.parent.id, widget, GdkEventButton, user_data) -} - -func (w *linuxWebviewWindow) buttonRelease(widget uintptr, event uintptr, user_data uintptr) { - GdkEventButton := (*byte)(unsafe.Pointer(event)) - fmt.Println("buttonrelease", w.parent.id, widget, GdkEventButton, user_data) -} - -func (w *linuxWebviewWindow) run() { - for eventId := range w.parent.eventListeners { - w.on(eventId) - } - - globalApplication.dispatchOnMainThread(func() { - app := getNativeApplication() - menu := app.applicationMenu - var newWindow func(uintptr) uintptr - purego.RegisterLibFunc(&newWindow, gtk, "gtk_application_window_new") - var refSink func(uintptr) - purego.RegisterLibFunc(&refSink, gtk, "g_object_ref_sink") - var boxNew func(int, int) uintptr - purego.RegisterLibFunc(&boxNew, gtk, "gtk_box_new") - var containerAdd func(uintptr, uintptr) - purego.RegisterLibFunc(&containerAdd, gtk, "gtk_container_add") - var boxPackStart func(uintptr, uintptr, int, int, int) - purego.RegisterLibFunc(&boxPackStart, gtk, "gtk_box_pack_start") - - var g_signal_connect func(uintptr, string, uintptr, uintptr, bool, int) int - purego.RegisterLibFunc(&g_signal_connect, gtk, "g_signal_connect_data") - - w.window = newWindow(w.application) - - refSink(w.window) - w.webview = w.newWebview(1) - w.vbox = boxNew(1, 0) - containerAdd(w.window, w.vbox) - if menu != 0 { - w.menubar = menu - boxPackStart(w.vbox, menu, 0, 0, 0) - } - boxPackStart(w.vbox, w.webview, 1, 1, 0) - - w.setSize(w.parent.options.Width, w.parent.options.Height) - w.setTitle(w.parent.options.Title) - w.setAlwaysOnTop(w.parent.options.AlwaysOnTop) - w.setResizable(!w.parent.options.DisableResize) - if w.parent.options.MinWidth != 0 && - w.parent.options.MinHeight != 0 && - w.parent.options.MaxWidth != 0 && - w.parent.options.MaxHeight != 0 { - w.setMinMaxSize( - w.parent.options.MinWidth, - w.parent.options.MinHeight, - w.parent.options.MaxWidth, - w.parent.options.MaxHeight, - ) - } - w.setZoom(w.parent.options.Zoom) - w.setBackgroundColour(w.parent.options.BackgroundColour) - w.setFrameless(w.parent.options.Frameless) - - switch w.parent.options.StartState { - case WindowStateMaximised: - w.maximise() - case WindowStateMinimised: - w.minimise() - case WindowStateFullscreen: - w.fullscreen() - - } - if w.parent.options.URL != "" { - w.setURL(w.parent.options.URL) - } - // We need to wait for the HTML to load before we can execute the javascript - w.parent.On(events.Mac.WebViewDidFinishNavigation, func(_ *WindowEventContext) { - if w.parent.options.JS != "" { - w.execJS(w.parent.options.JS) - } - if w.parent.options.CSS != "" { - js := fmt.Sprintf("(function() { var style = document.createElement('style'); style.appendChild(document.createTextNode('%s')); document.head.appendChild(style); })();", w.parent.options.CSS) - w.execJS(js) - } - }) - if w.parent.options.HTML != "" { - w.setHTML(w.parent.options.HTML) - } - if w.parent.options.Hidden == false { - w.show() - if w.parent.options.X != 0 || w.parent.options.Y != 0 { - w.setRelativePosition(w.parent.options.X, w.parent.options.Y) - } else { - w.center() - } - } - }) -} - -func (w *linuxWebviewWindow) setTransparent() { - var getScreen func(uintptr) uintptr - purego.RegisterLibFunc(&getScreen, gtk, "gtk_widget_get_screen") - var getVisual func(uintptr) uintptr - purego.RegisterLibFunc(&getVisual, gtk, "gdk_screen_get_rgba_visual") - var isComposited func(uintptr) int - purego.RegisterLibFunc(&isComposited, gtk, "gdk_screen_is_composited") - var setPaintable func(uintptr, int) - purego.RegisterLibFunc(&setPaintable, gtk, "gtk_widget_set_app_paintable") - var setVisual func(uintptr, uintptr) - purego.RegisterLibFunc(&setVisual, gtk, "gtk_widget_set_visual") - - screen := getScreen(w.window) - visual := getVisual(screen) - if visual == 0 { - return - } - if isComposited(screen) == 1 { - setPaintable(w.window, 1) - setVisual(w.window, visual) - } -} - -func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { - if colour.Alpha != 0 { - w.setTransparent() - } - - var rgbaParse func(uintptr, string) bool - purego.RegisterLibFunc(&rgbaParse, gtk, "gdk_rgba_parse") - var setBackgroundColor func(uintptr, uintptr) - purego.RegisterLibFunc(&setBackgroundColor, webkit, "webkit_web_view_set_background_color") - - rgba := make([]byte, 4*8) // C.sizeof_GdkRGBA == 32 - pointer := uintptr(unsafe.Pointer(&rgba[0])) - if !rgbaParse( - pointer, - fmt.Sprintf("rgba(%v,%v,%v,%v)", - colour.Red, - colour.Green, - colour.Blue, - float32(colour.Alpha)/255.0, - )) { - return - } - setBackgroundColor(w.webview, pointer) -} - -func (w *linuxWebviewWindow) relativePosition() (int, int) { - var getPosition func(uintptr, *int, *int) bool - purego.RegisterLibFunc(&getPosition, gtk, "gtk_window_get_position") - - var x, y int - var wg sync.WaitGroup - wg.Add(1) - go globalApplication.dispatchOnMainThread(func() { - getPosition(w.window, &x, &y) - - // Get the position of the window relative to the screen - var getOrigin func(uintptr, *int, *int) - purego.RegisterLibFunc(&getOrigin, gtk, "gtk_widget_translate_coordinates") - getOrigin(w.window, &x, &y) - - wg.Done() - }) - wg.Wait() - return x, y -} - -func (w *linuxWebviewWindow) absolutePosition() (int, int) { - var getOrigin func(uintptr, *int, *int) - purego.RegisterLibFunc(&getOrigin, gtk, "gtk_widget_translate_coordinates") - - var x, y int - var wg sync.WaitGroup - wg.Add(1) - go globalApplication.dispatchOnMainThread(func() { - getOrigin(w.window, nil, nil) - wg.Done() - }) - wg.Wait() - return x, y -} - -func (w *linuxWebviewWindow) destroy() { - var close func(uintptr) - purego.RegisterLibFunc(&close, gtk, "gtk_window_close") - go globalApplication.dispatchOnMainThread(func() { - close(w.window) - }) -} - -func (w *linuxWebviewWindow) setHTML(html string) { - var loadHTML func(uintptr, string, string, *string) - purego.RegisterLibFunc(&loadHTML, webkit, "webkit_web_view_load_alternate_html") - go globalApplication.dispatchOnMainThread(func() { - loadHTML(w.webview, html, "wails://", nil) - }) -} - -func (w *linuxWebviewWindow) isNormal() bool { - return !w.isMinimised() && !w.isMaximised() && !w.isFullscreen() -} - -func (w *linuxWebviewWindow) isVisible() bool { - var isVisible func(uintptr) bool - purego.RegisterLibFunc(&isVisible, gtk, "gtk_widget_is_visible") - return isVisible(w.window) -} - -func (w *linuxWebviewWindow) nativeWindowHandle() uintptr { - return w.window -} From bef454f950738a12a969cd48be85ceaa332188e1 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 15 Jun 2023 13:53:22 -0500 Subject: [PATCH 02/17] [v3 linux] initial linux implementation --- v3/pkg/application/application_linux.go | 167 ++++ v3/pkg/application/clipboard_linux.go | 33 + v3/pkg/application/dialogs_linux.go | 139 +++ v3/pkg/application/linux_cgo.go | 955 +++++++++++++++++++++ v3/pkg/application/mainthread_linux.go | 18 + v3/pkg/application/menu_linux.go | 114 +++ v3/pkg/application/menuitem_linux.go | 364 ++++++++ v3/pkg/application/options_linux.go | 6 + v3/pkg/application/screen_linux.go | 29 + v3/pkg/application/systemtray_linux.go | 90 ++ v3/pkg/application/webview_window_linux.go | 492 +++++++++++ 11 files changed, 2407 insertions(+) create mode 100644 v3/pkg/application/application_linux.go create mode 100644 v3/pkg/application/clipboard_linux.go create mode 100644 v3/pkg/application/dialogs_linux.go create mode 100644 v3/pkg/application/linux_cgo.go create mode 100644 v3/pkg/application/mainthread_linux.go create mode 100644 v3/pkg/application/menu_linux.go create mode 100644 v3/pkg/application/menuitem_linux.go create mode 100644 v3/pkg/application/options_linux.go create mode 100644 v3/pkg/application/screen_linux.go create mode 100644 v3/pkg/application/systemtray_linux.go create mode 100644 v3/pkg/application/webview_window_linux.go diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go new file mode 100644 index 000000000..278ccdabd --- /dev/null +++ b/v3/pkg/application/application_linux.go @@ -0,0 +1,167 @@ +//go:build linux + +package application + +import ( + "fmt" + "log" + "os" + "strings" + "sync" + + "github.com/wailsapp/wails/v3/pkg/events" +) + +func init() { + // FIXME: This should be handled appropriately in the individual files most likely. + // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings + _ = os.Setenv("GDK_BACKEND", "x11") +} + +type linuxApp struct { + application pointer + applicationMenu pointer + parent *App + + startupActions []func() + + // Native -> uint + windows map[windowPointer]uint + windowsLock sync.Mutex +} + +func (m *linuxApp) GetFlags(options Options) map[string]any { + if options.Flags == nil { + options.Flags = make(map[string]any) + } + return options.Flags +} + +func getNativeApplication() *linuxApp { + return globalApplication.impl.(*linuxApp) +} + +func (m *linuxApp) hide() { + hideAllWindows(m.application) +} + +func (m *linuxApp) show() { + showAllWindows(m.application) +} + +func (m *linuxApp) on(eventID uint) { + // TODO: What do we need to do here? + log.Println("linuxApp.on()", eventID) +} + +func (m *linuxApp) setIcon(icon []byte) { + fmt.Println("linuxApp.setIcon", "not implemented") +} + +func (m *linuxApp) name() string { + return appName() +} + +func (m *linuxApp) getCurrentWindowID() uint { + return getCurrentWindowID(m.application, m.windows) +} + +func (m *linuxApp) setApplicationMenu(menu *Menu) { + if menu == nil { + // Create a default menu + menu = defaultApplicationMenu() + } + globalApplication.dispatchOnMainThread(func() { + fmt.Println("setApplicationMenu") + + menu.Update() + m.applicationMenu = (menu.impl).(*linuxMenu).native + }) +} + +func (m *linuxApp) run() error { + + // Add a hook to the ApplicationDidFinishLaunching event + // FIXME: add Wails specific events - i.e. Shouldn't platform specific ones be translated to Wails events? + m.parent.On(events.Mac.ApplicationDidFinishLaunching, func() { + // Do we need to do anything now? + fmt.Println("events.Mac.ApplicationDidFinishLaunching received!") + }) + + return appRun(m.application) +} + +func (m *linuxApp) destroy() { + appDestroy(m.application) +} + +// register our window to our parent mapping +func (m *linuxApp) registerWindow(window pointer, id uint) { + m.windowsLock.Lock() + m.windows[windowPointer(window)] = id + m.windowsLock.Unlock() +} + +func newPlatformApp(parent *App) *linuxApp { + name := strings.ToLower(strings.Replace(parent.options.Name, " ", "", -1)) + if name == "" { + name = "undefined" + } + app := &linuxApp{ + parent: parent, + application: appNew(name), + windows: map[windowPointer]uint{}, + } + return app +} + +/* +//export processApplicationEvent +func processApplicationEvent(eventID C.uint) { + // TODO: add translation to Wails events + // currently reusing Mac specific values + applicationEvents <- uint(eventID) +} + +//export processWindowEvent +func processWindowEvent(windowID C.uint, eventID C.uint) { + windowEvents <- &WindowEvent{ + WindowID: uint(windowID), + EventID: uint(eventID), + } +} + +//export processMessage +func processMessage(windowID C.uint, message *C.char) { + windowMessageBuffer <- &windowMessage{ + windowId: uint(windowID), + message: C.GoString(message), + } +} + +//export processDragItems +func processDragItems(windowID C.uint, arr **C.char, length C.int) { + var filenames []string + // Convert the C array to a Go slice + goSlice := (*[1 << 30]*C.char)(unsafe.Pointer(arr))[:length:length] + for _, str := range goSlice { + filenames = append(filenames, C.GoString(str)) + } + windowDragAndDropBuffer <- &dragAndDropMessage{ + windowId: uint(windowID), + filenames: filenames, + } +} + +//export processMenuItemClick +func processMenuItemClick(menuID identifier) { + menuItemClicked <- uint(menuID) +} + +func setIcon(icon []byte) { + if icon == nil { + return + } + //C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon))) +} +*/ diff --git a/v3/pkg/application/clipboard_linux.go b/v3/pkg/application/clipboard_linux.go new file mode 100644 index 000000000..ef7b25607 --- /dev/null +++ b/v3/pkg/application/clipboard_linux.go @@ -0,0 +1,33 @@ +//go:build linux + +package application + +import ( + "sync" +) + +var clipboardLock sync.RWMutex + +type linuxClipboard struct{} + +func (m linuxClipboard) setText(text string) bool { + clipboardLock.Lock() + defer clipboardLock.Unlock() + // cText := C.CString(text) + // success := C.setClipboardText(cText) + // C.free(unsafe.Pointer(cText)) + success := false + return bool(success) +} + +func (m linuxClipboard) text() (string, bool) { + clipboardLock.RLock() + defer clipboardLock.RUnlock() + // clipboardText := C.getClipboardText() + // result := C.GoString(clipboardText) + return "", false +} + +func newClipboardImpl() *linuxClipboard { + return &linuxClipboard{} +} diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go new file mode 100644 index 000000000..d715ef909 --- /dev/null +++ b/v3/pkg/application/dialogs_linux.go @@ -0,0 +1,139 @@ +package application + +import "fmt" + +func (m *linuxApp) showAboutDialog(title string, message string, icon []byte) { + window := globalApplication.getWindowForID(m.getCurrentWindowID()) + var parent pointer + if window != nil { + parent = window.impl.(*linuxWebviewWindow).window + } + about := newMessageDialog(InfoDialog) + about.SetTitle(title). + SetMessage(message). + SetIcon(icon) + runQuestionDialog( + parent, + about, + ) +} + +type linuxDialog struct { + dialog *MessageDialog +} + +func (m *linuxDialog) show() { + windowId := getNativeApplication().getCurrentWindowID() + window := globalApplication.getWindowForID(windowId) + var parent pointer + if window != nil { + parent = window.impl.(*linuxWebviewWindow).window + } + + response := runQuestionDialog(parent, m.dialog) + if response >= 0 { + fmt.Println("Response: ", response) + button := m.dialog.Buttons[response] + if button.Callback != nil { + go button.Callback() + } + } +} + +func newDialogImpl(d *MessageDialog) *linuxDialog { + return &linuxDialog{ + dialog: d, + } +} + +type linuxOpenFileDialog struct { + dialog *OpenFileDialog +} + +func newOpenFileDialogImpl(d *OpenFileDialog) *linuxOpenFileDialog { + return &linuxOpenFileDialog{ + dialog: d, + } +} + +func (m *linuxOpenFileDialog) show() ([]string, error) { + openFileResponses[m.dialog.id] = make(chan string) + // nsWindow := unsafe.Pointer(nil) + if m.dialog.window != nil { + // get NSWindow from window + //nsWindow = m.dialog.window.impl.(*macosWebviewWindow).nsWindow + } + + // Massage filter patterns into macOS format + // We iterate all filter patterns, tidy them up and then join them with a semicolon + // This should produce a single string of extensions like "png;jpg;gif" + // var filterPatterns string + // if len(m.dialog.filters) > 0 { + // var allPatterns []string + // for _, filter := range m.dialog.filters { + // patternComponents := strings.Split(filter.Pattern, ";") + // for i, component := range patternComponents { + // filterPattern := strings.TrimSpace(component) + // filterPattern = strings.TrimPrefix(filterPattern, "*.") + // patternComponents[i] = filterPattern + // } + // allPatterns = append(allPatterns, strings.Join(patternComponents, ";")) + // } + // filterPatterns = strings.Join(allPatterns, ";") + // } + + // C.showOpenFileDialog(C.uint(m.dialog.id), + // C.bool(m.dialog.canChooseFiles), + // C.bool(m.dialog.canChooseDirectories), + // C.bool(m.dialog.canCreateDirectories), + // C.bool(m.dialog.showHiddenFiles), + // C.bool(m.dialog.allowsMultipleSelection), + // C.bool(m.dialog.resolvesAliases), + // C.bool(m.dialog.hideExtension), + // C.bool(m.dialog.treatsFilePackagesAsDirectories), + // C.bool(m.dialog.allowsOtherFileTypes), + // toCString(filterPatterns), + // C.uint(len(filterPatterns)), + // toCString(m.dialog.message), + // toCString(m.dialog.directory), + // toCString(m.dialog.buttonText), + // nsWindow) + var result []string + for filename := range openFileResponses[m.dialog.id] { + result = append(result, filename) + } + return result, nil +} + +type linuxSaveFileDialog struct { + dialog *SaveFileDialog +} + +func newSaveFileDialogImpl(d *SaveFileDialog) *linuxSaveFileDialog { + return &linuxSaveFileDialog{ + dialog: d, + } +} + +func (m *linuxSaveFileDialog) show() (string, error) { + saveFileResponses[m.dialog.id] = make(chan string) + // nsWindow := unsafe.Pointer(nil) + if m.dialog.window != nil { + // get NSWindow from window + // nsWindow = m.dialog.window.impl.(*linuxWebviewWindow).nsWindow + } + + // C.showSaveFileDialog(C.uint(m.dialog.id), + // C.bool(m.dialog.canCreateDirectories), + // C.bool(m.dialog.showHiddenFiles), + // C.bool(m.dialog.canSelectHiddenExtension), + // C.bool(m.dialog.hideExtension), + // C.bool(m.dialog.treatsFilePackagesAsDirectories), + // C.bool(m.dialog.allowOtherFileTypes), + // toCString(m.dialog.message), + // toCString(m.dialog.directory), + // toCString(m.dialog.buttonText), + // toCString(m.dialog.filename), + // nsWindow) + return <-saveFileResponses[m.dialog.id], nil +} diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go new file mode 100644 index 000000000..cf7606f53 --- /dev/null +++ b/v3/pkg/application/linux_cgo.go @@ -0,0 +1,955 @@ +//go:build linux && cgo + +package application + +import ( + "fmt" + "strings" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include +#include +#include +#include +#include +#include + +typedef struct CallbackID +{ + unsigned int value; +} CallbackID; + +extern void dispatchOnMainThreadCallback(unsigned int); + +static gboolean dispatchCallback(gpointer data) { + struct CallbackID *args = data; + unsigned int cid = args->value; + dispatchOnMainThreadCallback(cid); + free(args); + + return G_SOURCE_REMOVE; +}; + +static void dispatchOnMainThread(unsigned int id) { + CallbackID *args = malloc(sizeof(CallbackID)); + args->value = id; + g_idle_add((GSourceFunc)dispatchCallback, (gpointer)args); +} + +// exported below +void activateLinux(gpointer data); +void handleClick(void*); +extern gboolean onButtonEvent(GtkWidget *widget, GdkEventButton *event, gpointer user_data); +extern void onDragNDrop( + void *target, + GdkDragContext* context, + gint x, + gint y, + gpointer seldata, + guint info, + guint time, + gpointer data); +extern void onProcessRequest(void *request, gpointer user_data); +// exported below (end) + +static void signal_connect(GtkWidget *widget, char *event, void *cb, void* data) { + // g_signal_connect is a macro and can't be called directly + g_signal_connect(widget, event, cb, data); +} + +static void* new_message_dialog(GtkWindow *parent, const gchar *msg, int dialogType, bool hasButtons) { + // gtk_message_dialog_new is variadic! Can't call from cgo directly + GtkWidget *dialog; + int buttonMask; + + // buttons will be added after creation + buttonMask = GTK_BUTTONS_OK; + if (hasButtons) { + buttonMask = GTK_BUTTONS_NONE; + } + + dialog = gtk_message_dialog_new( + parent, + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + dialogType, + buttonMask, + msg); + + // g_signal_connect_swapped (dialog, + // "response", + // G_CALLBACK (callback), + // dialog); + return dialog; +}; + +extern void messageDialogCB(gint button); + +typedef struct Screen { + const char* id; + const char* name; + int p_width; + int p_height; + int width; + int height; + int x; + int y; + int w_width; + int w_height; + int w_x; + int w_y; + float scale; + double rotation; + bool isPrimary; +} Screen; + + +static int GetNumScreens(){ + return 0; +} + +*/ +import "C" + +type windowPointer *C.GtkWindow +type identifier C.uint +type pointer unsafe.Pointer +type GSList C.GSList +type GSListPointer *GSList + +var ( + nilRadioGroup GSListPointer = nil + gtkSignalHandlers map[*C.GtkWidget]C.gulong + gtkSignalToMenuItem map[*C.GtkWidget]*MenuItem +) + +func init() { + fmt.Println("linux_cgo") + + gtkSignalHandlers = map[*C.GtkWidget]C.gulong{} + gtkSignalToMenuItem = map[*C.GtkWidget]*MenuItem{} +} + +// mainthread stuff +func dispatchOnMainThread(id uint) { + C.dispatchOnMainThread(C.uint(id)) +} + +//export dispatchOnMainThreadCallback +func dispatchOnMainThreadCallback(callbackID C.uint) { + executeOnMainThread(uint(callbackID)) +} + +//export activateLinux +func activateLinux(data pointer) { + // NOOP: Callback for now +} + +// implementation below +func appName() string { + name := C.g_get_application_name() + defer C.free(unsafe.Pointer(name)) + return C.GoString(name) +} + +func appNew(name string) pointer { + nameC := C.CString(fmt.Sprintf("org.wails.%s", name)) + defer C.free(unsafe.Pointer(nameC)) + return pointer(C.gtk_application_new(nameC, C.G_APPLICATION_DEFAULT_FLAGS)) +} + +func appRun(app pointer) error { + application := (*C.GApplication)(app) + C.g_application_hold(application) // allows it to run without a window + signal := C.CString("activate") + C.g_signal_connect_data(C.gpointer(application), signal, C.GCallback(C.activateLinux), nil, nil, 0) + status := C.g_application_run(application, 0, nil) + C.g_application_release(application) + C.g_object_unref(C.gpointer(app)) + + var err error + if status != 0 { + err = fmt.Errorf("exit code: %d", status) + } + return err +} + +func appDestroy(application pointer) { + C.g_application_quit((*C.GApplication)(application)) +} + +func getCurrentWindowID(application pointer, windows map[windowPointer]uint) uint { + // TODO: Add extra metadata to window and use it! + window := (*C.GtkWindow)(C.gtk_application_get_active_window((*C.GtkApplication)(application))) + if window == nil { + return uint(1) + } + identifier, ok := windows[window] + if ok { + return identifier + } + // FIXME: Should we panic here if not found? + return uint(1) +} + +func getWindows(application pointer) []pointer { + result := []pointer{} + windows := C.gtk_application_get_windows((*C.GtkApplication)(application)) + for { + result = append(result, pointer(windows.data)) + windows = windows.next + if windows == nil { + return result + } + } +} + +func hideAllWindows(application pointer) { + for _, window := range getWindows(application) { + C.gtk_widget_hide((*C.GtkWidget)(window)) + } +} + +func showAllWindows(application pointer) { + for _, window := range getWindows(application) { + C.gtk_widget_show_all((*C.GtkWidget)(window)) + } +} + +// Menu +func menuAddSeparator(menu *Menu) { + C.gtk_menu_shell_append( + (*C.GtkMenuShell)((menu.impl).(*linuxMenu).native), + C.gtk_separator_menu_item_new()) +} + +func menuAppend(parent *Menu, menu *MenuItem) { + C.gtk_menu_shell_append( + (*C.GtkMenuShell)((parent.impl).(*linuxMenu).native), + (*C.GtkWidget)((menu.impl).(*linuxMenuItem).native), + ) + /* gtk4 + C.gtk_menu_item_set_submenu( + (*C.struct__GtkMenuItem)((menu.impl).(*linuxMenuItem).native), + (*C.struct__GtkWidget)((parent.impl).(*linuxMenu).native), + ) + */ +} + +func menuBarNew() pointer { + return pointer(C.gtk_menu_bar_new()) +} + +func menuNew() pointer { + return pointer(C.gtk_menu_new()) +} + +func menuSetSubmenu(item *MenuItem, menu *Menu) { + C.gtk_menu_item_set_submenu( + (*C.GtkMenuItem)((item.impl).(*linuxMenuItem).native), + (*C.GtkWidget)((menu.impl).(*linuxMenu).native)) +} + +func menuGetRadioGroup(item *linuxMenuItem) *GSList { + return (*GSList)(C.gtk_radio_menu_item_get_group((*C.GtkRadioMenuItem)(item.native))) +} + +//export handleClick +func handleClick(idPtr unsafe.Pointer) { + id := (*C.GtkWidget)(idPtr) + item, ok := gtkSignalToMenuItem[id] + if !ok { + return + } + + switch item.itemType { + case text, checkbox: + menuItemClicked <- item.id + case radio: + menuItem := (item.impl).(*linuxMenuItem) + if menuItem.isChecked() { + menuItemClicked <- item.id + } + } +} + +func attachMenuHandler(item *MenuItem) { + signal := C.CString("activate") + defer C.free(unsafe.Pointer(signal)) + + impl := (item.impl).(*linuxMenuItem) + widget := impl.native + flags := C.GConnectFlags(0) + handlerId := C.g_signal_connect_object( + C.gpointer(widget), + signal, + C.GCallback(C.handleClick), + C.gpointer(widget), + flags) + + id := (*C.GtkWidget)(widget) + gtkSignalToMenuItem[id] = item + gtkSignalHandlers[id] = handlerId + impl.handlerId = uint(handlerId) +} + +// menuItem +func menuItemChecked(widget pointer) bool { + if C.gtk_check_menu_item_get_active((*C.GtkCheckMenuItem)(widget)) == C.int(1) { + return true + } + return false +} + +func menuItemNew(label string) pointer { + cLabel := C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + return pointer(C.gtk_menu_item_new_with_label(cLabel)) +} + +func menuCheckItemNew(label string) pointer { + cLabel := C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + return pointer(C.gtk_check_menu_item_new_with_label(cLabel)) +} + +func menuItemSetChecked(widget pointer, checked bool) { + value := C.int(0) + if checked { + value = C.int(1) + } + C.gtk_check_menu_item_set_active( + (*C.GtkCheckMenuItem)(widget), + value) +} + +func menuItemSetDisabled(widget pointer, disabled bool) { + value := C.int(1) + if disabled { + value = C.int(0) + } + C.gtk_widget_set_sensitive( + (*C.GtkWidget)(widget), + value) +} + +func menuItemSetLabel(widget pointer, label string) { + value := C.CString(label) + C.gtk_menu_item_set_label( + (*C.GtkMenuItem)(widget), + value) + C.free(unsafe.Pointer(value)) +} + +func menuItemSetToolTip(widget pointer, tooltip string) { + value := C.CString(tooltip) + C.gtk_widget_set_tooltip_text( + (*C.GtkWidget)(widget), + value) + C.free(unsafe.Pointer(value)) +} + +func menuItemSignalBlock(widget pointer, handlerId uint, block bool) { + if block { + C.g_signal_handler_block(C.gpointer(widget), C.ulong(handlerId)) + } else { + C.g_signal_handler_unblock(C.gpointer(widget), C.ulong(handlerId)) + } +} + +func menuRadioItemNew(group *GSList, label string) pointer { + cLabel := C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + return pointer(C.gtk_radio_menu_item_new_with_label((*C.GSList)(group), cLabel)) +} + +// screen related + +func getScreenByIndex(display *C.struct__GdkDisplay, index int) *Screen { + monitor := C.gdk_display_get_monitor(display, C.int(index)) + // TODO: Do we need to update Screen to contain current info? + // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) + + var geometry C.GdkRectangle + C.gdk_monitor_get_geometry(monitor, &geometry) + primary := false + if C.gdk_monitor_is_primary(monitor) == 1 { + primary = true + } + + return &Screen{ + IsPrimary: primary, + Scale: 1.0, + X: int(geometry.x), + Y: int(geometry.y), + Size: Size{ + Height: int(geometry.height), + Width: int(geometry.width), + }, + } +} + +func getScreens(app pointer) ([]*Screen, error) { + var screens []*Screen + window := C.gtk_application_get_active_window((*C.GtkApplication)(app)) + display := C.gdk_window_get_display((*C.GdkWindow)(unsafe.Pointer(window))) + count := C.gdk_display_get_n_monitors(display) + for i := 0; i < int(count); i++ { + screens = append(screens, getScreenByIndex(display, i)) + } + return screens, nil +} + +// widgets +func widgetSetVisible(widget pointer, hidden bool) { + if hidden { + C.gtk_widget_hide((*C.GtkWidget)(widget)) + } else { + C.gtk_widget_show((*C.GtkWidget)(widget)) + } +} + +// window related functions +func windowClose(window pointer) { + C.gtk_window_close((*C.GtkWindow)(window)) +} + +func windowEnableDND(id uint, webview pointer) { + dnd := C.CString("text/uri-list") + defer C.free(unsafe.Pointer(dnd)) + targetentry := C.gtk_target_entry_new(dnd, 0, C.guint(id)) + defer C.gtk_target_entry_free(targetentry) + C.gtk_drag_dest_set((*C.GtkWidget)(webview), C.GTK_DEST_DEFAULT_DROP, targetentry, 1, C.GDK_ACTION_COPY) + event := C.CString("drag-data-received") + defer C.free(unsafe.Pointer(event)) + windowId := C.uint(id) + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, C.onDragNDrop, unsafe.Pointer(C.gpointer(&windowId))) +} + +func windowExecJS(webview pointer, js string) { + value := C.CString(js) + C.webkit_web_view_evaluate_javascript((*C.WebKitWebView)(webview), + value, + C.long(len(js)), + nil, + C.CString(""), + nil, + nil, + nil) + C.free(unsafe.Pointer(value)) +} + +func windowDestroy(window pointer) { + // Should this truly 'destroy' ? + C.gtk_window_close((*C.GtkWindow)(window)) + //C.gtk_widget_destroy((*C.GtkWidget)(window)) +} + +func windowFullscreen(window pointer) { + C.gtk_window_fullscreen((*C.GtkWindow)(window)) +} + +func windowGetCurrentMonitor(window pointer) *C.GdkMonitor { + // Get the monitor that the window is currently on + display := C.gtk_widget_get_display((*C.GtkWidget)(window)) + gdk_window := C.gtk_widget_get_window((*C.GtkWidget)(window)) + if gdk_window == nil { + return nil + } + return C.gdk_display_get_monitor_at_window(display, gdk_window) +} + +func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, height int, scale int) { + monitor := windowGetCurrentMonitor(window) + if monitor == nil { + return -1, -1, -1, -1, 1 + } + var result C.GdkRectangle + C.gdk_monitor_get_geometry(monitor, &result) + scale = int(C.gdk_monitor_get_scale_factor(monitor)) + return int(result.x), int(result.y), int(result.width), int(result.height), scale +} + +func windowGetSize(window pointer) (int, int) { + var windowWidth C.int + var windowHeight C.int + C.gtk_window_get_size((*C.GtkWindow)(window), &windowWidth, &windowHeight) + return int(windowWidth), int(windowHeight) +} + +func windowGetPosition(window pointer) (int, int) { + var x C.int + var y C.int + C.gtk_window_get_position((*C.GtkWindow)(window), &x, &y) + return int(x), int(y) +} + +func windowHide(window pointer) { + C.gtk_widget_hide((*C.GtkWidget)(window)) +} + +func windowIsFullscreen(window pointer) bool { + gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window)) + state := C.gdk_window_get_state(gdkwindow) + return state&C.GDK_WINDOW_STATE_FULLSCREEN > 0 +} + +func windowIsMaximized(window pointer) bool { + gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window)) + state := C.gdk_window_get_state(gdkwindow) + return state&C.GDK_WINDOW_STATE_MAXIMIZED > 0 && state&C.GDK_WINDOW_STATE_FULLSCREEN == 0 +} + +func windowIsMinimized(window pointer) bool { + gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window)) + state := C.gdk_window_get_state(gdkwindow) + return state&C.GDK_WINDOW_STATE_ICONIFIED > 0 +} + +func windowIsVisible(window pointer) bool { + if C.gtk_widget_is_visible((*C.GtkWidget)(window)) == 1 { + return true + } + return false +} + +func windowMaximize(window pointer) { + C.gtk_window_maximize((*C.GtkWindow)(window)) +} + +func windowMinimize(window pointer) { + C.gtk_window_iconify((*C.GtkWindow)(window)) +} + +func windowNew(application pointer, menu pointer, windowId uint, gpuPolicy int) (window pointer, webview pointer) { + window = pointer(C.gtk_application_window_new((*C.GtkApplication)(application))) + C.g_object_ref_sink(C.gpointer(window)) + webview = windowNewWebview(windowId, gpuPolicy) + vbox := pointer(C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)) + C.gtk_container_add((*C.GtkContainer)(window), (*C.GtkWidget)(vbox)) + + if menu != nil { + C.gtk_box_pack_start((*C.GtkBox)(vbox), (*C.GtkWidget)(menu), 0, 0, 0) + } + C.gtk_box_pack_start((*C.GtkBox)(unsafe.Pointer(vbox)), (*C.GtkWidget)(webview), 1, 1, 0) + return +} + +func windowNewWebview(parentId uint, gpuPolicy int) pointer { + manager := C.webkit_user_content_manager_new() + external := C.CString("external") + C.webkit_user_content_manager_register_script_message_handler(manager, external) + C.free(unsafe.Pointer(external)) + webview := C.webkit_web_view_new_with_user_content_manager(manager) + id := C.uint(parentId) + if !registered { + wails := C.CString("wails") + C.webkit_web_context_register_uri_scheme( + C.webkit_web_context_get_default(), + wails, + C.WebKitURISchemeRequestCallback(C.onProcessRequest), + C.gpointer(&id), + nil) + registered = true + C.free(unsafe.Pointer(wails)) + } + settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(unsafe.Pointer(webview))) + wails_io := C.CString("wails.io") + empty := C.CString("") + defer C.free(unsafe.Pointer(wails_io)) + defer C.free(unsafe.Pointer(empty)) + C.webkit_settings_set_user_agent_with_application_details(settings, wails_io, empty) + + switch gpuPolicy { + case 0: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS) + break + case 1: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND) + break + case 2: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER) + break + default: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND) + } + return pointer(webview) +} + +func windowPresent(window pointer) { + C.gtk_window_present((*C.GtkWindow)(window)) + // gtk_window_unminimize ((*C.GtkWindow)(w.window)) /// gtk4 +} + +func windowReload(webview pointer, address string) { + uri := C.CString(address) + C.webkit_web_view_load_uri((*C.WebKitWebView)(webview), uri) + C.free(unsafe.Pointer(uri)) +} + +func windowResize(window pointer, width, height int) { + C.gtk_window_resize( + (*C.GtkWindow)(window), + C.gint(width), + C.gint(height)) +} + +func windowShow(window pointer) { + C.gtk_widget_show_all((*C.GtkWidget)(window)) +} + +func windowSetBackgroundColour(webview pointer, colour RGBA) { + rgba := C.GdkRGBA{C.double(colour.Red) / 255.0, C.double(colour.Green) / 255.0, C.double(colour.Blue) / 255.0, C.double(colour.Alpha) / 255.0} + C.webkit_web_view_set_background_color((*C.WebKitWebView)(webview), &rgba) +} + +func windowSetGeometryHints(window pointer, minWidth, minHeight, maxWidth, maxHeight int) { + size := C.GdkGeometry{ + min_width: C.int(minWidth), + min_height: C.int(minHeight), + max_width: C.int(maxWidth), + max_height: C.int(maxHeight), + } + C.gtk_window_set_geometry_hints((*C.GtkWindow)(window), nil, &size, C.GDK_HINT_MAX_SIZE|C.GDK_HINT_MIN_SIZE) +} + +func windowSetFrameless(window pointer, frameless bool) { + C.gtk_window_set_decorated((*C.GtkWindow)(window), gtkBool(!frameless)) + // TODO: Deal with transparency for the titlebar if possible when !frameless + // Perhaps we just make it undecorated and add a menu bar inside? +} + +// TODO: confirm this is working properly +func windowSetHTML(webview pointer, html string) { + cHTML := C.CString(html) + uri := C.CString("wails://") + empty := C.CString("") + defer C.free(unsafe.Pointer(cHTML)) + defer C.free(unsafe.Pointer(uri)) + defer C.free(unsafe.Pointer(empty)) + C.webkit_web_view_load_alternate_html( + (*C.WebKitWebView)(webview), + cHTML, + uri, + empty) +} + +func windowSetKeepAbove(window pointer, alwaysOnTop bool) { + C.gtk_window_set_keep_above((*C.GtkWindow)(window), gtkBool(alwaysOnTop)) +} + +func windowSetResizable(window pointer, resizable bool) { + C.gtk_window_set_resizable((*C.GtkWindow)(window), gtkBool(resizable)) +} + +func windowSetTitle(window pointer, title string) { + cTitle := C.CString(title) + C.gtk_window_set_title((*C.GtkWindow)(window), cTitle) + C.free(unsafe.Pointer(cTitle)) +} + +func windowSetTransparent(window pointer) { + screen := C.gtk_widget_get_screen((*C.GtkWidget)(window)) + visual := C.gdk_screen_get_rgba_visual(screen) + + if visual != nil && C.gdk_screen_is_composited(screen) == C.int(1) { + C.gtk_widget_set_app_paintable((*C.GtkWidget)(window), C.gboolean(1)) + C.gtk_widget_set_visual((*C.GtkWidget)(window), visual) + } +} + +func windowSetURL(webview pointer, uri string) { + target := C.CString(uri) + C.webkit_web_view_load_uri((*C.WebKitWebView)(webview), target) + C.free(unsafe.Pointer(target)) +} + +func windowSetupSignalHandlers(windowId uint, window, webview pointer, hideOnClose bool) { + event := C.CString("delete-event") + defer C.free(unsafe.Pointer(event)) + + // Window close handler + if hideOnClose { + C.signal_connect((*C.GtkWidget)(window), event, C.gtk_widget_hide_on_delete, C.NULL) + } else { + //FIXME: what event should be emitted? + // C.signal_connect((*C.GtkWidget)(window), event, C.close_button_pressed, w.parent.id) + } + + /* + event = C.CString("load-changed") + defer C.free(unsafe.Pointer(event)) + C.signal_connect(webview, event, C.webviewLoadChanged, unsafe.Pointer(&w.parent.id)) + */ + id := C.uint(windowId) + event = C.CString("button-press-event") + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, C.onButtonEvent, unsafe.Pointer(&id)) + C.free(unsafe.Pointer(event)) + event = C.CString("button-release-event") + defer C.free(unsafe.Pointer(event)) + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, C.onButtonEvent, unsafe.Pointer(&id)) +} + +func windowToggleDevTools(webview pointer) { + settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(webview)) + enabled := C.webkit_settings_get_enable_developer_extras(settings) + switch enabled { + case C.int(0): + enabled = C.int(1) + case C.int(1): + enabled = C.int(0) + } + C.webkit_settings_set_enable_developer_extras(settings, enabled) +} + +func windowUnfullscreen(window pointer) { + C.gtk_window_unfullscreen((*C.GtkWindow)(window)) +} + +func windowUnmaximize(window pointer) { + C.gtk_window_unmaximize((*C.GtkWindow)(window)) +} + +func windowZoom(webview pointer) float64 { + return float64(C.webkit_web_view_get_zoom_level((*C.WebKitWebView)(webview))) +} + +// FIXME: ZoomIn/Out is assumed to be incorrect! +func windowZoomIn(webview pointer) { + ZoomInFactor := 1.10 + windowZoomSet(webview, windowZoom(webview)*ZoomInFactor) +} +func windowZoomOut(webview pointer) { + ZoomOutFactor := -1.10 + windowZoomSet(webview, windowZoom(webview)*ZoomOutFactor) +} + +func windowZoomSet(webview pointer, zoom float64) { + C.webkit_web_view_set_zoom_level((*C.WebKitWebView)(webview), C.double(zoom)) +} + +func windowMove(window pointer, x, y int) { + C.gtk_window_move((*C.GtkWindow)(window), C.int(x), C.int(y)) +} + +//export onButtonEvent +func onButtonEvent(_ *C.GtkWidget, event *C.GdkEventButton, data unsafe.Pointer) C.gboolean { + // Constants (defined here to be easier to use with ) + GdkButtonPress := C.GDK_BUTTON_PRESS // 4 + Gdk2ButtonPress := C.GDK_2BUTTON_PRESS // 5 for double-click + GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7 + + windowId := uint(*((*C.uint)(data))) + window := globalApplication.getWindowForID(windowId) + if window == nil { + return C.gboolean(0) + } + lw, ok := (window.impl).(*linuxWebviewWindow) + if !ok { + return C.gboolean(0) + } + + if event == nil { + return C.gboolean(0) + } + if event.button == 3 { + return C.gboolean(0) + } + + switch int(event._type) { + case GdkButtonPress: + lw.startDrag() //uint(event.button), int(event.x_root), int(event.y_root)) + case Gdk2ButtonPress: + fmt.Printf("%d - button %d - double-clicked\n", windowId, int(event.button)) + case GdkButtonRelease: + lw.endDrag(uint(event.button), int(event.x_root), int(event.y_root)) + } + + return C.gboolean(0) +} + +//export onDragNDrop +func onDragNDrop(target unsafe.Pointer, context *C.GdkDragContext, x C.gint, y C.gint, seldata unsafe.Pointer, info C.guint, time C.guint, data unsafe.Pointer) { + fmt.Println("target", target, info) + var length C.gint + selection := unsafe.Pointer(C.gtk_selection_data_get_data_with_length((*C.GtkSelectionData)(seldata), &length)) + extracted := C.g_uri_list_extract_uris((*C.char)(selection)) + defer C.g_strfreev(extracted) + + uris := unsafe.Slice( + (**C.char)(unsafe.Pointer(extracted)), + int(length)) + + var filenames []string + for _, uri := range uris { + if uri == nil { + break + } + filenames = append(filenames, strings.TrimPrefix(C.GoString(uri), "file://")) + } + windowDragAndDropBuffer <- &dragAndDropMessage{ + windowId: uint(*((*C.uint)(data))), + filenames: filenames, + } + C.gtk_drag_finish(context, C.true, C.false, time) +} + +//export onProcessRequest +func onProcessRequest(request unsafe.Pointer, data unsafe.Pointer) { + windowId := uint(*((*C.uint)(data))) + webviewRequests <- &webViewAssetRequest{ + Request: webview.NewRequest(request), + windowId: windowId, + windowName: globalApplication.getWindowForID(windowId).Name(), + } +} + +func gtkBool(input bool) C.gboolean { + if input { + return C.gboolean(1) + } + return C.gboolean(0) +} + +// dialog related + +func setWindowIcon(window pointer, icon []byte) { + fmt.Println("setWindowIcon", len(icon)) + loader := C.gdk_pixbuf_loader_new() + if loader == nil { + return + } + written := C.gdk_pixbuf_loader_write( + loader, + (*C.uchar)(&icon[0]), + C.ulong(len(icon)), + nil) + if written == 0 { + fmt.Println("failed to write icon") + return + } + C.gdk_pixbuf_loader_close(loader, nil) + pixbuf := C.gdk_pixbuf_loader_get_pixbuf(loader) + if pixbuf != nil { + fmt.Println("gtk_window_set_icon", window) + C.gtk_window_set_icon((*C.GtkWindow)(window), pixbuf) + } + C.g_object_unref(C.gpointer(loader)) +} + +//export messageDialogCB +func messageDialogCB(button C.int) { + fmt.Println("messageDialogCB", button) + +} + +func runQuestionDialog(parent pointer, options *MessageDialog) int { + cMsg := C.CString(options.Message) + cTitle := C.CString(options.Title) + defer C.free(unsafe.Pointer(cMsg)) + defer C.free(unsafe.Pointer(cTitle)) + hasButtons := false + if len(options.Buttons) > 0 { + hasButtons = true + } + + dType, ok := map[DialogType]C.int{ + InfoDialog: C.GTK_MESSAGE_INFO, + QuestionDialog: C.GTK_MESSAGE_QUESTION, + WarningDialog: C.GTK_MESSAGE_WARNING, + }[options.DialogType] + if !ok { + // FIXME: Add logging here! + dType = C.GTK_MESSAGE_INFO + } + + dialog := C.new_message_dialog((*C.GtkWindow)(parent), cMsg, dType, C.bool(hasButtons)) + if options.Title != "" { + C.gtk_window_set_title( + (*C.GtkWindow)(unsafe.Pointer(dialog)), + cTitle) + } + + if img, err := pngToImage(options.Icon); err == nil { + gbytes := C.g_bytes_new_static( + C.gconstpointer(unsafe.Pointer(&img.Pix[0])), + C.ulong(len(img.Pix))) + defer C.g_bytes_unref(gbytes) + pixBuf := C.gdk_pixbuf_new_from_bytes( + gbytes, + C.GDK_COLORSPACE_RGB, + 1, // has_alpha + 8, + C.int(img.Bounds().Dx()), + C.int(img.Bounds().Dy()), + C.int(img.Stride), + ) + image := C.gtk_image_new_from_pixbuf(pixBuf) + C.gtk_widget_set_visible((*C.GtkWidget)(image), C.gboolean(1)) + contentArea := C.gtk_dialog_get_content_area((*C.GtkDialog)(dialog)) + C.gtk_container_add( + (*C.GtkContainer)(unsafe.Pointer(contentArea)), + (*C.GtkWidget)(image)) + } + for i, button := range options.Buttons { + cLabel := C.CString(button.Label) + defer C.free(unsafe.Pointer(cLabel)) + index := C.int(i) + C.gtk_dialog_add_button( + (*C.GtkDialog)(dialog), cLabel, index) + if button.IsDefault { + C.gtk_dialog_set_default_response((*C.GtkDialog)(dialog), index) + } + } + + defer C.gtk_widget_destroy((*C.GtkWidget)(dialog)) + return int(C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dialog)))) +} + +//export openFileDialogCallbackEnd +func openFileDialogCallbackEnd(cid C.uint) { + id := uint(cid) + channel, ok := openFileResponses[id] + if ok { + close(channel) + delete(openFileResponses, id) + freeDialogID(id) + } else { + panic("No channel found for open file dialog") + } +} + +//export openFileDialogCallback +func openFileDialogCallback(cid C.uint, cpath *C.char) { + path := C.GoString(cpath) + id := uint(cid) + channel, ok := openFileResponses[id] + if ok { + channel <- path + } else { + panic("No channel found for open file dialog") + } +} + +//export saveFileDialogCallback +func saveFileDialogCallback(cid C.uint, cpath *C.char) { + // Covert the path to a string + path := C.GoString(cpath) + id := uint(cid) + // put response on channel + channel, ok := saveFileResponses[id] + if ok { + channel <- path + close(channel) + delete(saveFileResponses, id) + freeDialogID(id) + + } else { + panic("No channel found for save file dialog") + } +} diff --git a/v3/pkg/application/mainthread_linux.go b/v3/pkg/application/mainthread_linux.go new file mode 100644 index 000000000..572b081be --- /dev/null +++ b/v3/pkg/application/mainthread_linux.go @@ -0,0 +1,18 @@ +//go:build linux + +package application + +func (m *linuxApp) dispatchOnMainThread(id uint) { + dispatchOnMainThread(id) +} + +func executeOnMainThread(callbackID uint) { + mainThreadFunctionStoreLock.RLock() + fn := mainThreadFunctionStore[callbackID] + if fn == nil { + Fatal("dispatchCallback called with invalid id: %v", callbackID) + } + delete(mainThreadFunctionStore, callbackID) + mainThreadFunctionStoreLock.RUnlock() + fn() +} diff --git a/v3/pkg/application/menu_linux.go b/v3/pkg/application/menu_linux.go new file mode 100644 index 000000000..2bd8684e9 --- /dev/null +++ b/v3/pkg/application/menu_linux.go @@ -0,0 +1,114 @@ +//go:build linux + +package application + +type linuxMenu struct { + menu *Menu + native pointer +} + +func newMenuImpl(menu *Menu) *linuxMenu { + result := &linuxMenu{ + menu: menu, + native: menuBarNew(), + } + return result +} + +func (m *linuxMenu) update() { + m.processMenu(m.menu) +} + +func (m *linuxMenu) processMenu(menu *Menu) { + if menu.impl == nil { + menu.impl = &linuxMenu{ + menu: menu, + native: menuNew(), + } + } + var currentRadioGroup GSListPointer + + for _, item := range menu.items { + // drop the group if we have run out of radio items + if item.itemType != radio { + currentRadioGroup = nilRadioGroup + } + + switch item.itemType { + case submenu: + menuItem := newMenuItemImpl(item) + item.impl = menuItem + m.processMenu(item.submenu) + m.addSubMenuToItem(item.submenu, item) + m.addMenuItem(menu, item) + case text, checkbox: + menuItem := newMenuItemImpl(item) + item.impl = menuItem + m.addMenuItem(menu, item) + case radio: + menuItem := newRadioItemImpl(item, currentRadioGroup) + item.impl = menuItem + m.addMenuItem(menu, item) + currentRadioGroup = menuGetRadioGroup(menuItem) + case separator: + m.addMenuSeparator(menu) + } + + } + + for _, item := range menu.items { + if item.callback != nil { + m.attachHandler(item) + } + } + +} + +func (m *linuxMenu) attachHandler(item *MenuItem) { + attachMenuHandler(item) +} + +func (m *linuxMenu) addSubMenuToItem(menu *Menu, item *MenuItem) { + if menu.impl == nil { + menu.impl = &linuxMenu{ + menu: menu, + native: menuNew(), + } + } + menuSetSubmenu(item, menu) +} + +func (m *linuxMenu) addMenuItem(parent *Menu, menu *MenuItem) { + menuAppend(parent, menu) +} + +func (m *linuxMenu) addMenuSeparator(menu *Menu) { + menuAddSeparator(menu) + +} + +func (m *linuxMenu) addServicesMenu(menu *Menu) { + // FIXME: Should this be required? +} + +func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { + impl := newMenuImpl(&Menu{label: name}) + menu := &Menu{ + label: name, + items: items, + impl: impl, + } + impl.menu = menu + return menu +} + +func defaultApplicationMenu() *Menu { + menu := NewMenu() + menu.AddRole(AppMenu) + menu.AddRole(FileMenu) + menu.AddRole(EditMenu) + menu.AddRole(ViewMenu) + menu.AddRole(WindowMenu) + menu.AddRole(HelpMenu) + return menu +} diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go new file mode 100644 index 000000000..8b32825f2 --- /dev/null +++ b/v3/pkg/application/menuitem_linux.go @@ -0,0 +1,364 @@ +//go:build linux + +package application + +import ( + "fmt" + "runtime" +) + +type linuxMenuItem struct { + menuItem *MenuItem + native pointer + handlerId uint +} + +func (l linuxMenuItem) setTooltip(tooltip string) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetToolTip(l.native, tooltip) + }) +} + +func (l linuxMenuItem) blockSignal() { + if l.handlerId != 0 { + menuItemSignalBlock(l.native, l.handlerId, true) + } +} + +func (l linuxMenuItem) unBlockSignal() { + if l.handlerId != 0 { + menuItemSignalBlock(l.native, l.handlerId, false) + } +} + +func (l linuxMenuItem) setLabel(s string) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetLabel(l.native, s) + }) +} + +func (l linuxMenuItem) isChecked() bool { + return menuItemChecked(l.native) +} + +func (l linuxMenuItem) setDisabled(disabled bool) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetDisabled(l.native, disabled) + }) +} + +func (l linuxMenuItem) setChecked(checked bool) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetChecked(l.native, checked) + }) +} + +func (l linuxMenuItem) setHidden(hidden bool) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + widgetSetVisible(l.native, hidden) + }) +} + +func (l linuxMenuItem) setAccelerator(accelerator *accelerator) { + fmt.Println("setAccelerator", accelerator) + // Set the keyboard shortcut of the menu item + // var modifier C.int + // var key *C.char + if accelerator != nil { + // modifier = C.int(toMacModifier(accelerator.Modifiers)) + // key = C.CString(accelerator.Key) + } + + // Convert the key to a string + // C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) +} + +func newMenuItemImpl(item *MenuItem) *linuxMenuItem { + result := &linuxMenuItem{ + menuItem: item, + } + switch item.itemType { + case text: + result.native = menuItemNew(item.label) + + case checkbox: + result.native = menuCheckItemNew(item.label) + result.setChecked(item.checked) + if item.accelerator != nil { + result.setAccelerator(item.accelerator) + } + case submenu: + result.native = menuItemNew(item.label) + + default: + panic(fmt.Sprintf("Unknown menu type: %v", item.itemType)) + } + result.setDisabled(result.menuItem.disabled) + return result +} + +func newRadioItemImpl(item *MenuItem, group GSListPointer) *linuxMenuItem { + result := &linuxMenuItem{ + menuItem: item, + native: menuRadioItemNew(group, item.label), + } + result.setChecked(item.checked) + result.setDisabled(result.menuItem.disabled) + return result +} + +func newSpeechMenu() *MenuItem { + speechMenu := NewMenu() + speechMenu.Add("Start Speaking"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+."). + OnClick(func(ctx *Context) { + // C.startSpeaking() + }) + speechMenu.Add("Stop Speaking"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+,"). + OnClick(func(ctx *Context) { + // C.stopSpeaking() + }) + subMenu := newSubMenuItem("Speech") + subMenu.submenu = speechMenu + return subMenu +} + +func newHideMenuItem() *MenuItem { + return newMenuItem("Hide " + globalApplication.options.Name). + SetAccelerator("CmdOrCtrl+h"). + OnClick(func(ctx *Context) { + + // C.hideApplication() + }) +} + +func newHideOthersMenuItem() *MenuItem { + return newMenuItem("Hide Others"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+h"). + OnClick(func(ctx *Context) { + // C.hideOthers() + }) +} + +func newUnhideMenuItem() *MenuItem { + return newMenuItem("Show All"). + OnClick(func(ctx *Context) { + // C.showAll() + }) +} + +func newUndoMenuItem() *MenuItem { + return newMenuItem("Undo"). + SetAccelerator("CmdOrCtrl+z"). + OnClick(func(ctx *Context) { + // C.undo() + }) +} + +// newRedoMenuItem creates a new menu item for redoing the last action +func newRedoMenuItem() *MenuItem { + return newMenuItem("Redo"). + SetAccelerator("CmdOrCtrl+Shift+z"). + OnClick(func(ctx *Context) { + // C.redo() + }) +} + +func newCutMenuItem() *MenuItem { + return newMenuItem("Cut"). + SetAccelerator("CmdOrCtrl+x"). + OnClick(func(ctx *Context) { + // C.cut() + }) +} + +func newCopyMenuItem() *MenuItem { + return newMenuItem("Copy"). + SetAccelerator("CmdOrCtrl+c"). + OnClick(func(ctx *Context) { + // C.copy() + }) +} + +func newPasteMenuItem() *MenuItem { + return newMenuItem("Paste"). + SetAccelerator("CmdOrCtrl+v"). + OnClick(func(ctx *Context) { + // C.paste() + }) +} + +func newPasteAndMatchStyleMenuItem() *MenuItem { + return newMenuItem("Paste and Match Style"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+v"). + OnClick(func(ctx *Context) { + // C.pasteAndMatchStyle() + }) +} + +func newDeleteMenuItem() *MenuItem { + return newMenuItem("Delete"). + SetAccelerator("backspace"). + OnClick(func(ctx *Context) { + // C.delete() + }) +} + +func newQuitMenuItem() *MenuItem { + return newMenuItem("Quit " + globalApplication.options.Name). + SetAccelerator("CmdOrCtrl+q"). + OnClick(func(ctx *Context) { + globalApplication.Quit() + }) +} + +func newSelectAllMenuItem() *MenuItem { + return newMenuItem("Select All"). + SetAccelerator("CmdOrCtrl+a"). + OnClick(func(ctx *Context) { + // C.selectAll() + }) +} + +func newAboutMenuItem() *MenuItem { + return newMenuItem("About " + globalApplication.options.Name). + OnClick(func(ctx *Context) { + globalApplication.ShowAboutDialog() + }) +} + +func newCloseMenuItem() *MenuItem { + return newMenuItem("Close"). + SetAccelerator("CmdOrCtrl+w"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Close() + } + }) +} + +func newReloadMenuItem() *MenuItem { + return newMenuItem("Reload"). + SetAccelerator("CmdOrCtrl+r"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Reload() + } + }) +} + +func newForceReloadMenuItem() *MenuItem { + return newMenuItem("Force Reload"). + SetAccelerator("CmdOrCtrl+Shift+r"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ForceReload() + } + }) +} + +func newToggleFullscreenMenuItem() *MenuItem { + result := newMenuItem("Toggle Full Screen"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ToggleFullscreen() + } + }) + if runtime.GOOS == "darwin" { + result.SetAccelerator("Ctrl+Command+F") + } else { + result.SetAccelerator("F11") + } + return result +} + +func newToggleDevToolsMenuItem() *MenuItem { + return newMenuItem("Toggle Developer Tools"). + SetAccelerator("Alt+Command+I"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ToggleDevTools() + } + }) +} + +func newZoomResetMenuItem() *MenuItem { + // reset zoom menu item + return newMenuItem("Actual Size"). + SetAccelerator("CmdOrCtrl+0"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomReset() + } + }) +} + +func newZoomInMenuItem() *MenuItem { + return newMenuItem("Zoom In"). + SetAccelerator("CmdOrCtrl+plus"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomIn() + } + }) +} + +func newZoomOutMenuItem() *MenuItem { + return newMenuItem("Zoom Out"). + SetAccelerator("CmdOrCtrl+-"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomOut() + } + }) +} + +func newMinimizeMenuItem() *MenuItem { + return newMenuItem("Minimize"). + SetAccelerator("CmdOrCtrl+M"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Minimise() + } + }) +} + +func newZoomMenuItem() *MenuItem { + return newMenuItem("Zoom"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Zoom() + } + }) +} + +func newFullScreenMenuItem() *MenuItem { + return newMenuItem("Fullscreen"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Fullscreen() + } + }) +} diff --git a/v3/pkg/application/options_linux.go b/v3/pkg/application/options_linux.go new file mode 100644 index 000000000..bf3d0c308 --- /dev/null +++ b/v3/pkg/application/options_linux.go @@ -0,0 +1,6 @@ +package application + +// LinuxWindow contains macOS specific options +type LinuxWindow struct { + ShowApplicationMenu bool +} diff --git a/v3/pkg/application/screen_linux.go b/v3/pkg/application/screen_linux.go new file mode 100644 index 000000000..6f8026a60 --- /dev/null +++ b/v3/pkg/application/screen_linux.go @@ -0,0 +1,29 @@ +//go:build linux + +package application + +import ( + "fmt" + "sync" +) + +func (m *linuxApp) getPrimaryScreen() (*Screen, error) { + return nil, fmt.Errorf("not implemented") +} + +func (m *linuxApp) getScreens() ([]*Screen, error) { + var wg sync.WaitGroup + var screens []*Screen + var err error + wg.Add(1) + globalApplication.dispatchOnMainThread(func() { + screens, err = getScreens(m.application) + wg.Done() + }) + wg.Wait() + return screens, err +} + +func getScreenForWindow(window *linuxWebviewWindow) (*Screen, error) { + return window.getScreen() +} diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go new file mode 100644 index 000000000..8cc74460a --- /dev/null +++ b/v3/pkg/application/systemtray_linux.go @@ -0,0 +1,90 @@ +//go:build linux + +package application + +type linuxSystemTray struct { + id uint + label string + icon []byte + menu *Menu + + iconPosition int + isTemplateIcon bool +} + +func (s *linuxSystemTray) setIconPosition(position int) { + s.iconPosition = position +} + +func (s *linuxSystemTray) setMenu(menu *Menu) { + s.menu = menu +} + +func (s *linuxSystemTray) run() { + globalApplication.dispatchOnMainThread(func() { + // if s.nsStatusItem != nil { + // Fatal("System tray '%d' already running", s.id) + // } + // s.nsStatusItem = unsafe.Pointer(C.systemTrayNew()) + if s.label != "" { + // C.systemTraySetLabel(s.nsStatusItem, C.CString(s.label)) + } + if s.icon != nil { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&s.icon[0]), C.int(len(s.icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + } + if s.menu != nil { + s.menu.Update() + // Convert impl to macosMenu object + // s.nsMenu = (s.menu.impl).(*macosMenu).nsMenu + // C.systemTraySetMenu(s.nsStatusItem, s.nsMenu) + } + + }) +} + +func (s *linuxSystemTray) setIcon(icon []byte) { + s.icon = icon + globalApplication.dispatchOnMainThread(func() { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + }) +} + +func (s *linuxSystemTray) setDarkModeIcon(icon []byte) { + s.icon = icon + globalApplication.dispatchOnMainThread(func() { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + }) +} + +func (s *linuxSystemTray) setTemplateIcon(icon []byte) { + s.icon = icon + s.isTemplateIcon = true + globalApplication.dispatchOnMainThread(func() { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + }) +} + +func newSystemTrayImpl(s *SystemTray) systemTrayImpl { + return &linuxSystemTray{ + id: s.id, + label: s.label, + icon: s.icon, + menu: s.menu, + iconPosition: s.iconPosition, + isTemplateIcon: s.isTemplateIcon, + } +} + +func (s *linuxSystemTray) setLabel(label string) { + s.label = label + // C.systemTraySetLabel(s.nsStatusItem, C.CString(label)) +} + +func (s *linuxSystemTray) destroy() { + // Remove the status item from the status bar and its associated menu + // C.systemTrayDestroy(s.nsStatusItem) +} diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go new file mode 100644 index 000000000..c1eb7b2ef --- /dev/null +++ b/v3/pkg/application/webview_window_linux.go @@ -0,0 +1,492 @@ +//go:build linux + +package application + +import ( + "fmt" + "net/url" + "sync" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v3/pkg/events" +) + +var showDevTools = func(window unsafe.Pointer) {} + +type dragInfo struct { + XRoot int + YRoot int + DragTime int + MouseButton uint +} + +type linuxWebviewWindow struct { + id uint + application pointer + window pointer + webview pointer + parent *WebviewWindow + menubar pointer + vbox pointer + menu *menu.Menu + accels pointer + lastWidth int + lastHeight int + drag dragInfo +} + +var ( + registered bool = false // avoid 'already registered message' about 'wails://' +) + +func (w *linuxWebviewWindow) startDrag() error { + return nil +} + +func (w *linuxWebviewWindow) endDrag(button uint, x, y int) { + fmt.Println("endDrag", button, x, y) +} + +func (w *linuxWebviewWindow) enableDND() { + windowEnableDND(w.parent.id, w.webview) +} + +func (w *linuxWebviewWindow) connectSignals() { + windowSetupSignalHandlers(w.parent.id, w.window, w.webview, w.parent.options.HideOnClose) +} + +func (w *linuxWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) { + // Create the menu + thisMenu := newMenuImpl(menu) + thisMenu.update() + fmt.Println("linux.openContextMenu() - not implemented") + /* void + gtk_menu_popup_at_rect ( + GtkMenu* menu, + GdkWindow* rect_window, + const GdkRectangle* rect, + GdkGravity rect_anchor, + GdkGravity menu_anchor, + const GdkEvent* trigger_event + ) + */ +} + +func (w *linuxWebviewWindow) getZoom() float64 { + return windowZoom(w.webview) +} + +func (w *linuxWebviewWindow) setZoom(zoom float64) { + windowZoomSet(w.webview, zoom) +} + +func (w *linuxWebviewWindow) setFrameless(frameless bool) { + windowSetFrameless(w.window, frameless) +} + +func (w *linuxWebviewWindow) getScreen() (*Screen, error) { + mx, my, width, height, scale := windowGetCurrentMonitorGeometry(w.window) + return &Screen{ + ID: fmt.Sprintf("%d", w.id), // A unique identifier for the display + Name: w.parent.Name(), // The name of the display + Scale: float32(scale), // The scale factor of the display + X: mx, // The x-coordinate of the top-left corner of the rectangle + Y: my, // The y-coordinate of the top-left corner of the rectangle + Size: Size{Width: width, Height: height}, // The size of the display + Bounds: Rect{}, // The bounds of the display + WorkArea: Rect{}, // The work area of the display + IsPrimary: false, // Whether this is the primary display + Rotation: 0.0, // The rotation of the display + }, nil +} + +func (w *linuxWebviewWindow) focus() { + globalApplication.dispatchOnMainThread(func() { + windowPresent(w.window) + }) +} + +func (w *linuxWebviewWindow) show() { + globalApplication.dispatchOnMainThread(func() { + windowShow(w.window) + }) +} + +func (w *linuxWebviewWindow) hide() { + globalApplication.dispatchOnMainThread(func() { + windowHide(w.window) + }) +} + +func (w *linuxWebviewWindow) isNormal() bool { + return !w.isMinimised() && !w.isMaximised() && !w.isFullscreen() +} + +func (w *linuxWebviewWindow) isVisible() bool { + return windowIsVisible(w.window) +} + +func (w *linuxWebviewWindow) setFullscreenButtonEnabled(enabled bool) { + // C.setFullscreenButtonEnabled(w.nsWindow, C.bool(enabled)) + fmt.Println("setFullscreenButtonEnabled - not implemented") +} + +func (w *linuxWebviewWindow) disableSizeConstraints() { + x, y, width, height, scale := windowGetCurrentMonitorGeometry(w.window) + w.setMinMaxSize(x, y, width*scale, height*scale) +} + +func (w *linuxWebviewWindow) unfullscreen() { + fmt.Println("unfullscreen") + globalApplication.dispatchOnMainThread(func() { + windowUnfullscreen(w.window) + w.unmaximise() + }) +} + +func (w *linuxWebviewWindow) fullscreen() { + w.maximise() + w.lastWidth, w.lastHeight = w.size() + globalApplication.dispatchOnMainThread(func() { + x, y, width, height, scale := windowGetCurrentMonitorGeometry(w.window) + if x == -1 && y == -1 && width == -1 && height == -1 { + return + } + w.setMinMaxSize(0, 0, width*scale, height*scale) + w.setSize(width*scale, height*scale) + windowFullscreen(w.window) + w.setPosition(0, 0) + }) +} + +func (w *linuxWebviewWindow) unminimise() { + windowPresent(w.window) +} + +func (w *linuxWebviewWindow) unmaximise() { + windowUnmaximize(w.window) +} + +func (w *linuxWebviewWindow) maximise() { + windowMaximize(w.window) +} + +func (w *linuxWebviewWindow) minimise() { + windowMinimize(w.window) +} + +func (w *linuxWebviewWindow) on(eventID uint) { + // Don't think this is correct! + // GTK Events are strings + fmt.Println("on()", eventID) + //C.registerListener(C.uint(eventID)) +} + +func (w *linuxWebviewWindow) zoom() { + w.zoomIn() +} + +func (w *linuxWebviewWindow) windowZoom() { + w.zoom() // FIXME> This should be removed +} + +func (w *linuxWebviewWindow) close() { + windowClose(w.window) + if !w.parent.options.HideOnClose { + globalApplication.deleteWindowByID(w.parent.id) + } +} + +func (w *linuxWebviewWindow) zoomIn() { + windowZoomIn(w.webview) +} + +func (w *linuxWebviewWindow) zoomOut() { + windowZoomOut(w.webview) +} + +func (w *linuxWebviewWindow) zoomReset() { + windowZoomSet(w.webview, 0.0) +} + +func (w *linuxWebviewWindow) reload() { + windowReload(w.webview, "wails://") +} + +func (w *linuxWebviewWindow) forceReload() { + w.reload() +} + +func (w *linuxWebviewWindow) center() { + globalApplication.dispatchOnMainThread(func() { + x, y, width, height, _ := windowGetCurrentMonitorGeometry(w.window) + if x == -1 && y == -1 && width == -1 && height == -1 { + return + } + windowWidth, windowHeight := windowGetSize(w.window) + + newX := ((width - int(windowWidth)) / 2) + x + newY := ((height - int(windowHeight)) / 2) + y + + // Place the window at the center of the monitor + windowMove(w.window, newX, newY) + }) +} + +func (w *linuxWebviewWindow) isMinimised() bool { + return windowIsMinimized(w.window) +} + +func (w *linuxWebviewWindow) isMaximised() bool { + return w.syncMainThreadReturningBool(func() bool { + return windowIsMaximized(w.window) + }) +} + +func (w *linuxWebviewWindow) isFullscreen() bool { + return w.syncMainThreadReturningBool(func() bool { + return windowIsFullscreen(w.window) + }) +} + +func (w *linuxWebviewWindow) syncMainThreadReturningBool(fn func() bool) bool { + var wg sync.WaitGroup + wg.Add(1) + var result bool + globalApplication.dispatchOnMainThread(func() { + result = fn() + wg.Done() + }) + wg.Wait() + return result +} + +func (w *linuxWebviewWindow) restore() { + // restore window to normal size + // FIXME: never called! - remove from webviewImpl interface +} + +func (w *linuxWebviewWindow) execJS(js string) { + windowExecJS(w.webview, js) +} + +func (w *linuxWebviewWindow) setURL(uri string) { + if uri != "" { + url, err := url.Parse(uri) + if err == nil && url.Scheme == "" && url.Host == "" { + // TODO handle this in a central location, the scheme and host might be platform dependant. + url.Scheme = "wails" + url.Host = "wails" + uri = url.String() + } + } + windowSetURL(w.webview, uri) +} + +func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { + windowSetKeepAbove(w.window, alwaysOnTop) +} + +func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { + // (*C.struct__GtkWidget)(m.native) + //var menubar *C.struct__GtkWidget + return &linuxWebviewWindow{ + application: getNativeApplication().application, + parent: parent, + // menubar: menubar, + } +} + +func (w *linuxWebviewWindow) setTitle(title string) { + if !w.parent.options.Frameless { + windowSetTitle(w.window, title) + } +} + +func (w *linuxWebviewWindow) setSize(width, height int) { + windowResize(w.window, width, height) +} + +func (w *linuxWebviewWindow) setMinMaxSize(minWidth, minHeight, maxWidth, maxHeight int) { + if minWidth == 0 { + minWidth = -1 + } + if minHeight == 0 { + minHeight = -1 + } + if maxWidth == 0 { + maxWidth = -1 + } + if maxHeight == 0 { + maxHeight = -1 + } + windowSetGeometryHints(w.window, minWidth, minHeight, maxWidth, maxHeight) +} + +func (w *linuxWebviewWindow) setMinSize(width, height int) { + w.setMinMaxSize(width, height, w.parent.options.MaxWidth, w.parent.options.MaxHeight) +} + +func (w *linuxWebviewWindow) setMaxSize(width, height int) { + w.setMinMaxSize(w.parent.options.MinWidth, w.parent.options.MinHeight, width, height) +} + +func (w *linuxWebviewWindow) setResizable(resizable bool) { + windowSetResizable(w.window, resizable) +} + +func (w *linuxWebviewWindow) toggleDevTools() { + windowToggleDevTools(w.webview) +} + +func (w *linuxWebviewWindow) size() (int, int) { + /* var width, height C.int + var wg sync.WaitGroup + wg.Add(1) + globalApplication.dispatchOnMainThread(func() { + + C.gtk_window_get_size((*C.GtkWindow)(w.window), &width, &height) + wg.Done() + }) + wg.Wait() + return int(width), int(height) + */ + // Does this need to be guarded? + return windowGetSize(w.window) +} + +func (w *linuxWebviewWindow) setPosition(x, y int) { + mx, my, _, _, _ := windowGetCurrentMonitorGeometry(w.window) + globalApplication.dispatchOnMainThread(func() { + windowMove(w.window, x+mx, y+my) + }) +} + +func (w *linuxWebviewWindow) width() int { + width, _ := w.size() + return width +} + +func (w *linuxWebviewWindow) height() int { + _, height := w.size() + return height +} + +func (w *linuxWebviewWindow) run() { + for eventId := range w.parent.eventListeners { + w.on(eventId) + } + + app := getNativeApplication() + menu := app.applicationMenu + + globalApplication.dispatchOnMainThread(func() { + w.window, w.webview = windowNew(app.application, menu, w.parent.id, 1) + app.registerWindow(w.window, w.parent.id) // record our mapping + w.connectSignals() + if w.parent.options.EnableDragAndDrop { + w.enableDND() + } + w.setTitle(w.parent.options.Title) + w.setAlwaysOnTop(w.parent.options.AlwaysOnTop) + w.setResizable(!w.parent.options.DisableResize) + // only set min/max size if actually set + if w.parent.options.MinWidth != 0 && + w.parent.options.MinHeight != 0 && + w.parent.options.MaxWidth != 0 && + w.parent.options.MaxHeight != 0 { + w.setMinMaxSize( + w.parent.options.MinWidth, + w.parent.options.MinHeight, + w.parent.options.MaxWidth, + w.parent.options.MaxHeight, + ) + } + w.setSize(w.parent.options.Width, w.parent.options.Height) + w.setZoom(w.parent.options.Zoom) + w.setBackgroundColour(w.parent.options.BackgroundColour) + w.setFrameless(w.parent.options.Frameless) + + if w.parent.options.X != 0 || w.parent.options.Y != 0 { + w.setPosition(w.parent.options.X, w.parent.options.Y) + } else { + fmt.Println("attempting to set in the center") + w.center() + } + switch w.parent.options.StartState { + case WindowStateMaximised: + w.maximise() + case WindowStateMinimised: + w.minimise() + case WindowStateFullscreen: + w.fullscreen() + } + + if w.parent.options.URL != "" { + w.setURL(w.parent.options.URL) + } + // We need to wait for the HTML to load before we can execute the javascript + // FIXME: What event is this? DomReady? + w.parent.On(events.Mac.WebViewDidFinishNavigation, func(_ *WindowEventContext) { + if w.parent.options.JS != "" { + w.execJS(w.parent.options.JS) + } + if w.parent.options.CSS != "" { + js := fmt.Sprintf("(function() { var style = document.createElement('style'); style.appendChild(document.createTextNode('%s')); document.head.appendChild(style); })();", w.parent.options.CSS) + w.execJS(js) + } + }) + if w.parent.options.HTML != "" { + w.setHTML(w.parent.options.HTML) + } + if !w.parent.options.Hidden { + w.show() + if w.parent.options.X != 0 || w.parent.options.Y != 0 { + w.setPosition(w.parent.options.X, w.parent.options.Y) + } else { + w.center() // needs to be queued until after GTK starts up! + } + } + }) +} + +func (w *linuxWebviewWindow) setTransparent() { + windowSetTransparent(w.window) +} + +func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { + if colour.Alpha != 0 { + w.setTransparent() + } + windowSetBackgroundColour(w.webview, colour) +} + +func (w *linuxWebviewWindow) position() (int, int) { + var x, y int + var wg sync.WaitGroup + wg.Add(1) + go globalApplication.dispatchOnMainThread(func() { + x, y = windowGetPosition(w.window) + wg.Done() + }) + wg.Wait() + return x, y +} + +func (w *linuxWebviewWindow) destroy() { + windowDestroy(w.window) +} + +func (w *linuxWebviewWindow) setHTML(html string) { + windowSetHTML(w.webview, html) +} + +func (w *linuxWebviewWindow) startResize(border string) error { + // FIXME: what do we need to do here? + return nil +} + +func (w *linuxWebviewWindow) nativeWindowHandle() uintptr { + return uintptr(w.window) +} From e44fbc26bad31cfdf2179bd92a9846c369215672 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 15 Jun 2023 14:04:26 -0500 Subject: [PATCH 03/17] [v3 linux] STATUS.md --- v3/STATUS.md | 114 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/v3/STATUS.md b/v3/STATUS.md index 1bfb6d08a..784f4a54b 100644 --- a/v3/STATUS.md +++ b/v3/STATUS.md @@ -16,19 +16,19 @@ Application interface methods | Method | Windows | Linux | Mac | Notes | |---------------------------------------------------------------|---------|-------|-----|-------| -| run() error | Y | | Y | | -| destroy() | | | Y | | -| setApplicationMenu(menu *Menu) | Y | | Y | | -| name() string | | | Y | | -| getCurrentWindowID() uint | Y | | Y | | -| showAboutDialog(name string, description string, icon []byte) | | | Y | | -| setIcon(icon []byte) | - | | Y | | +| run() error | Y | Y | Y | | +| destroy() | | Y | Y | | +| setApplicationMenu(menu *Menu) | Y | Y | Y | | +| name() string | | Y | Y | | +| getCurrentWindowID() uint | Y | Y | Y | | +| showAboutDialog(name string, description string, icon []byte) | | Y | Y | | +| setIcon(icon []byte) | - | Y | Y | | | on(id uint) | | | Y | | -| dispatchOnMainThread(fn func()) | Y | | Y | | -| hide() | Y | | Y | | -| show() | Y | | Y | | -| getPrimaryScreen() (*Screen, error) | | | Y | | -| getScreens() ([]*Screen, error) | | | Y | | +| dispatchOnMainThread(fn func()) | Y | Y | Y | | +| hide() | Y | Y | Y | | +| show() | Y | Y | Y | | +| getPrimaryScreen() (*Screen, error) | | Y | Y | | +| getScreens() ([]*Screen, error) | | Y | Y | | ## Webview Window @@ -90,7 +90,7 @@ Webview Window Interface Methods | Feature | Windows | Linux | Mac | Notes | |---------|---------|-------|-----|-------| -| Quit | Y | | Y | | +| Quit | Y | Y | Y | | | Hide | Y | | Y | | | Show | Y | | Y | | @@ -98,10 +98,10 @@ Webview Window Interface Methods | Feature | Windows | Linux | Mac | Notes | |----------|---------|-------|-----|-------| -| Info | Y | | Y | | -| Warning | Y | | Y | | -| Error | Y | | Y | | -| Question | Y | | Y | | +| Info | Y | Y | Y | | +| Warning | Y | Y | Y | | +| Error | Y | Y | Y | | +| Question | Y | Y | Y | | | OpenFile | Y | | Y | | | SaveFile | Y | | Y | | @@ -132,9 +132,9 @@ explicitly set with `--default-contextmenu: show`. | Feature | Windows | Linux | Mac | Notes | |------------|---------|-------|-----|-------| -| GetAll | Y | | Y | | -| GetPrimary | Y | | Y | | -| GetCurrent | Y | | Y | | +| GetAll | Y | Y | Y | | +| GetPrimary | Y | Y | Y | | +| GetCurrent | Y | Y | Y | | ### Window @@ -183,37 +183,38 @@ An 'X' indicates that the option is not supported by the platform. | Feature | Windows | Linux | Mac | Notes | |---------------------------------|---------|-------|-----|--------------------------------------------| | AlwaysOnTop | Y | | | | -| BackgroundColour | Y | | | | +| BackgroundColour | Y | Y | | | | BackgroundType | | | | Acrylic seems to work but the others don't | -| CSS | Y | | | | -| DevToolsEnabled | Y | | Y | | -| DisableResize | Y | | | | -| EnableDragAndDrop | | | | | +| CSS | Y | Y | | | +| DevToolsEnabled | Y | Y | Y | | +| DisableResize | Y | Y | | | +| EnableDragAndDrop | | Y | | | | EnableFraudulentWebsiteWarnings | | | | | -| Focused | Y | | | | -| Frameless | Y | | | | +| Focused | Y | Y | | | +| Frameless | Y | Y | | | | FullscreenButtonEnabled | Y | | | | -| Height | Y | | | | -| Hidden | Y | | | | -| HTML | Y | | | | -| JS | Y | | | | +| Height | Y | Y | | | +| Hidden | Y | Y | | | +| HTML | Y | Y | | | +| JS | Y | Y | | | | Mac | - | - | | | -| MaxHeight | Y | | | | -| MaxWidth | Y | | | | -| MinHeight | Y | | | | -| MinWidth | Y | | | | -| Name | Y | | | | +| MaxHeight | Y | Y | | | +| MaxWidth | Y | Y | | | +| MinHeight | Y | Y | | | +| MinWidth | Y | Y | | | +| Name | Y | Y | | | | OpenInspectorOnStartup | | | | | | StartState | Y | | | | -| Title | Y | | | | -| URL | Y | | | | -| Width | Y | | | | +| Title | Y | Y | | | +| URL | Y | Y | | | +| Width | Y | Y | | | | Windows | Y | - | - | | -| X | Y | | | | -| Y | Y | | | | +| X | Y | Y | | | +| Y | Y | Y | | | | Zoom | | | | | | ZoomControlEnabled | | | | | + ### Log To log or not to log? System logger vs custom logger. @@ -222,7 +223,7 @@ To log or not to log? System logger vs custom logger. | Event | Windows | Linux | Mac | Notes | |--------------------------|---------|-------|-----|-------| -| Default Application Menu | Y | | Y | | +| Default Application Menu | Y | Y | Y | | ## Tray Menus @@ -295,13 +296,15 @@ Built-in plugin support: | Plugin | Windows | Linux | Mac | Notes | |-----------------|---------|-------|-----|-------| | Browser | Y | | Y | | -| KV Store | Y | | Y | | -| Log | Y | | Y | | +| KV Store | Y | Y | Y | | +| Log | Y | Y | Y | | | Single Instance | Y | | Y | | -| SQLite | Y | | Y | | +| SQLite | Y | Y | Y | | | Start at login | | | Y | | | Server | | | | | +TODO: + - Ensure each plugin has a JS wrapper that can be injected into the window. ## Packaging @@ -320,7 +323,8 @@ Built-in plugin support: | Feature | Windows | Linux | Mac | Notes | |---------|---------|-------|-----|-----------------------------------------------| | Resize | | | | | -| Drag | | Y | | Linux - can always drag with `Alt`+left mouse | +| Drag | | Y | | Linux - can always drag with `Meta`+left mouse | + ## Mac Specific @@ -355,6 +359,24 @@ Built-in plugin support: ## Linux Specific + +Implementation details for the functions utilized by the `*_linux.go` files are located in the following files: + +- linux_cgo.go: CGo implementation +- linux_purego.go: PureGo implementation + +### CGO + +By default CGO is utilized to compile the Linux port. This prevents easy cross-compilation and so the PureGo implementation is also being simultaneously developed. + +### Purego + +The examples can be compiled using the following command: + + CGO_ENABLED=0 go build -tags purego + +Note: things are currently not working after the refactor + ## Examples | Example | Windows | Linux | Mac | From da4a7ac64b7d5c97e312f87c8098aa5d39fafb85 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:37:43 -0500 Subject: [PATCH 04/17] [v2] assetserver/webview purego implementation --- .../webview/request_linux_purego.go | 94 ++++++++++ .../webview/responsewriter_linux_purego.go | 174 ++++++++++++++++++ .../assetserver/webview/webkit2_36+_purego.go | 94 ++++++++++ .../assetserver/webview/webkit2_40+_purego.go | 74 ++++++++ .../webview/webkit2_legacy_purego.go | 36 ++++ 5 files changed, 472 insertions(+) create mode 100644 v2/pkg/assetserver/webview/request_linux_purego.go create mode 100644 v2/pkg/assetserver/webview/responsewriter_linux_purego.go create mode 100644 v2/pkg/assetserver/webview/webkit2_36+_purego.go create mode 100644 v2/pkg/assetserver/webview/webkit2_40+_purego.go create mode 100644 v2/pkg/assetserver/webview/webkit2_legacy_purego.go diff --git a/v2/pkg/assetserver/webview/request_linux_purego.go b/v2/pkg/assetserver/webview/request_linux_purego.go new file mode 100644 index 000000000..bf724a55b --- /dev/null +++ b/v2/pkg/assetserver/webview/request_linux_purego.go @@ -0,0 +1,94 @@ +//go:build linux && purego +// +build linux,purego + +package webview + +import ( + "io" + "net/http" + + "github.com/ebitengine/purego" +) + +// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest` +// +// Please make sure to call Release() when finished using the request. +func NewRequest(webKitURISchemeRequest uintptr) Request { + webkitReq := webKitURISchemeRequest + req := &request{req: webkitReq} + req.AddRef() + return req +} + +var _ Request = &request{} + +type request struct { + req uintptr + + header http.Header + body io.ReadCloser + rw *responseWriter +} + +func (r *request) AddRef() error { + var objectRef func(uintptr) + purego.RegisterLibFunc(&objectRef, gtk, "g_object_ref") + objectRef(r.req) + return nil +} + +func (r *request) Release() error { + var objectUnref func(uintptr) + purego.RegisterLibFunc(&objectUnref, gtk, "g_object_unref") + objectUnref(r.req) + return nil +} + +func (r *request) URL() (string, error) { + var getUri func(uintptr) string + purego.RegisterLibFunc(&getUri, webkit, "webkit_uri_scheme_request_get_uri") + return getUri(r.req), nil +} + +func (r *request) Method() (string, error) { + return webkit_uri_scheme_request_get_http_method(r.req), nil +} + +func (r *request) Header() (http.Header, error) { + if r.header != nil { + return r.header, nil + } + + r.header = webkit_uri_scheme_request_get_http_headers(r.req) + return r.header, nil +} + +func (r *request) Body() (io.ReadCloser, error) { + if r.body != nil { + return r.body, nil + } + + // WebKit2GTK has currently no support for request bodies. + r.body = http.NoBody + + return r.body, nil +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{req: r.req} + return r.rw +} + +func (r *request) Close() error { + var err error + if r.body != nil { + err = r.body.Close() + } + r.Response().Finish() + r.Release() + return err +} diff --git a/v2/pkg/assetserver/webview/responsewriter_linux_purego.go b/v2/pkg/assetserver/webview/responsewriter_linux_purego.go new file mode 100644 index 000000000..c62f54a55 --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter_linux_purego.go @@ -0,0 +1,174 @@ +//go:build linux && purego +// +build linux,purego + +package webview + +import ( + "fmt" + "io" + "net/http" + "os" + "strconv" + "syscall" + + "github.com/ebitengine/purego" +) + +const ( + gtk3 = "libgtk-3.so" + gtk4 = "libgtk-4.so" +) + +var ( + gtk uintptr + webkit uintptr + version int +) + +func init() { + var err error + // gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + // if err == nil { + // version = 4 + // return + // } + // log.Println("Failed to open GTK4: Falling back to GTK3") + gtk, err = purego.Dlopen(gtk3, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + version = 3 + + var webkit4 string = "libwebkit2gtk-4.1.so" + webkit, err = purego.Dlopen(webkit4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } +} + +type responseWriter struct { + req uintptr + + header http.Header + wroteHeader bool + finished bool + + w io.WriteCloser + wErr error +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + if rw.wErr != nil { + return 0, rw.wErr + } + return rw.w.Write(buf) +} + +func (rw *responseWriter) WriteHeader(code int) { + // TODO? Is this ever called? I don't think so! + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + contentLength := int64(-1) + if sLen := rw.Header().Get(HeaderContentLength); sLen != "" { + if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 { + contentLength = pLen + } + } + // We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the + // read FD is given to the InputStream and will be closed there. + // Furthermore we especially don't want to have the FD_CLOEXEC + rFD, w, err := pipe() + if err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err)) + return + } + rw.w = w + + var newStream func(int, bool) uintptr + purego.RegisterLibFunc(&newStream, gtk, "g_unix_input_stream_new") + var unRef func(uintptr) + purego.RegisterLibFunc(&unRef, gtk, "g_object_unref") + stream := newStream(rFD, true) + + /* var reqFinish func(uintptr, uintptr, uintptr, uintptr, int64) int + purego.RegisterLibFunc(&reqFinish, webkit, "webkit_uri_scheme_request_finish") + + header := rw.Header() + defer unRef(stream) + if err := reqFinish(rw.req, code, header, stream, contentLength); err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) + } + */ + if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) + return + } +} + +func (rw *responseWriter) Finish() { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return + } + rw.finished = true + if rw.w != nil { + rw.w.Close() + } +} + +func (rw *responseWriter) finishWithError(code int, err error) { + if rw.w != nil { + rw.w.Close() + rw.w = &nopCloser{io.Discard} + } + rw.wErr = err + + var newLiteral func(uint32, string, int, string) uintptr // is this correct? + purego.RegisterLibFunc(&newLiteral, gtk, "g_error_new_literal") + var newQuark func(string) uintptr + purego.RegisterLibFunc(&newQuark, gtk, "g_quark_from_string") + var freeError func(uintptr) + purego.RegisterLibFunc(&freeError, gtk, "g_error_free") + var finishError func(uintptr, uintptr) + purego.RegisterLibFunc(&finishError, webkit, "webkit_uri_scheme_request_finish_error") + + msg := string(err.Error()) + //gquark := newQuark(msg) + gerr := newLiteral(1, msg, code, msg) + finishError(rw.req, gerr) + freeError(gerr) +} + +type nopCloser struct { + io.Writer +} + +func (nopCloser) Close() error { return nil } + +func pipe() (r int, w *os.File, err error) { + var p [2]int + e := syscall.Pipe2(p[0:], 0) + if e != nil { + return 0, nil, fmt.Errorf("pipe2: %s", e) + } + + return p[0], os.NewFile(uintptr(p[1]), "|1"), nil +} diff --git a/v2/pkg/assetserver/webview/webkit2_36+_purego.go b/v2/pkg/assetserver/webview/webkit2_36+_purego.go new file mode 100644 index 000000000..2386868c3 --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_36+_purego.go @@ -0,0 +1,94 @@ +//go:build linux && (webkit2_36 || webkit2_40) && purego + +package webview + +import ( + "net/http" + "strings" + + "github.com/ebitengine/purego" +) + +func webkit_uri_scheme_request_get_http_method(req uintptr) string { + var getMethod func(uintptr) string + purego.RegisterLibFunc(&getMethod, gtk, "webkit_uri_scheme_request_get_http_method") + return strings.ToUpper(getMethod(req)) +} + +func webkit_uri_scheme_request_get_http_headers(req uintptr) http.Header { + var getHeaders func(uintptr) uintptr + purego.RegisterLibFunc(&getUri, webkit, "webkit_uri_scheme_request_get_http_headers") + + hdrs := getHeaders(req) + + var headersIterInit func(uintptr, uintptr) uintptr + purego.RegisterLibFunc(&headersIterInit, gtk, "soup_message_headers_iter_init") + + // TODO: How do we get a struct? + /* + typedef struct { + SoupMessageHeaders *hdrs; + int index_common; + int index_uncommon; + } SoupMessageHeadersIterReal; + */ + iter := make([]byte, 12) + headersIterInit(&iter, hdrs) + + var iterNext func(uintptr, *string, *string) int + purego.RegisterLibFunc(&iterNext, gtk, "soup_message_headers_iter_next") + + var name string + var value string + h := http.Header{} + + for iterNext(&iter, &name, &value) != 0 { + h.Add(name, value) + } + + return h +} + +func webkit_uri_scheme_request_finish(req uintptr, code int, header http.Header, stream uintptr, streamLength int64) error { + + var newResponse func(uintptr, int64) string + purego.RegisterLibFunc(&newResponse, webkit, "webkit_uri_scheme_response_new") + var unRef func(uintptr) + purego.RegisterLibFunc(&unRef, gtk, "g_object_unref") + + resp := newResponse(stream, streamLength) + defer unRef(resp) + + var setStatus func(uintptr, int, string) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_status") + + setStatus(resp, code, cReason) + + var setContentType func(uintptr, string) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_content_type") + + setContentType(resp, header.Get(HeaderContentType)) + + soup := gtk + var soupHeadersNew func(int) uintptr + purego.RegisterLibFunc(&unRef, soup, "soup_message_headers_new") + var soupHeadersAppend func(uintptr, string, string) + purego.RegisterLibFunc(&unRef, soup, "soup_message_headers_append") + + hdrs := soupHeadersNew(SOUP_MESSAGE_HEADERS_RESPONSE) + for name, values := range header { + for _, value := range values { + soupHeadersAppend(hdrs, name, value) + } + } + + var setHttpHeaders func(uintptr, uintptr) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_http_headers") + + setHttpHeaders(resp, hdrs) + var finishWithResponse func(uintptr, uintptr) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_request_finish_with_response") + finishWithResponse(req, resp) + + return nil +} diff --git a/v2/pkg/assetserver/webview/webkit2_40+_purego.go b/v2/pkg/assetserver/webview/webkit2_40+_purego.go new file mode 100644 index 000000000..1088be25e --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_40+_purego.go @@ -0,0 +1,74 @@ +//go:build linux && webkit2_40 && purego + +package webview + +import ( + "fmt" + "io" + "net/http" + "unsafe" +) + +func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser { + stream := C.webkit_uri_scheme_request_get_http_body(req) + if stream == nil { + return http.NoBody + } + return &webkitRequestBody{stream: stream} +} + +type webkitRequestBody struct { + stream *C.GInputStream + closed bool +} + +// Read implements io.Reader +func (r *webkitRequestBody) Read(p []byte) (int, error) { + if r.closed { + return 0, io.ErrClosedPipe + } + + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + var n C.gsize + var gErr *C.GError + res := C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr) + if res == 0 { + return 0, formatGError("stream read failed", gErr) + } else if n == 0 { + return 0, io.EOF + } + return int(n), nil +} + +func (r *webkitRequestBody) Close() error { + if r.closed { + return nil + } + r.closed = true + + // https://docs.gtk.org/gio/method.InputStream.close.html + // Streams will be automatically closed when the last reference is dropped, but you might want to call this function + // to make sure resources are released as early as possible. + var err error + var gErr *C.GError + if C.g_input_stream_close(r.stream, nil, &gErr) == 0 { + err = formatGError("stream close failed", gErr) + } + C.g_object_unref(C.gpointer(r.stream)) + r.stream = nil + return err +} + +func formatGError(msg string, gErr *C.GError, args ...any) error { + if gErr != nil && gErr.message != nil { + msg += ": " + C.GoString(gErr.message) + C.g_error_free(gErr) + } + return fmt.Errorf(msg, args...) +} diff --git a/v2/pkg/assetserver/webview/webkit2_legacy_purego.go b/v2/pkg/assetserver/webview/webkit2_legacy_purego.go new file mode 100644 index 000000000..2e88864c8 --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_legacy_purego.go @@ -0,0 +1,36 @@ +//go:build linux && !(webkit2_36 || webkit2_40) && purego + +package webview + +import ( + "fmt" + "io" + "net/http" + + "github.com/ebitengine/purego" +) + +const Webkit2MinMinorVersion = 0 + +func webkit_uri_scheme_request_get_http_method(_ uintptr) string { + return http.MethodGet +} + +func webkit_uri_scheme_request_get_http_headers(_ uintptr) http.Header { + return http.Header{} +} + +func webkit_uri_scheme_request_get_http_body(_ uintptr) io.ReadCloser { + return http.NoBody +} + +func webkit_uri_scheme_request_finish(req uintptr, code int, header http.Header, stream uintptr, streamLength int64) error { + if code != http.StatusOK { + return fmt.Errorf("StatusCodes not supported: %d - %s", code, http.StatusText(code)) + } + + var requestFinish func(uintptr, uintptr, int64, string) + purego.RegisterLibFunc(&requestFinish, webkit, "webkit_uri_scheme_request_finish") + requestFinish(req, stream, streamLength, header.Get(HeaderContentType)) + return nil +} From de2e78b50759329e30abfbd2ed88c4d828bae9f4 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 15 Jun 2023 14:04:41 -0500 Subject: [PATCH 05/17] [v3 linux] purego implementation --- v3/examples/binding/go.mod | 5 +- v3/examples/binding/go.sum | 2 + v3/go.mod | 3 + v3/go.sum | 2 + v3/pkg/application/linux_purego.go | 1062 ++++++++++++++++++++++++++++ 5 files changed, 1073 insertions(+), 1 deletion(-) create mode 100644 v3/pkg/application/linux_purego.go diff --git a/v3/examples/binding/go.mod b/v3/examples/binding/go.mod index af6d1a3d0..f46983cc9 100644 --- a/v3/examples/binding/go.mod +++ b/v3/examples/binding/go.mod @@ -6,6 +6,7 @@ require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 require ( github.com/bep/debounce v1.2.1 // indirect + github.com/ebitengine/purego v0.4.0-alpha.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect @@ -16,7 +17,7 @@ require ( github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/go-webview2 v1.0.2-0.20230604075323-d593c659ca7b // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect + github.com/wailsapp/wails/v2 v2.5.1 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.8.0 // indirect @@ -25,3 +26,5 @@ require ( replace github.com/wailsapp/wails/v3 => ../.. replace github.com/wailsapp/wails/v2 => ../../../v2 + +replace github.com/ebitengine/purego v0.4.0-alpha.4 => github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01 diff --git a/v3/examples/binding/go.sum b/v3/examples/binding/go.sum index fc2f20f32..b95eaf8ed 100644 --- a/v3/examples/binding/go.sum +++ b/v3/examples/binding/go.sum @@ -26,6 +26,8 @@ github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpo github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01 h1:oQwu3iNDywGp1Hry+PDvz+grwbCGpzY+ckSnWKCnX5Y= +github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/wailsapp/go-webview2 v1.0.2-0.20230604075323-d593c659ca7b h1:cztK9x+ikg6nFscy5c8NgtfIXv/d0ESdENy9+JkE8i4= github.com/wailsapp/go-webview2 v1.0.2-0.20230604075323-d593c659ca7b/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= diff --git a/v3/go.mod b/v3/go.mod index ec8262c08..38fce0226 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/bep/debounce v1.2.1 + github.com/ebitengine/purego v0.4.0-alpha.4 github.com/go-ole/go-ole v1.2.6 github.com/go-task/task/v3 v3.20.0 github.com/google/go-cmp v0.5.9 @@ -75,3 +76,5 @@ require ( ) replace github.com/wailsapp/wails/v2 => ../v2 + +replace github.com/ebitengine/purego v0.4.0-alpha.4 => github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01 diff --git a/v3/go.sum b/v3/go.sum index 715d5133d..6b4f5ce7c 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -143,6 +143,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8= github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A= +github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01 h1:oQwu3iNDywGp1Hry+PDvz+grwbCGpzY+ckSnWKCnX5Y= +github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/wailsapp/go-webview2 v1.0.2-0.20230604075323-d593c659ca7b h1:cztK9x+ikg6nFscy5c8NgtfIXv/d0ESdENy9+JkE8i4= github.com/wailsapp/go-webview2 v1.0.2-0.20230604075323-d593c659ca7b/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go new file mode 100644 index 000000000..cc7d6b8b7 --- /dev/null +++ b/v3/pkg/application/linux_purego.go @@ -0,0 +1,1062 @@ +//go:build linux && purego + +package application + +import ( + "fmt" + "os" + "strings" + "unsafe" + + "github.com/ebitengine/purego" + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +type windowPointer uintptr +type identifier uint +type pointer uintptr +type GSList uintptr +type GSListPointer *GSList + +const ( + GSourceRemove int = 0 + + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkwindow.h#L121 + GdkHintMinSize = 1 << 1 + GdkHintMaxSize = 1 << 2 + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkevents.h#L512 + GdkWindowStateIconified = 1 << 1 + GdkWindowStateMaximized = 1 << 2 + GdkWindowStateFullscreen = 1 << 4 + + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkmessagedialog.h#L87 + GtkButtonsNone int = iota + GtkButtonsOk + GtkButtonsClose + GtkButtonsCancel + GtkButtonsYesNo + GtkButtonsOkCancel + + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkdialog.h#L36 + GtkDialogModal = 1 << 0 + GtkDialogDestroyWithParent = 1 << 1 + GtkDialogUseHeaderBar = 1 << 2 // actions in header bar instead of action area + + GtkOrientationVertical = 1 + + // enum GtkMessageType + GtkMessageInfo = iota + GtkMessageWarning + GtkMessageQuestion + GtkMessageError +) + +type GdkGeometry struct { + minWidth int32 + minHeight int32 + maxWidth int32 + maxHeight int32 + baseWidth int32 + baseHeight int32 + widthInc int32 + heightInc int32 + padding int32 + minAspect float64 + maxAspect float64 + GdkGravity int32 +} + +var ( + nilRadioGroup GSListPointer = nil + gtkSignalHandlers map[pointer]uint = map[pointer]uint{} + gtkSignalToMenuItem map[pointer]*MenuItem = map[pointer]*MenuItem{} +) + +const ( + gtk3 = "libgtk-3.so" + gtk4 = "libgtk-4.so" +) + +var ( + gtk uintptr + version int + webkit uintptr + + // function references + gIdleAdd func(uintptr) + gApplicationHold func(pointer) + gApplicationQuit func(pointer) + gApplicationName func() string + gApplicationRelease func(pointer) + gApplicationRun func(pointer, int, []string) int + gObjectRefSink func(pointer) + gObjectUnref func(pointer) + gSignalConnectData func(pointer, string, uintptr, pointer, bool, int) int + gSignalConnectObject func(pointer, string, pointer, pointer, int) uint + gSignalHandlerBlock func(pointer, uint) + gSignalHandlerUnblock func(pointer, uint) + + // gdk functions + gdkDisplayGetMonitor func(pointer, int) pointer + gdkDisplayGetMonitorAtWindow func(pointer, pointer) pointer + gdkDisplayGetNMonitors func(pointer) int + gdkMonitorGetGeometry func(pointer, pointer) pointer + gdkMonitorGetScaleFactor func(pointer) int + gdkMonitorIsPrimary func(pointer) int + gdkRgbaParse func(pointer, string) bool + gdkScreenGetRgbaVisual func(pointer) pointer + gdkScreenIsComposited func(pointer) int + gdkWindowGetState func(pointer) int + gdkWindowGetDisplay func(pointer) pointer + + // gtk functions + gtkApplicationNew func(string, uint) pointer + gtkApplicationGetActiveWindow func(pointer) pointer + gtkApplicationGetWindows func(pointer) *GList + gtkApplicationWindowNew func(pointer) pointer + gtkBoxNew func(int, int) pointer + gtkBoxPackStart func(pointer, pointer, int, int, int) + gtkCheckMenuItemGetActive func(pointer) int + gtkCheckMenuItemNewWithLabel func(string) pointer + gtkCheckMenuItemSetActive func(pointer, int) + gtkContainerAdd func(pointer, pointer) + gtkDialogAddButton func(pointer, string, int) + gtkDialogRun func(pointer) int + gtkDialogSetDefaultResponse func(pointer, int) + gtkDragDestSet func(pointer, uint, pointer, uint, uint) + gtkMenuBarNew func() pointer + gtkMenuItemNewWithLabel func(string) pointer + gtkMenuItemSetLabel func(pointer, string) + gtkMenuItemSetSubmenu func(pointer, pointer) + gtkMenuNew func() pointer + gtkMenuShellAppend func(pointer, pointer) + gtkMessageDialogNew func(pointer, int, int, int, string) pointer + gtkRadioMenuItemGetGroup func(pointer) GSListPointer + gtkRadioMenuItemNewWithLabel func(GSListPointer, string) pointer + gtkSeparatorMenuItemNew func() pointer + gtkTargetEntryFree func(pointer) + gtkTargetEntryNew func(string, int, uint) pointer + gtkWidgetDestroy func(pointer) + gtkWidgetGetDisplay func(pointer) pointer + gtkWidgetGetWindow func(pointer) pointer + gtkWidgetGetScreen func(pointer) pointer + gtkWidgetHide func(pointer) + gtkWidgetIsVisible func(pointer) bool + gtkWidgetShow func(pointer) + gtkWidgetShowAll func(pointer) + gtkWidgetSetAppPaintable func(pointer, int) + gtkWidgetSetSensitive func(pointer, int) + gtkWidgetSetToolTipText func(pointer, string) + gtkWidgetSetVisual func(pointer, pointer) + gtkWindowClose func(pointer) + gtkWindowFullScreen func(pointer) + gtkWindowGetPosition func(pointer, *int, *int) bool + gtkWindowGetSize func(pointer, *int, *int) + gtkWindowKeepAbove func(pointer, bool) + gtkWindowMaximize func(pointer) + gtkWindowMinimize func(pointer) + gtkWindowPresent func(pointer) + gtkWindowResize func(pointer, int, int) + gtkWindowSetDecorated func(pointer, int) + gtkWindowSetGeometryHints func(pointer, pointer, pointer, int) + gtkWindowSetKeepAbove func(pointer, bool) + gtkWindowSetResizable func(pointer, bool) + gtkWindowSetTitle func(pointer, string) + gtkWindowUnfullscreen func(pointer) + gtkWindowUnmaximize func(pointer) + + // webkit + webkitNewWithUserContentManager func(pointer) pointer + webkitRegisterUriScheme func(pointer, string, pointer, int, int) + webkitSettingsGetEnableDeveloperExtras func(pointer) bool + webkitSettingsSetHardwareAccelerationPolicy func(pointer, int) + webkitSettingsSetEnableDeveloperExtras func(pointer, bool) + webkitSettingsSetUserAgentWithApplicationDetails func(pointer, string, string) + webkitUserContentManagerNew func() pointer + webkitUserContentManagerRegisterScriptMessageHandler func(pointer, string) + webkitWebContextGetDefault func() pointer + webkitWebViewEvaluateJS func(pointer, string, int, pointer, string, pointer, pointer, pointer) + webkitWebViewGetSettings func(pointer) pointer + webkitWebViewGetZoom func(pointer) float64 + webkitWebViewLoadAlternateHTML func(pointer, string, string, *string) + webkitWebViewLoadUri func(pointer, string) + webkitWebViewSetBackgroundColor func(pointer, pointer) + webkitWebViewSetSettings func(pointer, pointer) +) + +func init() { + // needed for GTK4 to function + _ = os.Setenv("GDK_BACKEND", "x11") + var err error + + /* + gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err == nil { + version = 4 + return + } + + log.Println("Failed to open GTK4: Falling back to GTK3") + */ + gtk, err = purego.Dlopen(gtk3, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + version = 3 + + var webkit4 string = "libwebkit2gtk-4.1.so" + webkit, err = purego.Dlopen(webkit4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + + // Function registration + // GLib + purego.RegisterLibFunc(&gApplicationHold, gtk, "g_application_hold") + purego.RegisterLibFunc(&gApplicationName, gtk, "g_get_application_name") + purego.RegisterLibFunc(&gApplicationQuit, gtk, "g_application_quit") + purego.RegisterLibFunc(&gApplicationRelease, gtk, "g_application_release") + purego.RegisterLibFunc(&gApplicationRun, gtk, "g_application_run") + purego.RegisterLibFunc(&gIdleAdd, gtk, "g_idle_add") + purego.RegisterLibFunc(&gObjectRefSink, gtk, "g_object_ref_sink") + purego.RegisterLibFunc(&gObjectUnref, gtk, "g_object_unref") + purego.RegisterLibFunc(&gSignalConnectData, gtk, "g_signal_connect_data") + purego.RegisterLibFunc(&gSignalConnectObject, gtk, "g_signal_connect_object") + purego.RegisterLibFunc(&gSignalHandlerBlock, gtk, "g_signal_handler_block") + purego.RegisterLibFunc(&gSignalHandlerUnblock, gtk, "g_signal_handler_unblock") + + // GDK + purego.RegisterLibFunc(&gdkDisplayGetMonitor, gtk, "gdk_display_get_monitor") + purego.RegisterLibFunc(&gdkDisplayGetMonitorAtWindow, gtk, "gdk_display_get_monitor_at_window") + purego.RegisterLibFunc(&gdkDisplayGetNMonitors, gtk, "gdk_display_get_n_monitors") + purego.RegisterLibFunc(&gdkMonitorGetGeometry, gtk, "gdk_monitor_get_geometry") + purego.RegisterLibFunc(&gdkMonitorGetScaleFactor, gtk, "gdk_monitor_get_scale_factor") + purego.RegisterLibFunc(&gdkMonitorIsPrimary, gtk, "gdk_monitor_is_primary") + purego.RegisterLibFunc(&gdkRgbaParse, gtk, "gdk_rgba_parse") + purego.RegisterLibFunc(&gdkScreenGetRgbaVisual, gtk, "gdk_screen_get_rgba_visual") + purego.RegisterLibFunc(&gdkScreenIsComposited, gtk, "gdk_screen_is_composited") + purego.RegisterLibFunc(&gdkWindowGetDisplay, gtk, "gdk_window_get_display") + purego.RegisterLibFunc(&gdkWindowGetState, gtk, "gdk_window_get_state") + + // GTK3 + purego.RegisterLibFunc(>kApplicationNew, gtk, "gtk_application_new") + purego.RegisterLibFunc(>kApplicationGetActiveWindow, gtk, "gtk_application_get_active_window") + purego.RegisterLibFunc(>kApplicationGetWindows, gtk, "gtk_application_get_windows") + purego.RegisterLibFunc(>kApplicationWindowNew, gtk, "gtk_application_window_new") + purego.RegisterLibFunc(>kBoxNew, gtk, "gtk_box_new") + purego.RegisterLibFunc(>kBoxPackStart, gtk, "gtk_box_pack_start") + purego.RegisterLibFunc(>kCheckMenuItemGetActive, gtk, "gtk_check_menu_item_get_active") + purego.RegisterLibFunc(>kCheckMenuItemNewWithLabel, gtk, "gtk_check_menu_item_new_with_label") + purego.RegisterLibFunc(>kCheckMenuItemSetActive, gtk, "gtk_check_menu_item_set_active") + purego.RegisterLibFunc(>kContainerAdd, gtk, "gtk_container_add") + purego.RegisterLibFunc(>kDialogAddButton, gtk, "gtk_dialog_add_button") + purego.RegisterLibFunc(>kDialogRun, gtk, "gtk_dialog_run") + purego.RegisterLibFunc(>kDialogSetDefaultResponse, gtk, "gtk_dialog_set_default_response") + purego.RegisterLibFunc(>kDragDestSet, gtk, "gtk_drag_dest_set") + + purego.RegisterLibFunc(>kMenuItemSetLabel, gtk, "gtk_menu_item_set_label") + + purego.RegisterLibFunc(>kMenuBarNew, gtk, "gtk_menu_bar_new") + purego.RegisterLibFunc(>kMenuItemNewWithLabel, gtk, "gtk_menu_item_new_with_label") + purego.RegisterLibFunc(>kMenuItemSetSubmenu, gtk, "gtk_menu_item_set_submenu") + purego.RegisterLibFunc(>kMenuNew, gtk, "gtk_menu_new") + purego.RegisterLibFunc(>kMenuShellAppend, gtk, "gtk_menu_shell_append") + purego.RegisterLibFunc(>kMessageDialogNew, gtk, "gtk_message_dialog_new") + purego.RegisterLibFunc(>kRadioMenuItemGetGroup, gtk, "gtk_radio_menu_item_get_group") + purego.RegisterLibFunc(>kRadioMenuItemNewWithLabel, gtk, "gtk_radio_menu_item_new_with_label") + purego.RegisterLibFunc(>kSeparatorMenuItemNew, gtk, "gtk_separator_menu_item_new") + purego.RegisterLibFunc(>kTargetEntryFree, gtk, "gtk_target_entry_free") + purego.RegisterLibFunc(>kTargetEntryNew, gtk, "gtk_target_entry_new") + purego.RegisterLibFunc(>kWidgetDestroy, gtk, "gtk_widget_destroy") + purego.RegisterLibFunc(>kWidgetGetDisplay, gtk, "gtk_widget_get_display") + purego.RegisterLibFunc(>kWidgetGetScreen, gtk, "gtk_widget_get_screen") + purego.RegisterLibFunc(>kWidgetGetWindow, gtk, "gtk_widget_get_window") + purego.RegisterLibFunc(>kWidgetHide, gtk, "gtk_widget_hide") + purego.RegisterLibFunc(>kWidgetIsVisible, gtk, "gtk_widget_is_visible") + purego.RegisterLibFunc(>kWidgetSetAppPaintable, gtk, "gtk_widget_set_app_paintable") + purego.RegisterLibFunc(>kWidgetSetSensitive, gtk, "gtk_widget_set_sensitive") + purego.RegisterLibFunc(>kWidgetSetToolTipText, gtk, "gtk_widget_set_tooltip_text") + purego.RegisterLibFunc(>kWidgetSetVisual, gtk, "gtk_widget_set_visual") + purego.RegisterLibFunc(>kWidgetShow, gtk, "gtk_widget_show") + purego.RegisterLibFunc(>kWidgetShowAll, gtk, "gtk_widget_show_all") + purego.RegisterLibFunc(>kWindowFullScreen, gtk, "gtk_window_fullscreen") + purego.RegisterLibFunc(>kWindowClose, gtk, "gtk_window_close") + purego.RegisterLibFunc(>kWindowGetPosition, gtk, "gtk_window_get_position") + purego.RegisterLibFunc(>kWindowGetSize, gtk, "gtk_window_get_size") + purego.RegisterLibFunc(>kWindowMaximize, gtk, "gtk_window_maximize") + purego.RegisterLibFunc(>kWindowPresent, gtk, "gtk_window_present") + //purego.RegisterLibFunc(>kWindowPresent, gtk, "gtk_window_unminimize") // gtk4 + purego.RegisterLibFunc(>kWindowMinimize, gtk, "gtk_window_iconify") // gtk3 + // purego.RegisterLibFunc(>kWindowMinimize, gtk, "gtk_window_minimize") // gtk4 + purego.RegisterLibFunc(>kWindowResize, gtk, "gtk_window_resize") + purego.RegisterLibFunc(>kWindowSetGeometryHints, gtk, "gtk_window_set_geometry_hints") + purego.RegisterLibFunc(>kWindowSetDecorated, gtk, "gtk_window_set_decorated") + purego.RegisterLibFunc(>kWindowKeepAbove, gtk, "gtk_window_set_keep_above") + purego.RegisterLibFunc(>kWindowSetResizable, gtk, "gtk_window_set_resizable") + purego.RegisterLibFunc(>kWindowSetTitle, gtk, "gtk_window_set_title") + purego.RegisterLibFunc(>kWindowUnfullscreen, gtk, "gtk_window_unfullscreen") + purego.RegisterLibFunc(>kWindowUnmaximize, gtk, "gtk_window_unmaximize") + + // webkit + purego.RegisterLibFunc(&webkitNewWithUserContentManager, webkit, "webkit_web_view_new_with_user_content_manager") + purego.RegisterLibFunc(&webkitRegisterUriScheme, webkit, "webkit_web_context_register_uri_scheme") + + purego.RegisterLibFunc(&webkitSettingsGetEnableDeveloperExtras, webkit, "webkit_settings_get_enable_developer_extras") + purego.RegisterLibFunc(&webkitSettingsSetEnableDeveloperExtras, webkit, "webkit_settings_set_enable_developer_extras") + purego.RegisterLibFunc(&webkitSettingsSetHardwareAccelerationPolicy, webkit, "webkit_settings_set_hardware_acceleration_policy") + purego.RegisterLibFunc(&webkitSettingsSetUserAgentWithApplicationDetails, webkit, "webkit_settings_set_user_agent_with_application_details") + purego.RegisterLibFunc(&webkitUserContentManagerNew, webkit, "webkit_user_content_manager_new") + purego.RegisterLibFunc(&webkitUserContentManagerRegisterScriptMessageHandler, webkit, "webkit_user_content_manager_register_script_message_handler") + purego.RegisterLibFunc(&webkitWebContextGetDefault, webkit, "webkit_web_context_get_default") + purego.RegisterLibFunc(&webkitWebViewEvaluateJS, webkit, "webkit_web_view_evaluate_javascript") + purego.RegisterLibFunc(&webkitWebViewGetSettings, webkit, "webkit_web_view_get_settings") + purego.RegisterLibFunc(&webkitWebViewGetZoom, webkit, "webkit_web_view_get_zoom_level") + purego.RegisterLibFunc(&webkitWebViewLoadAlternateHTML, webkit, "webkit_web_view_load_alternate_html") + purego.RegisterLibFunc(&webkitWebViewLoadUri, webkit, "webkit_web_view_load_uri") + purego.RegisterLibFunc(&webkitWebViewSetBackgroundColor, webkit, "webkit_web_view_set_background_color") + purego.RegisterLibFunc(&webkitWebViewSetSettings, webkit, "webkit_web_view_set_settings") +} + +// mainthread stuff +func dispatchOnMainThread(id uint) { + gIdleAdd(purego.NewCallback(func(pointer) int { + executeOnMainThread(id) + return GSourceRemove + })) +} + +// implementation below +func appName() string { + return gApplicationName() +} + +func appNew(name string) pointer { + GApplicationDefaultFlags := uint(0) + + name = strings.ToLower(name) + if name == "" { + name = "undefined" + } + identifier := fmt.Sprintf("org.wails.%s", strings.Replace(name, " ", "-", -1)) + + return pointer(gtkApplicationNew(identifier, GApplicationDefaultFlags)) +} + +func appRun(application pointer) error { + app := pointer(application) + activate := func() { + // TODO: Do we care? + fmt.Println("linux.activated!") + gApplicationHold(app) // allow running without a window + } + gSignalConnectData( + application, + "activate", + purego.NewCallback(activate), + app, + false, + 0) + + status := gApplicationRun(app, 0, nil) + gApplicationRelease(app) + gObjectUnref(app) + + var err error + if status != 0 { + err = fmt.Errorf("exit code: %d", status) + } + return err +} + +func appDestroy(application pointer) { + gApplicationQuit(pointer(application)) +} + +func getCurrentWindowID(application pointer, windows map[windowPointer]uint) uint { + // TODO: Add extra metadata to window and use it! + window := gtkApplicationGetActiveWindow(pointer(application)) + identifier, ok := windows[windowPointer(window)] + if ok { + return identifier + } + // FIXME: Should we panic here if not found? + return uint(1) +} + +type GList struct { + data pointer + next *GList + prev *GList +} + +func getWindows(application pointer) []pointer { + result := []pointer{} + windows := gtkApplicationGetWindows(pointer(application)) + // FIXME: Need to make a struct here to deal with response data + for { + result = append(result, pointer(windows.data)) + windows = windows.next + if windows == nil { + return result + } + } +} + +func hideAllWindows(application pointer) { + for _, window := range getWindows(application) { + gtkWidgetHide(window) + } +} + +func showAllWindows(application pointer) { + for _, window := range getWindows(application) { + gtkWidgetShowAll(window) + } +} + +// Menu +func menuAddSeparator(menu *Menu) { + gtkMenuShellAppend( + pointer((menu.impl).(*linuxMenu).native), + gtkSeparatorMenuItemNew()) +} + +func menuAppend(parent *Menu, menu *MenuItem) { + // TODO: override this with the GTK4 version if needed - possibly rename to imply it's an alias + gtkMenuShellAppend( + pointer((parent.impl).(*linuxMenu).native), + pointer((menu.impl).(*linuxMenuItem).native)) + + /* gtk4 + C.gtk_menu_item_set_submenu( + (*C.struct__GtkMenuItem)((menu.impl).(*linuxMenuItem).native), + (*C.struct__GtkWidget)((parent.impl).(*linuxMenu).native), + ) + */ +} + +func menuBarNew() pointer { + return pointer(gtkMenuBarNew()) +} + +func menuNew() pointer { + return pointer(gtkMenuNew()) +} + +func menuSetSubmenu(item *MenuItem, menu *Menu) { + // FIXME: How is this different than `menuAppend` above? + gtkMenuItemSetSubmenu( + pointer((item.impl).(*linuxMenuItem).native), + pointer((menu.impl).(*linuxMenu).native)) +} + +func menuGetRadioGroup(item *linuxMenuItem) *GSList { + return (*GSList)(gtkRadioMenuItemGetGroup(pointer(item.native))) +} + +func attachMenuHandler(item *MenuItem) { + handleClick := func() { + item := item + switch item.itemType { + case text, checkbox: + menuItemClicked <- item.id + case radio: + menuItem := (item.impl).(*linuxMenuItem) + if menuItem.isChecked() { + menuItemClicked <- item.id + } + } + } + + impl := (item.impl).(*linuxMenuItem) + widget := impl.native + flags := 0 + handlerId := gSignalConnectObject( + pointer(widget), + "activate", + pointer(purego.NewCallback(handleClick)), + pointer(widget), + flags) + impl.handlerId = uint(handlerId) +} + +// menuItem +func menuItemChecked(widget pointer) bool { + if gtkCheckMenuItemGetActive(widget) == 1 { + return true + } + return false +} + +func menuItemNew(label string) pointer { + return pointer(gtkMenuItemNewWithLabel(label)) +} + +func menuCheckItemNew(label string) pointer { + return pointer(gtkCheckMenuItemNewWithLabel(label)) +} + +func menuItemSetChecked(widget pointer, checked bool) { + value := 0 + if checked { + value = 1 + } + gtkCheckMenuItemSetActive(pointer(widget), value) +} + +func menuItemSetDisabled(widget pointer, disabled bool) { + value := 1 + if disabled { + value = 0 + } + gtkWidgetSetSensitive(widget, value) +} + +func menuItemSetLabel(widget pointer, label string) { + gtkMenuItemSetLabel( + pointer(widget), + label) +} + +func menuItemSetToolTip(widget pointer, tooltip string) { + gtkWidgetSetToolTipText( + pointer(widget), + tooltip) +} + +func menuItemSignalBlock(widget pointer, handlerId uint, block bool) { + if block { + gSignalHandlerBlock(widget, handlerId) + } else { + gSignalHandlerUnblock(widget, handlerId) + } +} + +func menuRadioItemNew(group GSListPointer, label string) pointer { + return pointer(gtkRadioMenuItemNewWithLabel(group, label)) +} + +// screen related + +func getScreenByIndex(display pointer, index int) *Screen { + monitor := gdkDisplayGetMonitor(display, index) + // TODO: Do we need to update Screen to contain current info? + // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) + + geometry := struct { + x int32 + y int32 + width int32 + height int32 + }{} + result := pointer(unsafe.Pointer(&geometry)) + gdkMonitorGetGeometry(monitor, result) + + primary := false + if gdkMonitorIsPrimary(monitor) == 1 { + primary = true + } + + return &Screen{ + IsPrimary: primary, + Scale: 1.0, + X: int(geometry.x), + Y: int(geometry.y), + Size: Size{ + Height: int(geometry.height), + Width: int(geometry.width), + }, + } +} + +func getScreens(app pointer) ([]*Screen, error) { + var screens []*Screen + window := gtkApplicationGetActiveWindow(app) + display := gdkWindowGetDisplay(window) + count := gdkDisplayGetNMonitors(display) + for i := 0; i < int(count); i++ { + screens = append(screens, getScreenByIndex(display, i)) + } + return screens, nil +} + +// widgets +func widgetSetVisible(widget pointer, hidden bool) { + if hidden { + gtkWidgetHide(widget) + } else { + gtkWidgetShow(widget) + } +} + +// window related functions +func windowClose(window pointer) { + gtkWindowClose(window) +} + +func windowEnableDND(id uint, webview pointer) { + targetentry := gtkTargetEntryNew("text/uri-list", 0, id) + defer gtkTargetEntryFree(targetentry) + + GtkDestDefaultDrop := uint(0) + GdkActionCopy := uint(0) //? + gtkDragDestSet(webview, GtkDestDefaultDrop, targetentry, 1, GdkActionCopy) + + // FIXME: enable and process + /* gSignalConnectData(webview, + "drag-data-received", + purego.NewCallback(onDragNDrop), + 0, + false, + 0)*/ +} + +func windowExecJS(webview pointer, js string) { + webkitWebViewEvaluateJS( + webview, + js, + len(js), + 0, + "", + 0, + 0, + 0) +} + +func windowDestroy(window pointer) { + // Should this truly 'destroy' ? + gtkWindowClose(window) +} + +func windowFullscreen(window pointer) { + gtkWindowFullScreen(window) +} + +func windowGetCurrentMonitor(window pointer) pointer { + // Get the monitor that the window is currently on + display := gtkWidgetGetDisplay(window) + window = gtkWidgetGetWindow(window) + if window == 0 { + return 0 + } + return gdkDisplayGetMonitorAtWindow(display, window) +} + +func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, height int, scale int) { + monitor := windowGetCurrentMonitor(window) + if monitor == 0 { + return -1, -1, -1, -1, 1 + } + + result := struct { + x int32 + y int32 + width int32 + height int32 + }{} + gdkMonitorGetGeometry(monitor, pointer(unsafe.Pointer(&result))) + return int(result.x), int(result.y), int(result.width), int(result.height), gdkMonitorGetScaleFactor(monitor) +} + +func windowGetSize(window pointer) (int, int) { + // TODO: dispatchOnMainThread? + var width, height int + gtkWindowGetSize(window, &width, &height) + return width, height +} + +func windowGetPosition(window pointer) (int, int) { + // TODO: dispatchOnMainThread? + var x, y int + gtkWindowGetPosition(window, &x, &y) + return x, y +} + +func windowHide(window pointer) { + gtkWidgetHide(window) +} + +func windowIsFullscreen(window pointer) bool { + gdkwindow := gtkWidgetGetWindow(window) + state := gdkWindowGetState(gdkwindow) + return state&GdkWindowStateFullscreen > 0 +} + +func windowIsMaximized(window pointer) bool { + gdkwindow := gtkWidgetGetWindow(window) + state := gdkWindowGetState(gdkwindow) + return state&GdkWindowStateMaximized > 0 && state&GdkWindowStateFullscreen == 0 +} + +func windowIsMinimized(window pointer) bool { + gdkwindow := gtkWidgetGetWindow(window) + state := gdkWindowGetState(gdkwindow) + return state&GdkWindowStateIconified > 0 +} + +func windowIsVisible(window pointer) bool { + // TODO: validate this works.. (used a `bool` in the registration) + return gtkWidgetIsVisible(window) +} + +func windowMaximize(window pointer) { + gtkWindowMaximize(window) +} + +func windowMinimize(window pointer) { + gtkWindowMinimize(window) +} + +func windowNew(application pointer, menu pointer, windowId uint, gpuPolicy int) (pointer, pointer) { + window := gtkApplicationWindowNew(application) + gObjectRefSink(window) + webview := windowNewWebview(windowId, gpuPolicy) + vbox := gtkBoxNew(GtkOrientationVertical, 0) + gtkContainerAdd(window, vbox) + + if menu != 0 { + gtkBoxPackStart(vbox, menu, 0, 0, 0) + } + gtkBoxPackStart(vbox, webview, 1, 1, 0) + return pointer(window), pointer(webview) +} + +func windowNewWebview(parentId uint, gpuPolicy int) pointer { + manager := webkitUserContentManagerNew() + webkitUserContentManagerRegisterScriptMessageHandler(manager, "external") + wv := webkitNewWithUserContentManager(manager) + if !registered { + webkitRegisterUriScheme( + webkitWebContextGetDefault(), + "wails", + pointer(purego.NewCallback(func(request uintptr) { + webviewRequests <- &webViewAssetRequest{ + Request: webview.NewRequest(request), + windowId: parentId, + windowName: globalApplication.getWindowForID(parentId).Name(), + } + })), + 0, + 0, + ) + registered = true + } + + settings := webkitWebViewGetSettings(wv) + webkitSettingsSetUserAgentWithApplicationDetails( + settings, + "wails.io", + "") + webkitSettingsSetHardwareAccelerationPolicy(settings, gpuPolicy) + webkitWebViewSetSettings(wv, settings) + return wv +} + +func windowPresent(window pointer) { + gtkWindowPresent(pointer(window)) +} + +func windowReload(webview pointer, address string) { + webkitWebViewLoadUri(pointer(webview), address) +} + +func windowResize(window pointer, width, height int) { + gtkWindowResize(window, width, height) +} + +func windowShow(window pointer) { + gtkWidgetShowAll(pointer(window)) +} + +func windowSetBackgroundColour(webview pointer, colour RGBA) { + // FIXME: Use a struct! + rgba := make([]byte, 4*8) // C.sizeof_GdkRGBA == 32 + rgbaPointer := pointer(unsafe.Pointer(&rgba[0])) + if !gdkRgbaParse( + rgbaPointer, + fmt.Sprintf("rgba(%v,%v,%v,%v)", + colour.Red, + colour.Green, + colour.Blue, + float32(colour.Alpha)/255.0, + )) { + return + } + webkitWebViewSetBackgroundColor(pointer(webview), rgbaPointer) +} + +func windowSetGeometryHints(window pointer, minWidth, minHeight, maxWidth, maxHeight int) { + size := GdkGeometry{ + minWidth: int32(minWidth), + minHeight: int32(minHeight), + maxWidth: int32(maxWidth), + maxHeight: int32(maxHeight), + } + gtkWindowSetGeometryHints( + pointer(window), + pointer(0), + pointer(unsafe.Pointer(&size)), + GdkHintMinSize|GdkHintMaxSize) +} + +func windowSetFrameless(window pointer, frameless bool) { + decorated := 1 + if frameless { + decorated = 0 + } + gtkWindowSetDecorated(pointer(window), decorated) + + // TODO: Deal with transparency for the titlebar if possible when !frameless + // Perhaps we just make it undecorated and add a menu bar inside? +} + +// TODO: confirm this is working properly +func windowSetHTML(webview pointer, html string) { + webkitWebViewLoadAlternateHTML(webview, html, "wails://", nil) +} + +func windowSetKeepAbove(window pointer, alwaysOnTop bool) { + gtkWindowKeepAbove(window, alwaysOnTop) +} + +func windowSetResizable(window pointer, resizable bool) { + // FIXME: Does this work? + gtkWindowSetResizable( + pointer(window), + resizable, + ) +} + +func windowSetTitle(window pointer, title string) { + gtkWindowSetTitle(pointer(window), title) +} + +func windowSetTransparent(window pointer) { + screen := gtkWidgetGetScreen(pointer(window)) + visual := gdkScreenGetRgbaVisual(screen) + if visual == 0 { + return + } + if gdkScreenIsComposited(screen) == 1 { + gtkWidgetSetAppPaintable(pointer(window), 1) + gtkWidgetSetVisual(pointer(window), visual) + } +} + +func windowSetURL(webview pointer, uri string) { + var loadUri func(pointer, string) + purego.RegisterLibFunc(&loadUri, webkit, "webkit_web_view_load_uri") + loadUri(webview, uri) +} + +func windowSetupSignalHandlers(windowId uint, window, webview pointer, hideOnClose bool) { + // Window close handler + if hideOnClose { + handleDelete := purego.NewCallback(func(pointer) { + // FIXME: I think this should be the WebviewWindow + //w.close() + if !hideOnClose { + fmt.Println("Need to do more!") + } + }) + + gSignalConnectData(window, "delete-event", handleDelete, 0, false, 0) + + } else { + //FIXME: what event should be emitted? + // C.signal_connect((*C.GtkWidget)(window), event, C.close_button_pressed, w.parent.id) + } + + /* + event = C.CString("load-changed") + defer C.free(unsafe.Pointer(event)) + C.signal_connect(webview, event, C.webviewLoadChanged, unsafe.Pointer(&w.parent.id)) + */ + + // TODO: Handle mouse button / drag events + /* id := C.uint(windowId) + event = C.CString("button-press-event") + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, C.onButtonEvent, unsafe.Pointer(&id)) + C.free(unsafe.Pointer(event)) + event = C.CString("button-release-event") + defer C.free(unsafe.Pointer(event)) + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, onButtonEvent, unsafe.Pointer(&id)) + */ +} + +func windowToggleDevTools(webview pointer) { + settings := webkitWebViewGetSettings(pointer(webview)) + webkitSettingsSetEnableDeveloperExtras( + settings, + !webkitSettingsGetEnableDeveloperExtras(settings)) +} + +func windowUnfullscreen(window pointer) { + gtkWindowUnfullscreen(window) +} + +func windowUnmaximize(window pointer) { + gtkWindowUnmaximize(window) +} + +func windowZoom(webview pointer) float64 { + return webkitWebViewGetZoom(webview) +} + +// FIXME: ZoomIn/Out is assumed to be incorrect! +func windowZoomIn(webview pointer) { + ZoomInFactor := 1.10 + windowZoomSet(webview, windowZoom(webview)*ZoomInFactor) +} +func windowZoomOut(webview pointer) { + ZoomOutFactor := -1.10 + windowZoomSet(webview, windowZoom(webview)*ZoomOutFactor) +} + +func windowZoomSet(webview pointer, zoom float64) { + var setZoom func(pointer, float64) + purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") + setZoom(webview, zoom) +} + +func windowMove(window pointer, x, y int) { + var windowMove func(pointer, int, int) + purego.RegisterLibFunc(&windowMove, gtk, "gtk_window_move") + windowMove(window, x, y) +} + +/* +func onButtonEvent(_ pointer, event *C.GdkEventButton, data unsafe.Pointer) C.gboolean { + // Constants (defined here to be easier to use with ) + GdkButtonPress := C.GDK_BUTTON_PRESS // 4 + Gdk2ButtonPress := C.GDK_2BUTTON_PRESS // 5 for double-click + GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7 + + windowId := uint(*((*C.uint)(data))) + window := globalApplication.getWindowForID(windowId) + if window == nil { + return C.gboolean(0) + } + lw, ok := (window.impl).(*linuxWebviewWindow) + if !ok { + return C.gboolean(0) + } + + if event == nil { + return C.gboolean(0) + } + if event.button == 3 { + return C.gboolean(0) + } + + switch int(event._type) { + case GdkButtonPress: + lw.startDrag(uint(event.button), int(event.x_root), int(event.y_root)) + case Gdk2ButtonPress: + fmt.Printf("%d - button %d - double-clicked\n", windowId, int(event.button)) + case GdkButtonRelease: + lw.endDrag(uint(event.button), int(event.x_root), int(event.y_root)) + } + + return C.gboolean(0) +} + + +func onDragNDrop(target unsafe.Pointer, context *C.GdkDragContext, x C.gint, y C.gint, seldata unsafe.Pointer, info C.guint, time C.guint, data unsafe.Pointer) { + fmt.Println("target", target, info) + var length C.gint + selection := unsafe.Pointer(C.gtk_selection_data_get_data_with_length((*C.GtkSelectionData)(seldata), &length)) + extracted := C.g_uri_list_extract_uris((*C.char)(selection)) + defer C.g_strfreev(extracted) + + uris := unsafe.Slice( + (**C.char)(unsafe.Pointer(extracted)), + int(length)) + + var filenames []string + for _, uri := range uris { + if uri == nil { + break + } + filenames = append(filenames, strings.TrimPrefix(C.GoString(uri), "file://")) + } + windowDragAndDropBuffer <- &dragAndDropMessage{ + windowId: uint(*((*C.uint)(data))), + filenames: filenames, + } + C.gtk_drag_finish(context, C.true, C.false, time) +} + +func onProcessRequest(request unsafe.Pointer, data unsafe.Pointer) { + windowId := uint(*((*C.uint)(data))) + webviewRequests <- &webViewAssetRequest{ + Request: webview.NewRequest(request), + windowId: windowId, + windowName: globalApplication.getWindowForID(windowId).Name(), + } +} +*/ + +// dialog reloated +func runQuestionDialog(parent pointer, options *MessageDialog) int { + dType, ok := map[DialogType]int{ + InfoDialog: GtkMessageInfo, + WarningDialog: GtkMessageWarning, + QuestionDialog: GtkMessageQuestion, + }[options.DialogType] + if !ok { + // FIXME: Add logging here! + dType = GtkMessageInfo + } + buttonMask := GtkButtonsOk + if len(options.Buttons) > 0 { + buttonMask = GtkButtonsNone + } + + dialog := gtkMessageDialogNew( + pointer(parent), + GtkDialogModal|GtkDialogDestroyWithParent, + dType, + buttonMask, + options.Message) + + if options.Title != "" { + gtkWindowSetTitle(dialog, options.Title) + } + + /* + if img, err := pngToImage(options.Icon); err == nil { + gbytes := C.g_bytes_new_static( + C.gconstpointer(unsafe.Pointer(&img.Pix[0])), + C.ulong(len(img.Pix))) + defer C.g_bytes_unref(gbytes) + pixBuf := C.gdk_pixbuf_new_from_bytes( + gbytes, + C.GDK_COLORSPACE_RGB, + 1, // has_alpha + 8, + C.int(img.Bounds().Dx()), + C.int(img.Bounds().Dy()), + C.int(img.Stride), + ) + image := C.gtk_image_new_from_pixbuf(pixBuf) + C.gtk_widget_set_visible((*C.GtkWidget)(image), C.gboolean(1)) + contentArea := C.gtk_dialog_get_content_area((*C.GtkDialog)(dialog)) + C.gtk_container_add( + (*C.GtkContainer)(unsafe.Pointer(contentArea)), + (*C.GtkWidget)(image)) + }*/ + + for i, button := range options.Buttons { + gtkDialogAddButton( + dialog, + button.Label, + i, + ) + if button.IsDefault { + gtkDialogSetDefaultResponse(dialog, i) + } + } + defer gtkWidgetDestroy(dialog) + return gtkDialogRun(dialog) +} From e942312c4c73ea6064766ff6b4fa526dcf91d5c4 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 15 Jun 2023 14:05:26 -0500 Subject: [PATCH 06/17] [v3 menuitem] bug: avoid setting if nil --- v3/pkg/application/menuitem.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index e30750781..74ed4ada4 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -203,7 +203,9 @@ func (m *MenuItem) handleClick() { if m.itemType == radio { for _, member := range m.radioGroupMembers { member.checked = false - member.impl.setChecked(false) + if member.impl != nil { + member.impl.setChecked(false) + } } m.checked = true ctx.withChecked(true) From 33e20cbc77b821e33a1e91cba8a71ad0b0b31616 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 15 Jun 2023 14:05:52 -0500 Subject: [PATCH 07/17] [v3 linux] allow menu setting on Linux --- v3/pkg/application/application.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 4c8c3490d..80f01e5e5 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -2,7 +2,6 @@ package application import ( "encoding/json" - "github.com/wailsapp/wails/v3/internal/capabilities" "log" "net/http" "os" @@ -10,6 +9,8 @@ import ( "strconv" "sync" + "github.com/wailsapp/wails/v3/internal/capabilities" + "github.com/wailsapp/wails/v3/pkg/icons" "github.com/samber/lo" @@ -414,7 +415,7 @@ func (a *App) Run() error { a.runLock.Unlock() // set the application menu - if runtime.GOOS == "darwin" { + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { a.impl.setApplicationMenu(a.ApplicationMenu) } a.impl.setIcon(a.options.Icon) From c123430f22c8dc215383514df7221311ef4f9432 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 15 Jun 2023 14:33:50 -0500 Subject: [PATCH 08/17] [v3 linux] dialog fixes --- v3/pkg/application/dialogs_linux.go | 5 +---- v3/pkg/application/linux_purego.go | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go index d715ef909..560f07624 100644 --- a/v3/pkg/application/dialogs_linux.go +++ b/v3/pkg/application/dialogs_linux.go @@ -1,7 +1,5 @@ package application -import "fmt" - func (m *linuxApp) showAboutDialog(title string, message string, icon []byte) { window := globalApplication.getWindowForID(m.getCurrentWindowID()) var parent pointer @@ -31,8 +29,7 @@ func (m *linuxDialog) show() { } response := runQuestionDialog(parent, m.dialog) - if response >= 0 { - fmt.Println("Response: ", response) + if response >= 0 && response < len(m.dialog.Buttons) { button := m.dialog.Buttons[response] if button.Callback != nil { go button.Callback() diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index cc7d6b8b7..8c2eebd68 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -30,12 +30,12 @@ const ( GdkWindowStateFullscreen = 1 << 4 // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkmessagedialog.h#L87 - GtkButtonsNone int = iota - GtkButtonsOk - GtkButtonsClose - GtkButtonsCancel - GtkButtonsYesNo - GtkButtonsOkCancel + GtkButtonsNone int = 0 + GtkButtonsOk = 1 + GtkButtonsClose = 2 + GtkButtonsCancel = 3 + GtkButtonsYesNo = 4 + GtkButtonsOkCancel = 5 // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkdialog.h#L36 GtkDialogModal = 1 << 0 @@ -45,10 +45,10 @@ const ( GtkOrientationVertical = 1 // enum GtkMessageType - GtkMessageInfo = iota - GtkMessageWarning - GtkMessageQuestion - GtkMessageError + GtkMessageInfo = 0 + GtkMessageWarning = 1 + GtkMessageQuestion = 2 + GtkMessageError = 3 ) type GdkGeometry struct { From 83900b03d47fcbd6a0a0f5fb0075b8f3a830668e Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 15 Jun 2023 15:10:51 -0500 Subject: [PATCH 09/17] [v3 linux] purego cleanup + dialog images --- v3/pkg/application/linux_purego.go | 93 +++++++++++++++--------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 8c2eebd68..8f46d334d 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -73,8 +73,9 @@ var ( ) const ( - gtk3 = "libgtk-3.so" - gtk4 = "libgtk-4.so" + // TODO: map distro => so filename - with fallback? + gtk3 = "libgtk-3.so.0" + gtk4 = "libgtk-4.so.1" ) var ( @@ -83,12 +84,14 @@ var ( webkit uintptr // function references - gIdleAdd func(uintptr) gApplicationHold func(pointer) gApplicationQuit func(pointer) gApplicationName func() string gApplicationRelease func(pointer) gApplicationRun func(pointer, int, []string) int + gBytesNewStatic func(uintptr, int) uintptr + gBytesUnref func(uintptr) + gIdleAdd func(uintptr) gObjectRefSink func(pointer) gObjectUnref func(pointer) gSignalConnectData func(pointer, string, uintptr, pointer, bool, int) int @@ -103,6 +106,7 @@ var ( gdkMonitorGetGeometry func(pointer, pointer) pointer gdkMonitorGetScaleFactor func(pointer) int gdkMonitorIsPrimary func(pointer) int + gdkPixbufNewFromBytes func(uintptr, int, int, int, int, int, int) pointer gdkRgbaParse func(pointer, string) bool gdkScreenGetRgbaVisual func(pointer) pointer gdkScreenIsComposited func(pointer) int @@ -121,9 +125,11 @@ var ( gtkCheckMenuItemSetActive func(pointer, int) gtkContainerAdd func(pointer, pointer) gtkDialogAddButton func(pointer, string, int) + gtkDialogGetContentArea func(pointer) pointer gtkDialogRun func(pointer) int gtkDialogSetDefaultResponse func(pointer, int) gtkDragDestSet func(pointer, uint, pointer, uint, uint) + gtkImageNewFromPixbuf func(pointer) pointer gtkMenuBarNew func() pointer gtkMenuItemNewWithLabel func(string) pointer gtkMenuItemSetLabel func(pointer, string) @@ -155,6 +161,7 @@ var ( gtkWindowKeepAbove func(pointer, bool) gtkWindowMaximize func(pointer) gtkWindowMinimize func(pointer) + gtkWindowMove func(pointer, int, int) gtkWindowPresent func(pointer) gtkWindowResize func(pointer, int, int) gtkWindowSetDecorated func(pointer, int) @@ -182,6 +189,7 @@ var ( webkitWebViewLoadUri func(pointer, string) webkitWebViewSetBackgroundColor func(pointer, pointer) webkitWebViewSetSettings func(pointer, pointer) + webkitWebViewSetZoomLevel func(pointer, float64) ) func init() { @@ -189,15 +197,14 @@ func init() { _ = os.Setenv("GDK_BACKEND", "x11") var err error - /* - gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) - if err == nil { - version = 4 - return - } + // gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + // if err == nil { + // version = 4 + // return + // } + + // log.Println("Failed to open GTK4: Falling back to GTK3") - log.Println("Failed to open GTK4: Falling back to GTK3") - */ gtk, err = purego.Dlopen(gtk3, purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { panic(err) @@ -217,6 +224,8 @@ func init() { purego.RegisterLibFunc(&gApplicationQuit, gtk, "g_application_quit") purego.RegisterLibFunc(&gApplicationRelease, gtk, "g_application_release") purego.RegisterLibFunc(&gApplicationRun, gtk, "g_application_run") + purego.RegisterLibFunc(&gBytesNewStatic, gtk, "g_bytes_new_static") + purego.RegisterLibFunc(&gBytesUnref, gtk, "g_bytes_unref") purego.RegisterLibFunc(&gIdleAdd, gtk, "g_idle_add") purego.RegisterLibFunc(&gObjectRefSink, gtk, "g_object_ref_sink") purego.RegisterLibFunc(&gObjectUnref, gtk, "g_object_unref") @@ -232,6 +241,7 @@ func init() { purego.RegisterLibFunc(&gdkMonitorGetGeometry, gtk, "gdk_monitor_get_geometry") purego.RegisterLibFunc(&gdkMonitorGetScaleFactor, gtk, "gdk_monitor_get_scale_factor") purego.RegisterLibFunc(&gdkMonitorIsPrimary, gtk, "gdk_monitor_is_primary") + purego.RegisterLibFunc(&gdkPixbufNewFromBytes, gtk, "gdk_pixbuf_new_from_bytes") purego.RegisterLibFunc(&gdkRgbaParse, gtk, "gdk_rgba_parse") purego.RegisterLibFunc(&gdkScreenGetRgbaVisual, gtk, "gdk_screen_get_rgba_visual") purego.RegisterLibFunc(&gdkScreenIsComposited, gtk, "gdk_screen_is_composited") @@ -250,12 +260,12 @@ func init() { purego.RegisterLibFunc(>kCheckMenuItemSetActive, gtk, "gtk_check_menu_item_set_active") purego.RegisterLibFunc(>kContainerAdd, gtk, "gtk_container_add") purego.RegisterLibFunc(>kDialogAddButton, gtk, "gtk_dialog_add_button") + purego.RegisterLibFunc(>kDialogGetContentArea, gtk, "gtk_dialog_get_content_area") purego.RegisterLibFunc(>kDialogRun, gtk, "gtk_dialog_run") purego.RegisterLibFunc(>kDialogSetDefaultResponse, gtk, "gtk_dialog_set_default_response") purego.RegisterLibFunc(>kDragDestSet, gtk, "gtk_drag_dest_set") - + purego.RegisterLibFunc(>kImageNewFromPixbuf, gtk, "gtk_image_new_from_pixbuf") purego.RegisterLibFunc(>kMenuItemSetLabel, gtk, "gtk_menu_item_set_label") - purego.RegisterLibFunc(>kMenuBarNew, gtk, "gtk_menu_bar_new") purego.RegisterLibFunc(>kMenuItemNewWithLabel, gtk, "gtk_menu_item_new_with_label") purego.RegisterLibFunc(>kMenuItemSetSubmenu, gtk, "gtk_menu_item_set_submenu") @@ -284,6 +294,7 @@ func init() { purego.RegisterLibFunc(>kWindowGetPosition, gtk, "gtk_window_get_position") purego.RegisterLibFunc(>kWindowGetSize, gtk, "gtk_window_get_size") purego.RegisterLibFunc(>kWindowMaximize, gtk, "gtk_window_maximize") + purego.RegisterLibFunc(>kWindowMove, gtk, "gtk_window_move") purego.RegisterLibFunc(>kWindowPresent, gtk, "gtk_window_present") //purego.RegisterLibFunc(>kWindowPresent, gtk, "gtk_window_unminimize") // gtk4 purego.RegisterLibFunc(>kWindowMinimize, gtk, "gtk_window_iconify") // gtk3 @@ -300,7 +311,6 @@ func init() { // webkit purego.RegisterLibFunc(&webkitNewWithUserContentManager, webkit, "webkit_web_view_new_with_user_content_manager") purego.RegisterLibFunc(&webkitRegisterUriScheme, webkit, "webkit_web_context_register_uri_scheme") - purego.RegisterLibFunc(&webkitSettingsGetEnableDeveloperExtras, webkit, "webkit_settings_get_enable_developer_extras") purego.RegisterLibFunc(&webkitSettingsSetEnableDeveloperExtras, webkit, "webkit_settings_set_enable_developer_extras") purego.RegisterLibFunc(&webkitSettingsSetHardwareAccelerationPolicy, webkit, "webkit_settings_set_hardware_acceleration_policy") @@ -315,6 +325,7 @@ func init() { purego.RegisterLibFunc(&webkitWebViewLoadUri, webkit, "webkit_web_view_load_uri") purego.RegisterLibFunc(&webkitWebViewSetBackgroundColor, webkit, "webkit_web_view_set_background_color") purego.RegisterLibFunc(&webkitWebViewSetSettings, webkit, "webkit_web_view_set_settings") + purego.RegisterLibFunc(&webkitWebViewSetZoomLevel, webkit, "webkit_web_view_set_zoom_level") } // mainthread stuff @@ -844,9 +855,7 @@ func windowSetTransparent(window pointer) { } func windowSetURL(webview pointer, uri string) { - var loadUri func(pointer, string) - purego.RegisterLibFunc(&loadUri, webkit, "webkit_web_view_load_uri") - loadUri(webview, uri) + webkitWebViewLoadUri(webview, uri) } func windowSetupSignalHandlers(windowId uint, window, webview pointer, hideOnClose bool) { @@ -914,15 +923,11 @@ func windowZoomOut(webview pointer) { } func windowZoomSet(webview pointer, zoom float64) { - var setZoom func(pointer, float64) - purego.RegisterLibFunc(&setZoom, webkit, "webkit_web_view_set_zoom_level") - setZoom(webview, zoom) + webkitWebViewSetZoomLevel(webview, zoom) } func windowMove(window pointer, x, y int) { - var windowMove func(pointer, int, int) - purego.RegisterLibFunc(&windowMove, gtk, "gtk_window_move") - windowMove(window, x, y) + gtkWindowMove(window, x, y) } /* @@ -1024,28 +1029,26 @@ func runQuestionDialog(parent pointer, options *MessageDialog) int { gtkWindowSetTitle(dialog, options.Title) } - /* - if img, err := pngToImage(options.Icon); err == nil { - gbytes := C.g_bytes_new_static( - C.gconstpointer(unsafe.Pointer(&img.Pix[0])), - C.ulong(len(img.Pix))) - defer C.g_bytes_unref(gbytes) - pixBuf := C.gdk_pixbuf_new_from_bytes( - gbytes, - C.GDK_COLORSPACE_RGB, - 1, // has_alpha - 8, - C.int(img.Bounds().Dx()), - C.int(img.Bounds().Dy()), - C.int(img.Stride), - ) - image := C.gtk_image_new_from_pixbuf(pixBuf) - C.gtk_widget_set_visible((*C.GtkWidget)(image), C.gboolean(1)) - contentArea := C.gtk_dialog_get_content_area((*C.GtkDialog)(dialog)) - C.gtk_container_add( - (*C.GtkContainer)(unsafe.Pointer(contentArea)), - (*C.GtkWidget)(image)) - }*/ + GdkColorspaceRGB := 0 + + if img, err := pngToImage(options.Icon); err == nil { + gbytes := gBytesNewStatic(uintptr(unsafe.Pointer(&img.Pix[0])), len(img.Pix)) + + defer gBytesUnref(gbytes) + pixBuf := gdkPixbufNewFromBytes( + gbytes, + GdkColorspaceRGB, + 1, // has_alpha + 8, + img.Bounds().Dx(), + img.Bounds().Dy(), + img.Stride, + ) + image := gtkImageNewFromPixbuf(pixBuf) + widgetSetVisible(image, false) + contentArea := gtkDialogGetContentArea(dialog) + gtkContainerAdd(contentArea, image) + } for i, button := range options.Buttons { gtkDialogAddButton( From d52c26e82f906926284d86431a1f1c39eec135bb Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Wed, 21 Jun 2023 12:36:10 -0500 Subject: [PATCH 10/17] [v3 linux] implement missing functions --- v3/pkg/application/application_linux.go | 5 ++++ v3/pkg/application/linux_cgo.go | 29 +++++++++++++++------- v3/pkg/application/linux_purego.go | 29 +++++++--------------- v3/pkg/application/webview_window_linux.go | 13 +++++++--- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index 278ccdabd..c5287e8cb 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -95,6 +95,11 @@ func (m *linuxApp) destroy() { appDestroy(m.application) } +func (m *linuxApp) isOnMainThread() bool { + // FIXME: How do we detect this properly? + return false +} + // register our window to our parent mapping func (m *linuxApp) registerWindow(window pointer, id uint) { m.windowsLock.Lock() diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index cf7606f53..b2a35fcab 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -8,6 +8,7 @@ import ( "unsafe" "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + "github.com/wailsapp/wails/v3/pkg/events" ) /* @@ -42,8 +43,14 @@ static void dispatchOnMainThread(unsigned int id) { g_idle_add((GSourceFunc)dispatchCallback, (gpointer)args); } +typedef struct WindowEvent { + uint id; + uint event; +} WindowEvent; + // exported below void activateLinux(gpointer data); +extern void emit(WindowEvent* data); void handleClick(void*); extern gboolean onButtonEvent(GtkWidget *widget, GdkEventButton *event, gpointer user_data); extern void onDragNDrop( @@ -112,7 +119,6 @@ typedef struct Screen { static int GetNumScreens(){ return 0; } - */ import "C" @@ -669,17 +675,22 @@ func windowSetURL(webview pointer, uri string) { C.free(unsafe.Pointer(target)) } -func windowSetupSignalHandlers(windowId uint, window, webview pointer, hideOnClose bool) { +//export emit +func emit(we *C.WindowEvent) { + window := globalApplication.getWindowForID(uint(we.id)) + if window != nil { + window.emit(events.WindowEventType(we.event)) + } +} + +func windowSetupSignalHandlers(windowId uint, window, webview pointer, emit func(e events.WindowEventType)) { event := C.CString("delete-event") defer C.free(unsafe.Pointer(event)) - - // Window close handler - if hideOnClose { - C.signal_connect((*C.GtkWidget)(window), event, C.gtk_widget_hide_on_delete, C.NULL) - } else { - //FIXME: what event should be emitted? - // C.signal_connect((*C.GtkWidget)(window), event, C.close_button_pressed, w.parent.id) + wEvent := C.WindowEvent{ + id: C.uint(windowId), + event: C.uint(events.Common.WindowClosing), } + C.signal_connect((*C.GtkWidget)(window), event, C.emit, unsafe.Pointer(&wEvent)) /* event = C.CString("load-changed") diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 8f46d334d..49b05ecef 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -10,6 +10,7 @@ import ( "github.com/ebitengine/purego" "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + "github.com/wailsapp/wails/v3/pkg/events" ) type windowPointer uintptr @@ -74,8 +75,9 @@ var ( const ( // TODO: map distro => so filename - with fallback? - gtk3 = "libgtk-3.so.0" - gtk4 = "libgtk-4.so.1" + gtk3 = "libgtk-3.so.0" + gtk4 = "libgtk-4.so.1" + webkit4 = "libwebkit2gtk-4.1.so.0" ) var ( @@ -211,7 +213,6 @@ func init() { } version = 3 - var webkit4 string = "libwebkit2gtk-4.1.so" webkit, err = purego.Dlopen(webkit4, purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { panic(err) @@ -858,23 +859,11 @@ func windowSetURL(webview pointer, uri string) { webkitWebViewLoadUri(webview, uri) } -func windowSetupSignalHandlers(windowId uint, window, webview pointer, hideOnClose bool) { - // Window close handler - if hideOnClose { - handleDelete := purego.NewCallback(func(pointer) { - // FIXME: I think this should be the WebviewWindow - //w.close() - if !hideOnClose { - fmt.Println("Need to do more!") - } - }) - - gSignalConnectData(window, "delete-event", handleDelete, 0, false, 0) - - } else { - //FIXME: what event should be emitted? - // C.signal_connect((*C.GtkWidget)(window), event, C.close_button_pressed, w.parent.id) - } +func windowSetupSignalHandlers(windowId uint, window, webview pointer, emit func(e events.WindowEventType)) { + handleDelete := purego.NewCallback(func(pointer) { + emit(events.Common.WindowClosing) + }) + gSignalConnectData(window, "delete-event", handleDelete, 0, false, 0) /* event = C.CString("load-changed") diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index c1eb7b2ef..20b25ace3 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -53,7 +53,10 @@ func (w *linuxWebviewWindow) enableDND() { } func (w *linuxWebviewWindow) connectSignals() { - windowSetupSignalHandlers(w.parent.id, w.window, w.webview, w.parent.options.HideOnClose) + cb := func(e events.WindowEventType) { + w.parent.emit(e) + } + windowSetupSignalHandlers(w.parent.id, w.window, w.webview, cb) } func (w *linuxWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) { @@ -193,9 +196,6 @@ func (w *linuxWebviewWindow) windowZoom() { func (w *linuxWebviewWindow) close() { windowClose(w.window) - if !w.parent.options.HideOnClose { - globalApplication.deleteWindowByID(w.parent.id) - } } func (w *linuxWebviewWindow) zoomIn() { @@ -490,3 +490,8 @@ func (w *linuxWebviewWindow) startResize(border string) error { func (w *linuxWebviewWindow) nativeWindowHandle() uintptr { return uintptr(w.window) } + +func (w *linuxWebviewWindow) print() error { + w.execJS("window.print();") + return nil +} From e92858f64d2c1143192b14ca98bbdecd77c458b5 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Wed, 21 Jun 2023 16:31:26 -0500 Subject: [PATCH 11/17] [v3 linux/purego] initial file/directory dialogs impl --- v3/pkg/application/dialogs_linux.go | 68 +------ v3/pkg/application/linux_purego.go | 304 +++++++++++++++++----------- 2 files changed, 190 insertions(+), 182 deletions(-) diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go index 560f07624..83b764556 100644 --- a/v3/pkg/application/dialogs_linux.go +++ b/v3/pkg/application/dialogs_linux.go @@ -54,52 +54,7 @@ func newOpenFileDialogImpl(d *OpenFileDialog) *linuxOpenFileDialog { } func (m *linuxOpenFileDialog) show() ([]string, error) { - openFileResponses[m.dialog.id] = make(chan string) - // nsWindow := unsafe.Pointer(nil) - if m.dialog.window != nil { - // get NSWindow from window - //nsWindow = m.dialog.window.impl.(*macosWebviewWindow).nsWindow - } - - // Massage filter patterns into macOS format - // We iterate all filter patterns, tidy them up and then join them with a semicolon - // This should produce a single string of extensions like "png;jpg;gif" - // var filterPatterns string - // if len(m.dialog.filters) > 0 { - // var allPatterns []string - // for _, filter := range m.dialog.filters { - // patternComponents := strings.Split(filter.Pattern, ";") - // for i, component := range patternComponents { - // filterPattern := strings.TrimSpace(component) - // filterPattern = strings.TrimPrefix(filterPattern, "*.") - // patternComponents[i] = filterPattern - // } - // allPatterns = append(allPatterns, strings.Join(patternComponents, ";")) - // } - // filterPatterns = strings.Join(allPatterns, ";") - // } - - // C.showOpenFileDialog(C.uint(m.dialog.id), - // C.bool(m.dialog.canChooseFiles), - // C.bool(m.dialog.canChooseDirectories), - // C.bool(m.dialog.canCreateDirectories), - // C.bool(m.dialog.showHiddenFiles), - // C.bool(m.dialog.allowsMultipleSelection), - // C.bool(m.dialog.resolvesAliases), - // C.bool(m.dialog.hideExtension), - // C.bool(m.dialog.treatsFilePackagesAsDirectories), - // C.bool(m.dialog.allowsOtherFileTypes), - // toCString(filterPatterns), - // C.uint(len(filterPatterns)), - // toCString(m.dialog.message), - // toCString(m.dialog.directory), - // toCString(m.dialog.buttonText), - // nsWindow) - var result []string - for filename := range openFileResponses[m.dialog.id] { - result = append(result, filename) - } - return result, nil + return runOpenFileDialog(m.dialog) } type linuxSaveFileDialog struct { @@ -113,24 +68,5 @@ func newSaveFileDialogImpl(d *SaveFileDialog) *linuxSaveFileDialog { } func (m *linuxSaveFileDialog) show() (string, error) { - saveFileResponses[m.dialog.id] = make(chan string) - // nsWindow := unsafe.Pointer(nil) - if m.dialog.window != nil { - // get NSWindow from window - // nsWindow = m.dialog.window.impl.(*linuxWebviewWindow).nsWindow - } - - // C.showSaveFileDialog(C.uint(m.dialog.id), - // C.bool(m.dialog.canCreateDirectories), - // C.bool(m.dialog.showHiddenFiles), - // C.bool(m.dialog.canSelectHiddenExtension), - // C.bool(m.dialog.hideExtension), - // C.bool(m.dialog.treatsFilePackagesAsDirectories), - // C.bool(m.dialog.allowOtherFileTypes), - // toCString(m.dialog.message), - // toCString(m.dialog.directory), - // toCString(m.dialog.buttonText), - // toCString(m.dialog.filename), - // nsWindow) - return <-saveFileResponses[m.dialog.id], nil + return runSaveFileDialog(m.dialog) } diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 49b05ecef..84c696e92 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -16,7 +16,13 @@ import ( type windowPointer uintptr type identifier uint type pointer uintptr -type GSList uintptr + +// type GSList uintptr +type GSList struct { + data pointer + next *GSList +} + type GSListPointer *GSList const ( @@ -93,6 +99,7 @@ var ( gApplicationRun func(pointer, int, []string) int gBytesNewStatic func(uintptr, int) uintptr gBytesUnref func(uintptr) + gFree func(pointer) gIdleAdd func(uintptr) gObjectRefSink func(pointer) gObjectUnref func(pointer) @@ -116,63 +123,70 @@ var ( gdkWindowGetDisplay func(pointer) pointer // gtk functions - gtkApplicationNew func(string, uint) pointer - gtkApplicationGetActiveWindow func(pointer) pointer - gtkApplicationGetWindows func(pointer) *GList - gtkApplicationWindowNew func(pointer) pointer - gtkBoxNew func(int, int) pointer - gtkBoxPackStart func(pointer, pointer, int, int, int) - gtkCheckMenuItemGetActive func(pointer) int - gtkCheckMenuItemNewWithLabel func(string) pointer - gtkCheckMenuItemSetActive func(pointer, int) - gtkContainerAdd func(pointer, pointer) - gtkDialogAddButton func(pointer, string, int) - gtkDialogGetContentArea func(pointer) pointer - gtkDialogRun func(pointer) int - gtkDialogSetDefaultResponse func(pointer, int) - gtkDragDestSet func(pointer, uint, pointer, uint, uint) - gtkImageNewFromPixbuf func(pointer) pointer - gtkMenuBarNew func() pointer - gtkMenuItemNewWithLabel func(string) pointer - gtkMenuItemSetLabel func(pointer, string) - gtkMenuItemSetSubmenu func(pointer, pointer) - gtkMenuNew func() pointer - gtkMenuShellAppend func(pointer, pointer) - gtkMessageDialogNew func(pointer, int, int, int, string) pointer - gtkRadioMenuItemGetGroup func(pointer) GSListPointer - gtkRadioMenuItemNewWithLabel func(GSListPointer, string) pointer - gtkSeparatorMenuItemNew func() pointer - gtkTargetEntryFree func(pointer) - gtkTargetEntryNew func(string, int, uint) pointer - gtkWidgetDestroy func(pointer) - gtkWidgetGetDisplay func(pointer) pointer - gtkWidgetGetWindow func(pointer) pointer - gtkWidgetGetScreen func(pointer) pointer - gtkWidgetHide func(pointer) - gtkWidgetIsVisible func(pointer) bool - gtkWidgetShow func(pointer) - gtkWidgetShowAll func(pointer) - gtkWidgetSetAppPaintable func(pointer, int) - gtkWidgetSetSensitive func(pointer, int) - gtkWidgetSetToolTipText func(pointer, string) - gtkWidgetSetVisual func(pointer, pointer) - gtkWindowClose func(pointer) - gtkWindowFullScreen func(pointer) - gtkWindowGetPosition func(pointer, *int, *int) bool - gtkWindowGetSize func(pointer, *int, *int) - gtkWindowKeepAbove func(pointer, bool) - gtkWindowMaximize func(pointer) - gtkWindowMinimize func(pointer) - gtkWindowMove func(pointer, int, int) - gtkWindowPresent func(pointer) - gtkWindowResize func(pointer, int, int) - gtkWindowSetDecorated func(pointer, int) - gtkWindowSetGeometryHints func(pointer, pointer, pointer, int) - gtkWindowSetKeepAbove func(pointer, bool) - gtkWindowSetResizable func(pointer, bool) - gtkWindowSetTitle func(pointer, string) - gtkWindowUnfullscreen func(pointer) - gtkWindowUnmaximize func(pointer) + gtkApplicationNew func(string, uint) pointer + gtkApplicationGetActiveWindow func(pointer) pointer + gtkApplicationGetWindows func(pointer) *GList + gtkApplicationWindowNew func(pointer) pointer + gtkBoxNew func(int, int) pointer + gtkBoxPackStart func(pointer, pointer, int, int, int) + gtkCheckMenuItemGetActive func(pointer) int + gtkCheckMenuItemNewWithLabel func(string) pointer + gtkCheckMenuItemSetActive func(pointer, int) + gtkContainerAdd func(pointer, pointer) + gtkDialogAddButton func(pointer, string, int) + gtkDialogGetContentArea func(pointer) pointer + gtkDialogRun func(pointer) int + gtkDialogSetDefaultResponse func(pointer, int) + gtkDragDestSet func(pointer, uint, pointer, uint, uint) + gtkFileChooserDialogNew func(string, pointer, int, string, int, string, int, pointer) pointer + gtkFileChooserGetFilenames func(pointer) *GSList + gtkFileChooserSetCreateFolders func(pointer, int) + gtkFileChooserSetCurrentFolder func(pointer, string) + gtkFileChooserSetSelectMultiple func(pointer, int) + gtkFileChooserSetShowHidden func(pointer, int) + + gtkImageNewFromPixbuf func(pointer) pointer + gtkMenuBarNew func() pointer + gtkMenuItemNewWithLabel func(string) pointer + gtkMenuItemSetLabel func(pointer, string) + gtkMenuItemSetSubmenu func(pointer, pointer) + gtkMenuNew func() pointer + gtkMenuShellAppend func(pointer, pointer) + gtkMessageDialogNew func(pointer, int, int, int, string) pointer + gtkRadioMenuItemGetGroup func(pointer) GSListPointer + gtkRadioMenuItemNewWithLabel func(GSListPointer, string) pointer + gtkSeparatorMenuItemNew func() pointer + gtkTargetEntryFree func(pointer) + gtkTargetEntryNew func(string, int, uint) pointer + gtkWidgetDestroy func(pointer) + gtkWidgetGetDisplay func(pointer) pointer + gtkWidgetGetWindow func(pointer) pointer + gtkWidgetGetScreen func(pointer) pointer + gtkWidgetHide func(pointer) + gtkWidgetIsVisible func(pointer) bool + gtkWidgetShow func(pointer) + gtkWidgetShowAll func(pointer) + gtkWidgetSetAppPaintable func(pointer, int) + gtkWidgetSetSensitive func(pointer, int) + gtkWidgetSetToolTipText func(pointer, string) + gtkWidgetSetVisual func(pointer, pointer) + gtkWindowClose func(pointer) + gtkWindowFullScreen func(pointer) + gtkWindowGetPosition func(pointer, *int, *int) bool + gtkWindowGetSize func(pointer, *int, *int) + gtkWindowKeepAbove func(pointer, bool) + gtkWindowMaximize func(pointer) + gtkWindowMinimize func(pointer) + gtkWindowMove func(pointer, int, int) + gtkWindowPresent func(pointer) + gtkWindowResize func(pointer, int, int) + gtkWindowSetDecorated func(pointer, int) + gtkWindowSetGeometryHints func(pointer, pointer, pointer, int) + gtkWindowSetKeepAbove func(pointer, bool) + gtkWindowSetResizable func(pointer, bool) + gtkWindowSetTitle func(pointer, string) + gtkWindowUnfullscreen func(pointer) + gtkWindowUnmaximize func(pointer) // webkit webkitNewWithUserContentManager func(pointer) pointer @@ -227,6 +241,7 @@ func init() { purego.RegisterLibFunc(&gApplicationRun, gtk, "g_application_run") purego.RegisterLibFunc(&gBytesNewStatic, gtk, "g_bytes_new_static") purego.RegisterLibFunc(&gBytesUnref, gtk, "g_bytes_unref") + purego.RegisterLibFunc(&gFree, gtk, "g_free") purego.RegisterLibFunc(&gIdleAdd, gtk, "g_idle_add") purego.RegisterLibFunc(&gObjectRefSink, gtk, "g_object_ref_sink") purego.RegisterLibFunc(&gObjectUnref, gtk, "g_object_unref") @@ -265,6 +280,12 @@ func init() { purego.RegisterLibFunc(>kDialogRun, gtk, "gtk_dialog_run") purego.RegisterLibFunc(>kDialogSetDefaultResponse, gtk, "gtk_dialog_set_default_response") purego.RegisterLibFunc(>kDragDestSet, gtk, "gtk_drag_dest_set") + purego.RegisterLibFunc(>kFileChooserDialogNew, gtk, "gtk_file_chooser_dialog_new") + purego.RegisterLibFunc(>kFileChooserGetFilenames, gtk, "gtk_file_chooser_get_filenames") + purego.RegisterLibFunc(>kFileChooserSetCreateFolders, gtk, "gtk_file_chooser_set_create_folders") + purego.RegisterLibFunc(>kFileChooserSetCurrentFolder, gtk, "gtk_file_chooser_set_current_folder") + purego.RegisterLibFunc(>kFileChooserSetSelectMultiple, gtk, "gtk_file_chooser_set_select_multiple") + purego.RegisterLibFunc(>kFileChooserSetShowHidden, gtk, "gtk_file_chooser_set_show_hidden") purego.RegisterLibFunc(>kImageNewFromPixbuf, gtk, "gtk_image_new_from_pixbuf") purego.RegisterLibFunc(>kMenuItemSetLabel, gtk, "gtk_menu_item_set_label") purego.RegisterLibFunc(>kMenuBarNew, gtk, "gtk_menu_bar_new") @@ -919,79 +940,103 @@ func windowMove(window pointer, x, y int) { gtkWindowMove(window, x, y) } -/* -func onButtonEvent(_ pointer, event *C.GdkEventButton, data unsafe.Pointer) C.gboolean { - // Constants (defined here to be easier to use with ) - GdkButtonPress := C.GDK_BUTTON_PRESS // 4 - Gdk2ButtonPress := C.GDK_2BUTTON_PRESS // 5 for double-click - GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7 +func runChooserDialog(window pointer, allowMultiple, createFolders, showHidden bool, currentFolder, title string, action int, acceptLabel string, filters []FileFilter) ([]string, error) { + GtkResponseCancel := 0 + GtkResponseAccept := 1 - windowId := uint(*((*C.uint)(data))) - window := globalApplication.getWindowForID(windowId) - if window == nil { - return C.gboolean(0) - } - lw, ok := (window.impl).(*linuxWebviewWindow) - if !ok { - return C.gboolean(0) + fc := gtkFileChooserDialogNew( + title, + window, + action, + "_Cancel", + GtkResponseCancel, + acceptLabel, + GtkResponseAccept, + 0) + + for _, filter := range filters { + // TODO: Process and add filters + // gtk_file_chooser_add_filter(fc, thisFilter) + fmt.Println("filter", filter) } - if event == nil { - return C.gboolean(0) - } - if event.button == 3 { - return C.gboolean(0) + if allowMultiple { + gtkFileChooserSetSelectMultiple(fc, 1) } - switch int(event._type) { - case GdkButtonPress: - lw.startDrag(uint(event.button), int(event.x_root), int(event.y_root)) - case Gdk2ButtonPress: - fmt.Printf("%d - button %d - double-clicked\n", windowId, int(event.button)) - case GdkButtonRelease: - lw.endDrag(uint(event.button), int(event.x_root), int(event.y_root)) + if createFolders { + gtkFileChooserSetCreateFolders(fc, 1) } - return C.gboolean(0) -} + if showHidden { + gtkFileChooserSetShowHidden(fc, 1) + } + if currentFolder != "" { + gtkFileChooserSetCurrentFolder(fc, currentFolder) + } -func onDragNDrop(target unsafe.Pointer, context *C.GdkDragContext, x C.gint, y C.gint, seldata unsafe.Pointer, info C.guint, time C.guint, data unsafe.Pointer) { - fmt.Println("target", target, info) - var length C.gint - selection := unsafe.Pointer(C.gtk_selection_data_get_data_with_length((*C.GtkSelectionData)(seldata), &length)) - extracted := C.g_uri_list_extract_uris((*C.char)(selection)) - defer C.g_strfreev(extracted) - - uris := unsafe.Slice( - (**C.char)(unsafe.Pointer(extracted)), - int(length)) - - var filenames []string - for _, uri := range uris { - if uri == nil { - break + buildStringAndFree := func(s pointer) string { + bytes := []byte{} + p := unsafe.Pointer(s) + for { + val := *(*byte)(p) + if val == 0 { // this is the null terminator + break + } + bytes = append(bytes, val) + p = unsafe.Add(p, 1) } - filenames = append(filenames, strings.TrimPrefix(C.GoString(uri), "file://")) + gFree(s) // so we don't have to iterate a second time + return string(bytes) } - windowDragAndDropBuffer <- &dragAndDropMessage{ - windowId: uint(*((*C.uint)(data))), - filenames: filenames, + + response := gtkDialogRun(fc) + selections := []string{} + if response == GtkResponseAccept { + filenames := gtkFileChooserGetFilenames(fc) + iter := filenames + count := 0 + for { + selection := buildStringAndFree(iter.data) + selections = append(selections, selection) + iter = iter.next + if iter == nil || count == 1024 { + break + } + count++ + } } - C.gtk_drag_finish(context, C.true, C.false, time) + defer gtkWidgetDestroy(fc) + return selections, nil } -func onProcessRequest(request unsafe.Pointer, data unsafe.Pointer) { - windowId := uint(*((*C.uint)(data))) - webviewRequests <- &webViewAssetRequest{ - Request: webview.NewRequest(request), - windowId: windowId, - windowName: globalApplication.getWindowForID(windowId).Name(), - } -} -*/ +// dialog related +func runOpenFileDialog(dialog *OpenFileDialog) ([]string, error) { + GtkFileChooserActionOpen := 0 + // GtkFileChooserActionSave := 1 + // GtkFileChooserActionSelectFolder := 2 + // GtkFileChooserActionCreateFolder := 3 + + // (dialog.window.impl).(*linuxWebviewWindow).window // FIXME: dialog.window == nil! + window := pointer(0) + buttonText := dialog.buttonText + if buttonText == "" { + buttonText = "_Open" + } + + return runChooserDialog( + window, + dialog.allowsMultipleSelection, + dialog.canCreateDirectories, + dialog.showHiddenFiles, + dialog.directory, + dialog.title, + GtkFileChooserActionOpen, + buttonText, + dialog.filters) +} -// dialog reloated func runQuestionDialog(parent pointer, options *MessageDialog) int { dType, ok := map[DialogType]int{ InfoDialog: GtkMessageInfo, @@ -1052,3 +1097,30 @@ func runQuestionDialog(parent pointer, options *MessageDialog) int { defer gtkWidgetDestroy(dialog) return gtkDialogRun(dialog) } + +func runSaveFileDialog(dialog *SaveFileDialog) (string, error) { + GtkFileChooserActionSave := 1 + // GtkFileChooserActionSelectFolder := 2 + // GtkFileChooserActionCreateFolder := 3 + window := pointer(0) + buttonText := dialog.buttonText + if buttonText == "" { + buttonText = "_Save" + } + results, err := runChooserDialog( + window, + false, // multiple selection + dialog.canCreateDirectories, + dialog.showHiddenFiles, + dialog.directory, + dialog.title, + GtkFileChooserActionSave, + buttonText, + dialog.filters) + + if err != nil || len(results) == 0 { + return "", err + } + + return results[0], nil +} From daa0cf83d9860a89727772ca958d986e70073b4f Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Wed, 21 Jun 2023 16:35:24 -0500 Subject: [PATCH 12/17] [v3 linux] noop: remove fmt.Println --- v3/pkg/application/linux_cgo.go | 1 - 1 file changed, 1 deletion(-) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index b2a35fcab..dc344e237 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -786,7 +786,6 @@ func onButtonEvent(_ *C.GtkWidget, event *C.GdkEventButton, data unsafe.Pointer) //export onDragNDrop func onDragNDrop(target unsafe.Pointer, context *C.GdkDragContext, x C.gint, y C.gint, seldata unsafe.Pointer, info C.guint, time C.guint, data unsafe.Pointer) { - fmt.Println("target", target, info) var length C.gint selection := unsafe.Pointer(C.gtk_selection_data_get_data_with_length((*C.GtkSelectionData)(seldata), &length)) extracted := C.g_uri_list_extract_uris((*C.char)(selection)) From 1a09a8a4c84d646082dac06420f17c326f58d22b Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Wed, 21 Jun 2023 16:42:57 -0500 Subject: [PATCH 13/17] [v3 linux/cgo] open/save file dialog stub --- v3/pkg/application/linux_cgo.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index dc344e237..955dea05d 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -858,6 +858,10 @@ func messageDialogCB(button C.int) { } +func runOpenFileDialog(dialog *OpenFileDialog) ([]string, error) { + return []string{}, fmt.Errorf("not implemented") +} + func runQuestionDialog(parent pointer, options *MessageDialog) int { cMsg := C.CString(options.Message) cTitle := C.CString(options.Title) @@ -921,6 +925,10 @@ func runQuestionDialog(parent pointer, options *MessageDialog) int { return int(C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dialog)))) } +func runSaveFileDialog(dialog *SaveFileDialog) (string, error) { + return "", fmt.Errorf("not implemented") +} + //export openFileDialogCallbackEnd func openFileDialogCallbackEnd(cid C.uint) { id := uint(cid) From 5a40f25d032abbfd50f370c5f0a85dcbc73712ca Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Fri, 23 Jun 2023 15:49:37 -0500 Subject: [PATCH 14/17] [v3 linux] webview setAbsolutePosition --- v3/pkg/application/webview_window_linux.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 20b25ace3..508ce7cd5 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -373,6 +373,23 @@ func (w *linuxWebviewWindow) height() int { return height } +func (w *linuxWebviewWindow) setAbsolutePosition(x int, y int) { + // Set the window's absolute position + windowMove(w.window, x, y) +} + +func (w *linuxWebviewWindow) absolutePosition() (int, int) { + var x, y int + var wg sync.WaitGroup + wg.Add(1) + globalApplication.dispatchOnMainThread(func() { + x, y = windowGetAbsolutePosition(w.window) + wg.Done() + }) + wg.Wait() + return x, y +} + func (w *linuxWebviewWindow) run() { for eventId := range w.parent.eventListeners { w.on(eventId) From 67cada78f68dd89455502affe1549494bbc46239 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Fri, 23 Jun 2023 16:03:27 -0500 Subject: [PATCH 15/17] [v3 linux] purego: windowGetRelativePosition --- v3/pkg/application/linux_purego.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 84c696e92..ff2a64095 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -691,6 +691,17 @@ func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, h return int(result.x), int(result.y), int(result.width), int(result.height), gdkMonitorGetScaleFactor(monitor) } +func windowGetRelativePosition(window pointer) (int, int) { + absX, absY := windowGetAbsolutePosition(window) + x, y, _, _, _ := windowGetCurrentMonitorGeometry(window) + + relX := absX - x + relY := absY - y + + // TODO: Scale based on DPI + return relX, relY +} + func windowGetSize(window pointer) (int, int) { // TODO: dispatchOnMainThread? var width, height int From de5cff799effaf45ec17eac2d1b84aabcaebb41b Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Fri, 23 Jun 2023 21:44:02 -0500 Subject: [PATCH 16/17] [v3 linux] setEnabled --- v3/pkg/application/linux_cgo.go | 11 ++++++++++- v3/pkg/application/linux_purego.go | 14 ++++++++++++++ v3/pkg/application/webview_window_linux.go | 18 ++++++++++++------ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 955dea05d..5762c599f 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -412,6 +412,15 @@ func getScreens(app pointer) ([]*Screen, error) { } // widgets +func widgetSetSensitive(widget pointer, enabled bool) { + value := C.int(0) + if enabled { + value = C.int(1) + } + + C.gtk_widget_set_sensitive((*C.GtkWidget)(widget), value) +} + func widgetSetVisible(widget pointer, hidden bool) { if hidden { C.gtk_widget_hide((*C.GtkWidget)(widget)) @@ -488,7 +497,7 @@ func windowGetSize(window pointer) (int, int) { return int(windowWidth), int(windowHeight) } -func windowGetPosition(window pointer) (int, int) { +func windowGetRelativePosition(window pointer) (int, int) { var x C.int var y C.int C.gtk_window_get_position((*C.GtkWindow)(window), &x, &y) diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index ff2a64095..3a358bfcc 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -614,6 +614,14 @@ func getScreens(app pointer) ([]*Screen, error) { } // widgets +func widgetSetSensitive(widget pointer, enabled bool) { + value := 0 + if enabled { + value = 1 + } + gtkWidgetSetSensitive(widget, value) +} + func widgetSetVisible(widget pointer, hidden bool) { if hidden { gtkWidgetHide(widget) @@ -665,6 +673,12 @@ func windowFullscreen(window pointer) { gtkWindowFullScreen(window) } +func windowGetAbsolutePosition(window pointer) (int, int) { + var x, y int + gtkWindowGetPosition(window, &x, &y) + return x, y +} + func windowGetCurrentMonitor(window pointer) pointer { // Get the monitor that the window is currently on display := gtkWidgetGetDisplay(window) diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 508ce7cd5..e2fe95ed9 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -159,7 +159,7 @@ func (w *linuxWebviewWindow) fullscreen() { w.setMinMaxSize(0, 0, width*scale, height*scale) w.setSize(width*scale, height*scale) windowFullscreen(w.window) - w.setPosition(0, 0) + w.setRelativePosition(0, 0) }) } @@ -356,7 +356,7 @@ func (w *linuxWebviewWindow) size() (int, int) { return windowGetSize(w.window) } -func (w *linuxWebviewWindow) setPosition(x, y int) { +func (w *linuxWebviewWindow) setRelativePosition(x, y int) { mx, my, _, _, _ := windowGetCurrentMonitorGeometry(w.window) globalApplication.dispatchOnMainThread(func() { windowMove(w.window, x+mx, y+my) @@ -426,7 +426,7 @@ func (w *linuxWebviewWindow) run() { w.setFrameless(w.parent.options.Frameless) if w.parent.options.X != 0 || w.parent.options.Y != 0 { - w.setPosition(w.parent.options.X, w.parent.options.Y) + w.setRelativePosition(w.parent.options.X, w.parent.options.Y) } else { fmt.Println("attempting to set in the center") w.center() @@ -460,7 +460,7 @@ func (w *linuxWebviewWindow) run() { if !w.parent.options.Hidden { w.show() if w.parent.options.X != 0 || w.parent.options.Y != 0 { - w.setPosition(w.parent.options.X, w.parent.options.Y) + w.setRelativePosition(w.parent.options.X, w.parent.options.Y) } else { w.center() // needs to be queued until after GTK starts up! } @@ -479,12 +479,12 @@ func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { windowSetBackgroundColour(w.webview, colour) } -func (w *linuxWebviewWindow) position() (int, int) { +func (w *linuxWebviewWindow) relativePosition() (int, int) { var x, y int var wg sync.WaitGroup wg.Add(1) go globalApplication.dispatchOnMainThread(func() { - x, y = windowGetPosition(w.window) + x, y = windowGetRelativePosition(w.window) wg.Done() }) wg.Wait() @@ -495,6 +495,12 @@ func (w *linuxWebviewWindow) destroy() { windowDestroy(w.window) } +func (w *linuxWebviewWindow) setEnabled(enabled bool) { + globalApplication.dispatchOnMainThread(func() { + widgetSetSensitive(w.window, enabled) + }) +} + func (w *linuxWebviewWindow) setHTML(html string) { windowSetHTML(w.webview, html) } From 1a7d105917474198277d36ab6dc889288483a508 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Fri, 23 Jun 2023 21:49:47 -0500 Subject: [PATCH 17/17] [v3 linux] GetAbsolutePosition --- v3/pkg/application/linux_cgo.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 5762c599f..0a68d9262 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -490,6 +490,13 @@ func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, h return int(result.x), int(result.y), int(result.width), int(result.height), scale } +func windowGetAbsolutePosition(window pointer) (int, int) { + var x C.int + var y C.int + C.gtk_window_get_position((*C.GtkWindow)(window), &x, &y) + return int(x), int(y) +} + func windowGetSize(window pointer) (int, int) { var windowWidth C.int var windowHeight C.int @@ -498,10 +505,18 @@ func windowGetSize(window pointer) (int, int) { } func windowGetRelativePosition(window pointer) (int, int) { - var x C.int - var y C.int - C.gtk_window_get_position((*C.GtkWindow)(window), &x, &y) - return int(x), int(y) + x, y := windowGetAbsolutePosition(window) + // The position must be relative to the screen it is on + // We need to get the screen it is on + monitor := windowGetCurrentMonitor(window) + geometry := C.GdkRectangle{} + C.gdk_monitor_get_geometry(monitor, &geometry) + x = x - int(geometry.x) + y = y - int(geometry.y) + + // TODO: Scale based on DPI + + return x, y } func windowHide(window pointer) {