From 769662d77acd65ca9161cd9eb8340c820fb03066 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:37:43 -0500 Subject: [PATCH 01/24] [v2] assetserver/webview purego implementation --- .../webview/request_linux_purego.go | 86 +++++++++ .../webview/responsewriter_linux_purego.go | 176 ++++++++++++++++++ .../assetserver/webview/webkit2_36+_purego.go | 94 ++++++++++ .../assetserver/webview/webkit2_40+_purego.go | 74 ++++++++ .../webview/webkit2_legacy_purego.go | 36 ++++ 5 files changed, 466 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 00000000..97036740 --- /dev/null +++ b/v2/pkg/assetserver/webview/request_linux_purego.go @@ -0,0 +1,86 @@ +//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 +} 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 00000000..f19cd4ca --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter_linux_purego.go @@ -0,0 +1,176 @@ +//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() error { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return nil + } + rw.finished = true + if rw.w != nil { + rw.w.Close() + } + return nil +} + +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 00000000..2386868c --- /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 00000000..1088be25 --- /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 00000000..2e88864c --- /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 93a4b823c803ee3710c91cbdfba6e62942c596bf Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:38:23 -0500 Subject: [PATCH 02/24] [linux/purego] initial port --- .../application/application_linux_purego.go | 226 ++++++ v3/pkg/application/dialogs_linux_purego.go | 220 ++++++ v3/pkg/application/mainthread_linux_purego.go | 30 + v3/pkg/application/menu_linux_purego.go | 168 ++++ v3/pkg/application/menuitem_linux_purego.go | 396 ++++++++++ v3/pkg/application/screen_linux_purego.go | 92 +++ .../webview_window_linux_purego.go | 725 ++++++++++++++++++ 7 files changed, 1857 insertions(+) create mode 100644 v3/pkg/application/application_linux_purego.go create mode 100644 v3/pkg/application/dialogs_linux_purego.go create mode 100644 v3/pkg/application/mainthread_linux_purego.go create mode 100644 v3/pkg/application/menu_linux_purego.go create mode 100644 v3/pkg/application/menuitem_linux_purego.go create mode 100644 v3/pkg/application/screen_linux_purego.go create mode 100644 v3/pkg/application/webview_window_linux_purego.go diff --git a/v3/pkg/application/application_linux_purego.go b/v3/pkg/application/application_linux_purego.go new file mode 100644 index 00000000..6dae3da2 --- /dev/null +++ b/v3/pkg/application/application_linux_purego.go @@ -0,0 +1,226 @@ +//go:build linux && purego + +package application + +import ( + "fmt" + "log" + "os" + "strings" + "time" + + "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 +} + +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 { + fmt.Println("getCurrentWindowID") + var getCurrentWindow func(uintptr) uintptr + purego.RegisterLibFunc(&getCurrentWindow, gtk, "gtk_application_get_active_window") + // window := getCurrentWindow(m.application) + // if window != 0 { + // webview := (*WebviewWindow)(window) + // return webview.id + // } + return uint(1) +} + +func (m *linuxApp) setApplicationMenu(menu *Menu) { + if menu == nil { + // Create a default menu + menu = defaultApplicationMenu() + } + + 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!") + }) + */ + 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 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), + } + 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/dialogs_linux_purego.go b/v3/pkg/application/dialogs_linux_purego.go new file mode 100644 index 00000000..757b4f3d --- /dev/null +++ b/v3/pkg/application/dialogs_linux_purego.go @@ -0,0 +1,220 @@ +//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_purego.go b/v3/pkg/application/mainthread_linux_purego.go new file mode 100644 index 00000000..fb69c96e --- /dev/null +++ b/v3/pkg/application/mainthread_linux_purego.go @@ -0,0 +1,30 @@ +//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_purego.go b/v3/pkg/application/menu_linux_purego.go new file mode 100644 index 00000000..36c72ca4 --- /dev/null +++ b/v3/pkg/application/menu_linux_purego.go @@ -0,0 +1,168 @@ +//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(), + } + } + 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 +} diff --git a/v3/pkg/application/menuitem_linux_purego.go b/v3/pkg/application/menuitem_linux_purego.go new file mode 100644 index 00000000..f5dca543 --- /dev/null +++ b/v3/pkg/application/menuitem_linux_purego.go @@ -0,0 +1,396 @@ +//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: + fmt.Println("text", item.label) + result.native = newWithLabel(item.label) + + case checkbox: + fmt.Println("cb", item.label) + 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: + fmt.Println("submenu", item.label) + 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.Minimize() + } + }) +} + +func newZoomMenuItem() *MenuItem { + return newMenuItem("Zoom"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Zoom() + } + }) +} diff --git a/v3/pkg/application/screen_linux_purego.go b/v3/pkg/application/screen_linux_purego.go new file mode 100644 index 00000000..6f7add61 --- /dev/null +++ b/v3/pkg/application/screen_linux_purego.go @@ -0,0 +1,92 @@ +//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/webview_window_linux_purego.go b/v3/pkg/application/webview_window_linux_purego.go new file mode 100644 index 00000000..c8e09935 --- /dev/null +++ b/v3/pkg/application/webview_window_linux_purego.go @@ -0,0 +1,725 @@ +//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) + }) + g_signal_connect(webview, "load-changed", loadChanged, 0, false, 0) + + 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) 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.setPosition(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() { + 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.setPosition(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: (globalApplication.impl).(*linuxApp).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) setPosition(x, y int) { + var windowMove func(uintptr, int, int) + purego.RegisterLibFunc(&windowMove, gtk, "gtk_window_move") + mx, my, _, _, _ := w.getCurrentMonitorGeometry() + fmt.Println("setPosition", 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 := (globalApplication.impl).(*linuxApp) + 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() + + } + w.center() + + 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.setPosition(w.parent.options.X, w.parent.options.Y) + } else { + fmt.Println("attempting to set in the center") + 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 == nil { + return + } + + 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) position() (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) + 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) { + fmt.Println("setHTML") + 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) + }) +} From 68cfd130d3235e087a4f9b24c645a670a125f47a Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:38:50 -0500 Subject: [PATCH 03/24] [linux/cgo] initial implementation --- v3/pkg/application/application_linux.go | 231 +++++++ v3/pkg/application/clipboard_linux.go | 33 + v3/pkg/application/dialogs_linux.go | 300 ++++++++ v3/pkg/application/mainthread_linux.go | 51 ++ v3/pkg/application/menu_linux.go | 191 ++++++ v3/pkg/application/menuitem_linux.go | 397 +++++++++++ v3/pkg/application/options_linux.go | 6 + v3/pkg/application/screen_linux.go | 94 +++ v3/pkg/application/systemtray_linux.go | 82 +++ v3/pkg/application/webview_window_linux.go | 756 +++++++++++++++++++++ 10 files changed, 2141 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/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 00000000..cf37d9e7 --- /dev/null +++ b/v3/pkg/application/application_linux.go @@ -0,0 +1,231 @@ +//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" + "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() // startupActions should contain actions to take after `activate` signal arrival +} + +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) { + /* // 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.gtk_application_get_active_window((*C.GtkApplication)(m.application)) + if window != nil { + // return uint(window.id) + fmt.Println("getCurrentWindowID", window) + } + return uint(1) +} + +func (m *linuxApp) afterActivation(fn func()) { + m.startupActions = append(m.startupActions, fn) +} + +func (m *linuxApp) setApplicationMenu(menu *Menu) { + if menu == nil { + // Create a default menu + menu = defaultApplicationMenu() + } + + 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? + }) + 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)) +} + +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), + } + 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) { + globalApplication.activate() + app := (globalApplication.impl).(*linuxApp) + app.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/clipboard_linux.go b/v3/pkg/application/clipboard_linux.go new file mode 100644 index 00000000..cfbdf4df --- /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 { + 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 new file mode 100644 index 00000000..85733bdf --- /dev/null +++ b/v3/pkg/application/dialogs_linux.go @@ -0,0 +1,300 @@ +//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/mainthread_linux.go b/v3/pkg/application/mainthread_linux.go new file mode 100644 index 00000000..670f01f9 --- /dev/null +++ b/v3/pkg/application/mainthread_linux.go @@ -0,0 +1,51 @@ +//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/menu_linux.go b/v3/pkg/application/menu_linux.go new file mode 100644 index 00000000..73a6edd9 --- /dev/null +++ b/v3/pkg/application/menu_linux.go @@ -0,0 +1,191 @@ +//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)) + } + default: + fmt.Println("handleClick", item.itemType, 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 +} diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go new file mode 100644 index 00000000..5938f9c2 --- /dev/null +++ b/v3/pkg/application/menuitem_linux.go @@ -0,0 +1,397 @@ +//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.Minimize() + } + }) +} + +func newZoomMenuItem() *MenuItem { + return newMenuItem("Zoom"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Zoom() + } + }) +} diff --git a/v3/pkg/application/options_linux.go b/v3/pkg/application/options_linux.go new file mode 100644 index 00000000..bf3d0c30 --- /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 00000000..585f7db7 --- /dev/null +++ b/v3/pkg/application/screen_linux.go @@ -0,0 +1,94 @@ +//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/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go new file mode 100644 index 00000000..16008e63 --- /dev/null +++ b/v3/pkg/application/systemtray_linux.go @@ -0,0 +1,82 @@ +//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) 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 00000000..79c2e874 --- /dev/null +++ b/v3/pkg/application/webview_window_linux.go @@ -0,0 +1,756 @@ +//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' +) + +//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) 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.setPosition(0, 0) + }) +} + +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: (globalApplication.impl).(*linuxApp).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) { + globalApplication.dispatchOnMainThread(func() { + 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) setPosition(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) run() { + for eventId := range w.parent.eventListeners { + w.on(eventId) + } + + app := (globalApplication.impl).(*linuxApp) + menu := app.applicationMenu + + globalApplication.dispatchOnMainThread(func() { + w.window = unsafe.Pointer(C.gtk_application_window_new((*C.GtkApplication)(w.application))) + 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.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) + 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.setPosition(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 == nil { + return + } + 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) position() (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) + 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) +} From a9d834d715f1fb44d7d475c00c36c95fad4663f4 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:39:31 -0500 Subject: [PATCH 04/24] [examples] update label on click --- v3/go.mod | 3 + v3/go.sum | 2 + v3/pkg/application/application.go | 13 +- v3/pkg/application/application_darwin.go | 1 + v3/pkg/application/messageprocessor_call.go | 7 +- v3/pkg/application/messageprocessor_dialog.go | 7 +- v3/pkg/application/webview_window.go | 127 ++++++++++++------ 7 files changed, 100 insertions(+), 60 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index edc91284..8ecb85c2 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v3 go 1.19 require ( + github.com/ebitengine/purego v0.3.2 github.com/go-task/task/v3 v3.20.0 github.com/google/go-cmp v0.5.9 github.com/jackmordaunt/icns/v2 v2.2.1 @@ -72,3 +73,5 @@ 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 8fa85049..d5dcbcab 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -11,6 +11,8 @@ 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/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 287d8af8..9aea1c9a 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -363,6 +363,13 @@ func (a *App) Run() error { } }() + a.impl.setApplicationMenu(a.ApplicationMenu) + + // set the application menu + if runtime.GOOS == "darwin" { + a.impl.setIcon(a.options.Icon) + } + // run windows for _, window := range a.windows { go window.run() @@ -373,12 +380,6 @@ func (a *App) Run() error { go systray.Run() } - // set the application menu - if runtime.GOOS == "darwin" { - a.impl.setApplicationMenu(a.ApplicationMenu) - a.impl.setIcon(a.options.Icon) - } - err := a.impl.run() if err != nil { return err diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index a5127c2e..79b7edf7 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -185,6 +185,7 @@ func (m *macosApp) run() error { C.setApplicationShouldTerminateAfterLastWindowClosed(C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed)) C.setActivationPolicy(C.int(m.parent.options.Mac.ActivationPolicy)) C.activateIgnoringOtherApps() + globalApplication.activate() }) // setup event listeners for eventID := range m.parent.applicationEventListeners { diff --git a/v3/pkg/application/messageprocessor_call.go b/v3/pkg/application/messageprocessor_call.go index ddbddd37..78635864 100644 --- a/v3/pkg/application/messageprocessor_call.go +++ b/v3/pkg/application/messageprocessor_call.go @@ -4,19 +4,16 @@ 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) - msg := "_wails.callErrorCallback('" + *callID + "', " + strconv.Quote(errorMsg) + ");" - window.ExecJS(msg) + window.CallError(callID, errorMsg) } func (m *MessageProcessor) callCallback(window *WebviewWindow, callID *string, result string, isJSON bool) { - msg := fmt.Sprintf("_wails.callCallback('%s', %s, %v);", *callID, strconv.Quote(result), isJSON) - window.ExecJS(msg) + window.CallResponse(callID, result) } 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 84f519a4..f7bfcdef 100644 --- a/v3/pkg/application/messageprocessor_dialog.go +++ b/v3/pkg/application/messageprocessor_dialog.go @@ -5,19 +5,16 @@ 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) - msg := "_wails.dialogErrorCallback('" + *dialogID + "', " + strconv.Quote(errorMsg) + ");" - window.ExecJS(msg) + window.DialogError(dialogID, errorMsg) } func (m *MessageProcessor) dialogCallback(window *WebviewWindow, dialogID *string, result string, isJSON bool) { - msg := fmt.Sprintf("_wails.dialogCallback('%s', %s, %v);", *dialogID, strconv.Quote(result), isJSON) - window.ExecJS(msg) + window.DialogResponse(dialogID, result) } func (m *MessageProcessor) processDialogMethod(method string, rw http.ResponseWriter, r *http.Request, window *WebviewWindow, params QueryParams) { diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 736dacc1..a612398e 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -1,12 +1,14 @@ package application import ( + "encoding/json" "errors" "fmt" - "github.com/samber/lo" "sync" "time" + "github.com/samber/lo" + "github.com/wailsapp/wails/v3/pkg/logger" "github.com/wailsapp/wails/v3/pkg/events" @@ -14,54 +16,57 @@ import ( type ( webviewWindowImpl interface { - setTitle(title string) - setSize(width, height int) - setAlwaysOnTop(alwaysOnTop bool) - setURL(url string) - setResizable(resizable bool) - setMinSize(width, height int) - setMaxSize(width, height int) - execJS(js string) - setBackgroundColour(color RGBA) - run() center() - size() (int, int) - width() int - height() int - position() (int, int) - destroy() - reload() - forceReload() - toggleDevTools() - zoomReset() - zoomIn() - zoomOut() - getZoom() float64 - setZoom(zoom float64) close() - zoom() - setHTML(html string) - setPosition(x int, y int) - on(eventID uint) - minimise() - unminimise() - maximise() - unmaximise() + destroy() + disableSizeConstraints() + execJS(js string) + focus() + forceReload() fullscreen() - unfullscreen() - isMinimised() bool - isMaximised() bool + getScreen() (*Screen, error) + getZoom() float64 + height() int + hide() + hide() isFullscreen() bool + isMaximised() bool + isMinimised() bool isNormal() bool isVisible() bool - setFullscreenButtonEnabled(enabled bool) - focus() - show() - hide() - getScreen() (*Screen, error) - setFrameless(bool) - openContextMenu(menu *Menu, data *ContextMenuData) + maximise() + minimise() nativeWindowHandle() uintptr + on(eventID uint) + openContextMenu(menu *Menu, data *ContextMenuData) + position() (int, int) + reload() + restore() // FIXME: not used + run() + setAlwaysOnTop(alwaysOnTop bool) + setBackgroundColour(color RGBA) + setFrameless(bool) + setFullscreenButtonEnabled(enabled bool) + setHTML(html string) + setMaxSize(width, height int) + setMinSize(width, height int) + setPosition(x int, y int) + setResizable(resizable bool) + setSize(width, height int) + setTitle(title string) + setURL(url string) + setZoom(zoom float64) + show() + size() (int, int) + toggleDevTools() + unfullscreen() + unmaximise() + unminimise() + width() int + zoom() + zoomIn() + zoomOut() + zoomReset() } ) @@ -131,6 +136,39 @@ 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.implLock.RLock() @@ -744,8 +782,9 @@ func (w *WebviewWindow) SetFrameless(frameless bool) *WebviewWindow { } func (w *WebviewWindow) dispatchWailsEvent(event *WailsEvent) { - msg := fmt.Sprintf("_wails.dispatchWailsEvent(%s);", event.ToJSON()) - w.ExecJS(msg) + if w.impl != nil { + w.impl.execJS(fmt.Sprintf("_wails.dispatchWailsEvent(%s);", event.ToJSON())) + } } func (w *WebviewWindow) info(message string, args ...any) { From b1e79411e78bef381442e64e1bd16d65ca160ade Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:39:31 -0500 Subject: [PATCH 05/24] [examples] update label on click --- v3/examples/menu/main.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/v3/examples/menu/main.go b/v3/examples/menu/main.go index 3ca15cf4..d0f78d0b 100644 --- a/v3/examples/menu/main.go +++ b/v3/examples/menu/main.go @@ -29,7 +29,13 @@ func main() { // Click callbacks myMenu.Add("Click Me!").OnClick(func(ctx *application.Context) { - ctx.ClickedMenuItem().SetLabel("Thanks mate!") + label := ctx.ClickedMenuItem().Label() + if label == "Click Me!" { + label = "Thanks mate!" + } else { + label = "Click Me!" + } + ctx.ClickedMenuItem().SetLabel(label) }) // You can control the current window from the menu From ac5d0e54f02961d33a90e18532d12a464083f512 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:43:26 -0500 Subject: [PATCH 06/24] [purego] assethandler updates --- .../webview/request_linux_purego.go | 10 +++++++++ .../webview/responsewriter_linux_purego.go | 5 ++--- v3/pkg/application/application.go | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/v2/pkg/assetserver/webview/request_linux_purego.go b/v2/pkg/assetserver/webview/request_linux_purego.go index 97036740..03e3a5ca 100644 --- a/v2/pkg/assetserver/webview/request_linux_purego.go +++ b/v2/pkg/assetserver/webview/request_linux_purego.go @@ -84,3 +84,13 @@ func (r *request) Response() ResponseWriter { 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 index f19cd4ca..8f5f1a57 100644 --- a/v2/pkg/assetserver/webview/responsewriter_linux_purego.go +++ b/v2/pkg/assetserver/webview/responsewriter_linux_purego.go @@ -121,19 +121,18 @@ func (rw *responseWriter) WriteHeader(code int) { } } -func (rw *responseWriter) Finish() error { +func (rw *responseWriter) Finish() { if !rw.wroteHeader { rw.WriteHeader(http.StatusNotImplemented) } if rw.finished { - return nil + return } rw.finished = true if rw.w != nil { rw.w.Close() } - return nil } func (rw *responseWriter) finishWithError(code int, err error) { diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 9aea1c9a..fefd5304 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -390,6 +390,27 @@ func (a *App) Run() error { return nil } +// activate is called by the implementation to notify it is ready to run +func (a *App) activate() { + a.info("Starting window(s)") + // run windows + for _, window := range a.windows { + go window.run() + } + + a.info("Starting systray(s)") + // run system trays + for _, systray := range a.systemTrays { + go systray.Run() + } + + // set the application menu + a.impl.setApplicationMenu(a.ApplicationMenu) + + // set the application Icon + a.impl.setIcon(a.options.Icon) +} + func (a *App) handleApplicationEvent(event uint) { a.applicationEventListenersLock.RLock() listeners, ok := a.applicationEventListeners[event] From 1ed95164dba884601e5aac2cfbddee110dde03d3 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 17:07:06 -0500 Subject: [PATCH 07/24] [purego] --- v3/pkg/application/webview_window_linux_purego.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/v3/pkg/application/webview_window_linux_purego.go b/v3/pkg/application/webview_window_linux_purego.go index c8e09935..c9f58198 100644 --- a/v3/pkg/application/webview_window_linux_purego.go +++ b/v3/pkg/application/webview_window_linux_purego.go @@ -83,9 +83,11 @@ func (w *linuxWebviewWindow) newWebview(gpuPolicy int) uintptr { purego.RegisterLibFunc(&g_signal_connect, gtk, "g_signal_connect_data") loadChanged := purego.NewCallback(func(window uintptr) { - fmt.Println("loadChanged", window) + //fmt.Println("loadChanged", window) }) - g_signal_connect(webview, "load-changed", loadChanged, 0, false, 0) + 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") @@ -343,6 +345,8 @@ func (w linuxWebviewWindow) getCurrentMonitorGeometry() (x int, y int, width int } 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 @@ -610,8 +614,6 @@ func (w *linuxWebviewWindow) run() { w.fullscreen() } - w.center() - if w.parent.options.URL != "" { w.setURL(w.parent.options.URL) } @@ -633,7 +635,6 @@ func (w *linuxWebviewWindow) run() { 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() } } From cb1859a710b3a6827583fd17d034664c11b6a397 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 17:07:45 -0500 Subject: [PATCH 08/24] [cgo] registerWindow --- v3/pkg/application/webview_window_linux.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 79c2e874..3a689a6b 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -625,6 +625,7 @@ func (w *linuxWebviewWindow) run() { 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() From f87ec9d40125c55f3131e5ba7cd1d031b001cba0 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 17:08:15 -0500 Subject: [PATCH 09/24] [purego] --- .../application/application_linux_purego.go | 49 ++++++++++++++----- v3/pkg/application/menu_linux_purego.go | 11 +++++ v3/pkg/application/menuitem_linux_purego.go | 3 -- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/v3/pkg/application/application_linux_purego.go b/v3/pkg/application/application_linux_purego.go index 6dae3da2..3d6aa9be 100644 --- a/v3/pkg/application/application_linux_purego.go +++ b/v3/pkg/application/application_linux_purego.go @@ -7,7 +7,7 @@ import ( "log" "os" "strings" - "time" + "sync" "github.com/ebitengine/purego" "github.com/wailsapp/wails/v2/pkg/assetserver/webview" @@ -55,6 +55,10 @@ type linuxApp struct { application uintptr applicationMenu uintptr parent *App + + // Native -> uint + windows map[uintptr]uint + windowsLock sync.Mutex } func (m *linuxApp) hide() { @@ -67,6 +71,7 @@ func (m *linuxApp) 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! } @@ -80,15 +85,19 @@ func (m *linuxApp) name() string { } func (m *linuxApp) getCurrentWindowID() uint { - fmt.Println("getCurrentWindowID") var getCurrentWindow func(uintptr) uintptr purego.RegisterLibFunc(&getCurrentWindow, gtk, "gtk_application_get_active_window") - // window := getCurrentWindow(m.application) - // if window != 0 { - // webview := (*WebviewWindow)(window) - // return webview.id - // } - return uint(1) + 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) { @@ -96,19 +105,21 @@ func (m *linuxApp) setApplicationMenu(menu *Menu) { // Create a default menu menu = defaultApplicationMenu() } - - menu.Update() - m.applicationMenu = (menu.impl).(*linuxMenu).native + 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() + // time.Sleep(50 * time.Millisecond) + // m.parent.activate() } func (m *linuxApp) run() error { @@ -119,6 +130,11 @@ func (m *linuxApp) run() error { 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) @@ -146,6 +162,12 @@ func (m *linuxApp) destroy() { 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 == "" { @@ -159,6 +181,7 @@ func newPlatformApp(parent *App) *linuxApp { appName: identifier, parent: parent, application: gtkNew(identifier, 0), + windows: map[uintptr]uint{}, } return app } diff --git a/v3/pkg/application/menu_linux_purego.go b/v3/pkg/application/menu_linux_purego.go index 36c72ca4..8f22a89c 100644 --- a/v3/pkg/application/menu_linux_purego.go +++ b/v3/pkg/application/menu_linux_purego.go @@ -166,3 +166,14 @@ func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { 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_purego.go b/v3/pkg/application/menuitem_linux_purego.go index f5dca543..9a8dae60 100644 --- a/v3/pkg/application/menuitem_linux_purego.go +++ b/v3/pkg/application/menuitem_linux_purego.go @@ -122,11 +122,9 @@ func newMenuItemImpl(item *MenuItem) *linuxMenuItem { switch item.itemType { case text: - fmt.Println("text", item.label) result.native = newWithLabel(item.label) case checkbox: - fmt.Println("cb", item.label) result.native = newCBWithLabel(item.label) result.setChecked(item.checked) if item.accelerator != nil { @@ -136,7 +134,6 @@ func newMenuItemImpl(item *MenuItem) *linuxMenuItem { panic("Shouldn't get here with a radio item") case submenu: - fmt.Println("submenu", item.label) result.native = newWithLabel(item.label) default: From 9453a6025405426a07ca44eb84923b209b85b67b Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 17:08:34 -0500 Subject: [PATCH 10/24] [linux] window registration + default menu --- v3/pkg/application/application_linux.go | 43 ++++++++++++++++++------- v3/pkg/application/menu_linux.go | 13 ++++++-- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index cf37d9e7..b2475584 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -48,6 +48,7 @@ import ( "log" "os" "strings" + "sync" "unsafe" "github.com/wailsapp/wails/v3/pkg/events" @@ -63,7 +64,11 @@ type linuxApp struct { applicationMenu unsafe.Pointer parent *App - startupActions []func() // startupActions should contain actions to take after `activate` signal arrival + startupActions []func() + + // Native -> uint + windows map[*C.GtkWindow]uint + windowsLock sync.Mutex } func (m *linuxApp) hide() { @@ -112,26 +117,30 @@ func (m *linuxApp) name() string { func (m *linuxApp) getCurrentWindowID() uint { // TODO: Add extra metadata to window - window := C.gtk_application_get_active_window((*C.GtkApplication)(m.application)) - if window != nil { - // return uint(window.id) - fmt.Println("getCurrentWindowID", 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) afterActivation(fn func()) { - m.startupActions = append(m.startupActions, fn) -} - 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 + menu.Update() + m.applicationMenu = (menu.impl).(*linuxMenu).native + }) } func (m *linuxApp) run() error { @@ -140,7 +149,9 @@ func (m *linuxApp) run() error { // 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) @@ -151,6 +162,13 @@ 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 == "" { @@ -161,6 +179,7 @@ func newPlatformApp(parent *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 @@ -176,7 +195,7 @@ func (m *linuxApp) executeStartupActions() { //export activateLinux func activateLinux(data unsafe.Pointer) { - globalApplication.activate() + // globalApplication.activate() app := (globalApplication.impl).(*linuxApp) app.executeStartupActions() } diff --git a/v3/pkg/application/menu_linux.go b/v3/pkg/application/menu_linux.go index 73a6edd9..e0c0118a 100644 --- a/v3/pkg/application/menu_linux.go +++ b/v3/pkg/application/menu_linux.go @@ -44,8 +44,6 @@ func handleClick(idPtr unsafe.Pointer) { if menuItem.isChecked() { processMenuItemClick(C.uint(item.id)) } - default: - fmt.Println("handleClick", item.itemType, item.id) } } @@ -189,3 +187,14 @@ func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { 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 +} From 718e89ad1e1c36a9e151fd61257751bdf5c69943 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 17:09:12 -0500 Subject: [PATCH 11/24] [core/menu] prevent nil impl crash --- 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 e3075078..74ed4ada 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 a485b188b387e5b37838baff0c8a84ba431a11c3 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 11:43:05 -0500 Subject: [PATCH 12/24] [linux/cgo] updates --- v3/pkg/application/menuitem_linux.go | 2 +- v3/pkg/application/webview_window_linux.go | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go index 5938f9c2..59b2bfaf 100644 --- a/v3/pkg/application/menuitem_linux.go +++ b/v3/pkg/application/menuitem_linux.go @@ -381,7 +381,7 @@ func newMinimizeMenuItem() *MenuItem { OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { - currentWindow.Minimize() + currentWindow.Minimise() } }) } diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 3a689a6b..86e0a0cf 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -304,6 +304,17 @@ 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") @@ -714,7 +725,7 @@ func (w *linuxWebviewWindow) setTransparent() { } } -func (w *linuxWebviewWindow) setBackgroundColour(colour *RGBA) { +func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { if colour == nil { return } @@ -755,3 +766,7 @@ func (w *linuxWebviewWindow) setHTML(html string) { uri, empty) } + +func (w *linuxWebviewWindow) nativeWindowHandle() uintptr { + return uintptr(w.window) +} From ba34e47c4b8bc9dfea881ba184be76bce07027a4 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 11:43:28 -0500 Subject: [PATCH 13/24] [linux/purego] updates --- v3/pkg/application/menuitem_linux_purego.go | 2 +- .../webview_window_linux_purego.go | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/v3/pkg/application/menuitem_linux_purego.go b/v3/pkg/application/menuitem_linux_purego.go index 9a8dae60..f6827326 100644 --- a/v3/pkg/application/menuitem_linux_purego.go +++ b/v3/pkg/application/menuitem_linux_purego.go @@ -377,7 +377,7 @@ func newMinimizeMenuItem() *MenuItem { OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { - currentWindow.Minimize() + currentWindow.Minimise() } }) } diff --git a/v3/pkg/application/webview_window_linux_purego.go b/v3/pkg/application/webview_window_linux_purego.go index c9f58198..5a6f7bea 100644 --- a/v3/pkg/application/webview_window_linux_purego.go +++ b/v3/pkg/application/webview_window_linux_purego.go @@ -664,11 +664,7 @@ func (w *linuxWebviewWindow) setTransparent() { } } -func (w *linuxWebviewWindow) setBackgroundColour(colour *RGBA) { - if colour == nil { - return - } - +func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { if colour.Alpha != 0 { w.setTransparent() } @@ -717,10 +713,23 @@ func (w *linuxWebviewWindow) destroy() { } func (w *linuxWebviewWindow) setHTML(html string) { - fmt.Println("setHTML") 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 be2d616190f3ab72c3094d35bc50160ee2bbb7f9 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Mon, 8 May 2023 10:56:20 -0500 Subject: [PATCH 14/24] [linux] getNativeApplication --- v3/pkg/application/application_linux.go | 4 ++++ v3/pkg/application/application_linux_purego.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index b2475584..ba18a443 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -71,6 +71,10 @@ type linuxApp struct { windowsLock sync.Mutex } +func getNativeApplication() *linuxApp { + return globalApplication.impl.(*linuxApp) +} + func (m *linuxApp) hide() { // C.hide() } diff --git a/v3/pkg/application/application_linux_purego.go b/v3/pkg/application/application_linux_purego.go index 3d6aa9be..6585ad59 100644 --- a/v3/pkg/application/application_linux_purego.go +++ b/v3/pkg/application/application_linux_purego.go @@ -61,6 +61,10 @@ type linuxApp struct { windowsLock sync.Mutex } +func getNativeApplication() *linuxApp { + return globalApplication.impl.(*linuxApp) +} + func (m *linuxApp) hide() { // C.hide() } From 754d0934bfdf1fe9137d492c9846e3767d51496d Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Mon, 8 May 2023 10:56:49 -0500 Subject: [PATCH 15/24] [darwin] systray basic darkmode implementation --- v3/pkg/application/systemtray_darwin.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index 1c675018..c079c897 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -134,6 +134,10 @@ 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 From 41aef27631af2fbfdaf3ea56ac864d99b26a469f Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Mon, 8 May 2023 11:02:50 -0500 Subject: [PATCH 16/24] [linux] use getNativeApplication --- v3/pkg/application/application_linux.go | 4 +--- v3/pkg/application/menu_linux_purego.go | 2 +- v3/pkg/application/webview_window_linux.go | 4 ++-- v3/pkg/application/webview_window_linux_purego.go | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index ba18a443..a82dcf9a 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -199,9 +199,7 @@ func (m *linuxApp) executeStartupActions() { //export activateLinux func activateLinux(data unsafe.Pointer) { - // globalApplication.activate() - app := (globalApplication.impl).(*linuxApp) - app.executeStartupActions() + getNativeApplication().executeStartupActions() } //export processApplicationEvent diff --git a/v3/pkg/application/menu_linux_purego.go b/v3/pkg/application/menu_linux_purego.go index 8f22a89c..601d09b5 100644 --- a/v3/pkg/application/menu_linux_purego.go +++ b/v3/pkg/application/menu_linux_purego.go @@ -117,7 +117,7 @@ func (m *linuxMenu) addSubMenuToItem(menu *Menu, item *MenuItem) { menu.impl = &linuxMenu{ menu: menu, native: newMenu(), - } +N } } var itemSetSubmenu func(uintptr, uintptr) purego.RegisterLibFunc(&itemSetSubmenu, gtk, "gtk_menu_item_set_submenu") diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 86e0a0cf..359ff138 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -527,7 +527,7 @@ func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { // (*C.struct__GtkWidget)(m.native) //var menubar *C.struct__GtkWidget return &linuxWebviewWindow{ - application: (globalApplication.impl).(*linuxApp).application, + application: getNativeApplication(), parent: parent, // menubar: menubar, } @@ -631,7 +631,7 @@ func (w *linuxWebviewWindow) run() { w.on(eventId) } - app := (globalApplication.impl).(*linuxApp) + app := getNativeApplication() menu := app.applicationMenu globalApplication.dispatchOnMainThread(func() { diff --git a/v3/pkg/application/webview_window_linux_purego.go b/v3/pkg/application/webview_window_linux_purego.go index 5a6f7bea..bbca7752 100644 --- a/v3/pkg/application/webview_window_linux_purego.go +++ b/v3/pkg/application/webview_window_linux_purego.go @@ -442,7 +442,7 @@ func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { return &linuxWebviewWindow{ - application: (globalApplication.impl).(*linuxApp).application, + application: getNativeApplication(), parent: parent, } } @@ -558,7 +558,7 @@ func (w *linuxWebviewWindow) run() { } globalApplication.dispatchOnMainThread(func() { - app := (globalApplication.impl).(*linuxApp) + app := getNativeApplication() menu := app.applicationMenu var newWindow func(uintptr) uintptr purego.RegisterLibFunc(&newWindow, gtk, "gtk_application_window_new") From 5078e89a71afd9ff7ef086820e05621ca607ad0f Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 11:43:13 -0500 Subject: [PATCH 17/24] [darwin] implement isNormal --- v3/pkg/application/webview_window_darwin.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index ed39aa92..45b557ec 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -853,6 +853,10 @@ func (w *macosWebviewWindow) hide() { C.windowHide(w.nsWindow) } +func (w *macosWebviewWindow) isNormal() bool { + return !w.isMinimised() && !w.isMaximised() && !w.isFullscreen() +} + func (w *macosWebviewWindow) setFullscreenButtonEnabled(enabled bool) { C.setFullscreenButtonEnabled(w.nsWindow, C.bool(enabled)) } From 5129c0f4e2f6e1db7f0af9e7219485771fc5263f Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 13:12:15 -0500 Subject: [PATCH 18/24] [fixup] remove 'activate' --- v3/pkg/application/application.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index fefd5304..9aea1c9a 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -390,27 +390,6 @@ func (a *App) Run() error { return nil } -// activate is called by the implementation to notify it is ready to run -func (a *App) activate() { - a.info("Starting window(s)") - // run windows - for _, window := range a.windows { - go window.run() - } - - a.info("Starting systray(s)") - // run system trays - for _, systray := range a.systemTrays { - go systray.Run() - } - - // set the application menu - a.impl.setApplicationMenu(a.ApplicationMenu) - - // set the application Icon - a.impl.setIcon(a.options.Icon) -} - func (a *App) handleApplicationEvent(event uint) { a.applicationEventListenersLock.RLock() listeners, ok := a.applicationEventListeners[event] From 281f6335c1b81ef9580d971d09e128860da42e75 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 13:12:26 -0500 Subject: [PATCH 19/24] [linux] dispatchOnMainThread --- v3/pkg/application/webview_window_linux.go | 25 +++++++++------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 359ff138..53e11c96 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -326,26 +326,21 @@ func (w *linuxWebviewWindow) disableSizeConstraints() { } func (w *linuxWebviewWindow) unfullscreen() { - fmt.Println("unfullscreen") - globalApplication.dispatchOnMainThread(func() { - C.gtk_window_unfullscreen((*C.GtkWindow)(w.window)) - w.unmaximise() - }) + 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.setPosition(0, 0) - }) + 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.setPosition(0, 0) } func (w *linuxWebviewWindow) unminimise() { From 3784746ddb15a417196a60cd3965a460d450c978 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 16:02:22 -0500 Subject: [PATCH 20/24] [linux] STATUS.md --- v3/STATUS.md | 308 ++++++++++++++++++++++++++------------------------- 1 file changed, 155 insertions(+), 153 deletions(-) diff --git a/v3/STATUS.md b/v3/STATUS.md index 3f1e3678..eee2f79e 100644 --- a/v3/STATUS.md +++ b/v3/STATUS.md @@ -6,74 +6,74 @@ Status of features in v3. Incomplete - please add as you see fit. Application interface methods -| Method | Windows | Linux | Mac | Notes | -|---------------------------------------------------------------|---------|-------|-----|-------| -| run() error | | | Y | | -| destroy() | | | Y | | -| setApplicationMenu(menu *Menu) | | | Y | | -| name() string | | | Y | | -| getCurrentWindowID() uint | | | 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 | | +| Method | Windows | Linux | Mac | Notes | +|---------------------------------------------------------------|---------|-------|-----|------------------------------| +| run() error | | Y | Y | | +| destroy() | | Y | Y | | +| setApplicationMenu(menu *Menu) | | | Y | | +| name() string | | | Y | | +| getCurrentWindowID() uint | | 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 | | ## Webview Window Webview Window Interface Methods -| Method | Windows | Linux | Mac | Notes | -|----------------------------------------------------|---------|-------|-----|------------------------------------------| -| center() | Y | | Y | | -| close() | | | Y | | -| destroy() | | | Y | | -| execJS(js string) | | | Y | | -| focus() | Y | | | | -| forceReload() | | | Y | | -| fullscreen() | Y | | Y | | -| getScreen() (*Screen, error) | | | Y | | -| getZoom() float64 | | | Y | | -| height() int | Y | | Y | | -| hide() | Y | | Y | | -| isFullscreen() bool | Y | | Y | | -| isMaximised() bool | Y | | Y | | -| isMinimised() bool | Y | | Y | | -| maximise() | Y | | Y | | -| minimise() | Y | | Y | | -| nativeWindowHandle() (uintptr, error) | Y | | | | +| Method | Windows | Linux | Mac | Notes | +|----------------------------------------------------|---------|-------|-----|------------------------------------------| +| center() | Y | Y | Y | | +| close() | | Y | Y | | +| destroy() | | Y | Y | | +| execJS(js string) | | Y | Y | | +| focus() | Y | Y | | | +| forceReload() | | Y | Y | | +| fullscreen() | Y | Y | Y | | +| getScreen() (*Screen, error) | | Y | Y | | +| getZoom() float64 | | Y | Y | | +| height() int | Y | Y | Y | | +| hide() | Y | Y | Y | | +| isFullscreen() bool | Y | Y | Y | | +| isMaximised() bool | Y | Y | Y | | +| isMinimised() bool | Y | Y | Y | | +| maximise() | Y | Y | Y | | +| minimise() | Y | Y | Y | | +| nativeWindowHandle() (uintptr, error) | Y | Y | | | | on(eventID uint) | | | Y | | | openContextMenu(menu *Menu, data *ContextMenuData) | | | Y | | -| position() (int, int) | Y | | Y | | -| reload() | | | Y | | -| run() | Y | | Y | | -| setAlwaysOnTop(alwaysOnTop bool) | Y | | Y | | -| setBackgroundColour(color RGBA) | Y | | Y | | -| setFrameless(bool) | | | Y | | -| setFullscreenButtonEnabled(enabled bool) | - | | Y | There is no fullscreen button in Windows | -| setHTML(html string) | | | Y | | -| setMaxSize(width, height int) | Y | | Y | | -| setMinSize(width, height int) | Y | | Y | | -| setPosition(x int, y int) | Y | | Y | | -| setResizable(resizable bool) | Y | | Y | | -| setSize(width, height int) | Y | | Y | | -| setTitle(title string) | Y | | Y | | -| setURL(url string) | | | Y | | -| setZoom(zoom float64) | | | Y | | -| show() | Y | | Y | | -| size() (int, int) | Y | | Y | | -| toggleDevTools() | | | Y | | -| unfullscreen() | Y | | Y | | -| unmaximise() | Y | | Y | | -| unminimise() | Y | | Y | | -| width() int | Y | | Y | | -| zoom() | | | Y | | -| zoomIn() | | | Y | | -| zoomOut() | | | Y | | -| zoomReset() | | | Y | | +| position() (int, int) | Y | Y | Y | | +| reload() | | Y | Y | | +| run() | Y | Y | Y | | +| setAlwaysOnTop(alwaysOnTop bool) | Y | Y | Y | | +| setBackgroundColour(color RGBA) | Y | Y | Y | | +| setFrameless(bool) | | Y | Y | | +| setFullscreenButtonEnabled(enabled bool) | - | Y | Y | There is no fullscreen button in Windows | +| setHTML(html string) | | Y | Y | | +| setMaxSize(width, height int) | Y | Y | Y | | +| setMinSize(width, height int) | Y | Y | Y | | +| setPosition(x int, y int) | Y | Y | Y | | +| setResizable(resizable bool) | Y | Y | Y | | +| setSize(width, height int) | Y | Y | Y | | +| setTitle(title string) | Y | Y | Y | | +| setURL(url string) | | Y | Y | | +| setZoom(zoom float64) | | Y | Y | | +| show() | Y | Y | Y | | +| size() (int, int) | Y | Y | Y | | +| toggleDevTools() | | Y | Y | | +| unfullscreen() | Y | Y | Y | | +| unmaximise() | Y | Y | Y | | +| unminimise() | Y | Y | Y | | +| width() int | Y | Y | Y | | +| zoom() | | Y | Y | | +| zoomIn() | | Y | Y | | +| zoomOut() | | Y | Y | | +| zoomReset() | | Y | Y | | ## Runtime @@ -81,7 +81,7 @@ Webview Window Interface Methods | Feature | Windows | Linux | Mac | Notes | |---------|---------|-------|-----|-------| -| Quit | | | Y | | +| Quit | | Y | Y | | | Hide | Y | | Y | | | Show | Y | | Y | | @@ -113,79 +113,80 @@ Webview Window Interface Methods | Feature | Windows | Linux | Mac | Notes | |------------|---------|-------|-----|-------| -| GetAll | Y | | Y | | -| GetPrimary | | | Y | | -| GetCurrent | | | Y | | +| GetAll | | Y | Y | | +| GetPrimary | | Y | Y | | +| GetCurrent | | Y | Y | | ### Window | Feature | Windows | Linux | Mac | Notes | |---------------------|---------|-------|-----|--------------------------------------------------------------------------------------| -| SetTitle | Y | | Y | | -| SetSize | Y | | Y | | -| Size | Y | | Y | | -| SetPosition | Y | | Y | | -| Position | Y | | Y | | -| Focus | Y | | | | -| FullScreen | Y | | Y | | -| UnFullscreen | Y | | Y | | -| Minimise | Y | | Y | | -| UnMinimise | Y | | Y | | -| Maximise | Y | | Y | | -| UnMaximise | Y | | Y | | -| Show | Y | | Y | | -| Hide | Y | | Y | | -| Center | Y | | Y | | -| SetBackgroundColour | Y | | Y | https://github.com/MicrosoftEdge/WebView2Feedback/issues/1621#issuecomment-938234294 | -| SetAlwaysOnTop | Y | | Y | | -| SetResizable | Y | | Y | | -| SetMinSize | Y | | Y | | -| SetMaxSize | Y | | Y | | -| Width | Y | | Y | | -| Height | Y | | Y | | -| ZoomIn | | | Y | Increase view scale | -| ZoomOut | | | Y | Decrease view scale | -| ZoomReset | | | Y | Reset view scale | -| GetZoom | | | Y | Get current view scale | -| SetZoom | | | Y | Set view scale | -| Screen | | | Y | Get screen for window | +| | | | | | +| SetTitle | Y | Y | Y | | +| SetSize | Y | Y | Y | | +| Size | Y | Y | Y | | +| SetPosition | Y | Y | Y | | +| Position | Y | Y | Y | | +| Focus | Y | Y | | | +| FullScreen | Y | Y | Y | | +| UnFullscreen | Y | Y | Y | | +| Minimise | Y | Y | Y | | +| UnMinimise | Y | Y | Y | | +| Maximise | Y | Y | Y | | +| UnMaximise | Y | Y | Y | | +| Show | Y | Y | Y | | +| Hide | Y | Y | Y | | +| Center | Y | Y | Y | | +| SetBackgroundColour | Y | Y | Y | https://github.com/MicrosoftEdge/WebView2Feedback/issues/1621#issuecomment-938234294 | +| SetAlwaysOnTop | Y | Y | Y | | +| SetResizable | Y | Y | Y | | +| SetMinSize | Y | Y | Y | | +| SetMaxSize | Y | Y | Y | | +| Width | Y | Y | Y | | +| Height | Y | Y | Y | | +| ZoomIn | | Y | Y | Increase view scale | +| ZoomOut | | Y | Y | Decrease view scale | +| ZoomReset | | Y | Y | Reset view scale | +| GetZoom | | Y | Y | Get current view scale | +| SetZoom | | Y | Y | Set view scale | +| Screen | | Y | Y | Get screen for window | ### Window Options 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 | | | | -| Height | Y | | | | -| AlwaysOnTop | Y | | | | -| URL | | | | | -| DisableResize | Y | | | | -| Frameless | | | | | -| MinWidth | Y | | | | -| MinHeight | Y | | | | -| MaxWidth | Y | | | | -| MaxHeight | Y | | | | -| StartState | Y | | | | -| Mac | - | - | | | -| BackgroundType | | | | Acrylic seems to work but the others don't | -| BackgroundColour | Y | | | | -| HTML | | | | | -| JS | | | | | -| CSS | | | | | -| X | | | | | -| Y | | | | | -| HideOnClose | | | | | -| FullscreenButtonEnabled | | | | | -| Hidden | Y | | | | -| EnableFraudulentWebsiteWarnings | | | | | -| Zoom | | | | | -| EnableDragAndDrop | | | | | -| Windows | Y | - | - | | -| Focused | Y | | | | +| Feature | Windows | Linux | Mac | Notes | +|---------------------------------|---------|-------|-----|---------------------------------------------------| +| Name | | | | | +| Title | Y | | | | +| Width | Y | Y | | | +| Height | Y | Y | | | +| AlwaysOnTop | Y | Y | | | +| URL | | | | | +| DisableResize | Y | Y | | | +| Frameless | | 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 | | | +| JS | | Y | | | +| CSS | | Y | | | +| X | | Y | | | +| Y | | Y | | | +| HideOnClose | | Y | | | +| FullscreenButtonEnabled | | ? | | [linux] How is this different from DisableResize? | +| Hidden | Y | | | | +| EnableFraudulentWebsiteWarnings | | | | | +| Zoom | | Y | | | +| EnableDragAndDrop | | Y | | | +| Windows | Y | - | - | | +| Focused | Y | | | | ### Log @@ -195,7 +196,7 @@ To log or not to log? System logger vs custom logger. | Event | Windows | Linux | Mac | Notes | |--------------------------|---------|-------|-----|-------| -| Default Application Menu | | | Y | | +| Default Application Menu | | Y | Y | | ## Tray Menus @@ -255,30 +256,31 @@ Built-in plugin support: | Plugin | Windows | Linux | Mac | Notes | |-----------------|---------|-------|-----|-------| | Browser | | | Y | | -| KV Store | | | Y | | -| Log | | | Y | | +| KV Store | | Y | Y | | +| Log | | Y | Y | | | Single Instance | | | Y | | -| SQLite | | | Y | | +| SQLite | | Y | Y | | | Start at login | | | Y | | | Server | | | | | ## Packaging -| | Windows | Linux | Mac | Notes | -|-----------------|---------|-------|-----|-------| -| Icon Generation | | | Y | | -| Icon Embedding | | | Y | | -| Info.plist | | | Y | | -| NSIS Installer | | | - | | -| Mac bundle | | | Y | | -| Windows exe | | | - | | +| | Windows | Linux | Mac | Notes | +|-----------------|---------|-------|-----|-------| +| Icon Generation | | | Y | | +| Icon Embedding | | | Y | | +| Info.plist | | | Y | | +| NSIS Installer | | | - | | +| Mac bundle | | | Y | | +| Windows exe | | | - | | ## Frameless Windows -| Feature | Windows | Linux | Mac | Notes | -|---------|---------|-------|-----|-------| -| Resize | | | | | -| Drag | | | | | +| Feature | Windows | Linux | Mac | Notes | +|---------|---------|-------|-----|-----------------------------------------------| +| Resize | | | | | +| Drag | | Y | | Linux - can always drag with `Alt`+left mouse | + ## Mac Specific @@ -300,20 +302,20 @@ Built-in plugin support: | DisableFramelessWindowDecorations | | | | WindowMask | nil | Makes the window the contents of the bitmap | - // Select the type of translucent backdrop. Requires Windows 11 22621 or later. - BackdropType BackdropType - // Disable the icon in the titlebar - DisableIcon bool - // Theme. Defaults to SystemDefault which will use whatever the system theme is. The application will follow system theme changes. - Theme Theme - // Custom colours for dark/light mode - CustomTheme *ThemeSettings + // Select the type of translucent backdrop. Requires Windows 11 22621 or later. + BackdropType BackdropType + // Disable the icon in the titlebar + DisableIcon bool + // Theme. Defaults to SystemDefault which will use whatever the system theme is. The application will follow system theme changes. + Theme Theme + // Custom colours for dark/light mode + CustomTheme *ThemeSettings - // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown. - // "Rounded Corners" are only available on Windows 11. - DisableFramelessWindowDecorations bool + // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown. + // "Rounded Corners" are only available on Windows 11. + DisableFramelessWindowDecorations bool - // WindowMask is used to set the window shape. Use a PNG with an alpha channel to create a custom shape. - WindowMask []byte + // WindowMask is used to set the window shape. Use a PNG with an alpha channel to create a custom shape. + WindowMask []byte ## Linux Specific From 9da5cea88d8d087c0a71405db9d190f79491ab01 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 17:33:25 -0500 Subject: [PATCH 21/24] [linux/cgo] bugfix --- v3/pkg/application/webview_window_linux.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 53e11c96..264e93ca 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -78,7 +78,7 @@ type linuxWebviewWindow struct { } var ( - registered bool = false // avoid 'already registered message' + registered bool = false // avoid 'already registered message' about 'wails://' ) //export buttonEvent @@ -572,13 +572,11 @@ func (w *linuxWebviewWindow) setMaxSize(width, height int) { } func (w *linuxWebviewWindow) setResizable(resizable bool) { - globalApplication.dispatchOnMainThread(func() { - if resizable { - C.gtk_window_set_resizable((*C.GtkWindow)(w.window), 1) - } else { - C.gtk_window_set_resizable((*C.GtkWindow)(w.window), 0) - } - }) + 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() { @@ -721,9 +719,6 @@ func (w *linuxWebviewWindow) setTransparent() { } func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { - if colour == nil { - return - } if colour.Alpha != 0 { w.setTransparent() } From 17647e2c2f550125480ede937ae73c690d830397 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 17:33:46 -0500 Subject: [PATCH 22/24] [linux/cgo] hide/show implementation --- v3/pkg/application/application_linux.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index a82dcf9a..1bb5a3be 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -76,11 +76,27 @@ func getNativeApplication() *linuxApp { } func (m *linuxApp) hide() { - // C.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() { - // C.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) { From 54ba6d47f49ce8afc5c434ccce54f7512c1810ac Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Tue, 2 May 2023 17:37:23 -0500 Subject: [PATCH 23/24] Revert "[linux] dispatchOnMainThread" This reverts commit 346517a185640aa40bc8710b815cf3e5ff7dc4a8. --- v3/pkg/application/webview_window_linux.go | 25 +++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 264e93ca..04d4b9f6 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -326,21 +326,26 @@ func (w *linuxWebviewWindow) disableSizeConstraints() { } func (w *linuxWebviewWindow) unfullscreen() { - C.gtk_window_unfullscreen((*C.GtkWindow)(w.window)) - w.unmaximise() + 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() - 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.setPosition(0, 0) + 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.setPosition(0, 0) + }) } func (w *linuxWebviewWindow) unminimise() { From 9da5df354ed8fc1cdf7c8074daeba080a70faf22 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Mon, 8 May 2023 12:08:54 -0500 Subject: [PATCH 24/24] make it compilable again --- v3/pkg/application/menuitem_linux.go | 10 ++++++++++ v3/pkg/application/menuitem_linux_purego.go | 10 ++++++++++ v3/pkg/application/systemtray_linux.go | 8 ++++++++ v3/pkg/application/webview_window_linux.go | 2 +- v3/pkg/application/webview_window_linux_purego.go | 2 +- 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go index 59b2bfaf..d67a0272 100644 --- a/v3/pkg/application/menuitem_linux.go +++ b/v3/pkg/application/menuitem_linux.go @@ -395,3 +395,13 @@ func newZoomMenuItem() *MenuItem { } }) } + +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 index f6827326..be51f09e 100644 --- a/v3/pkg/application/menuitem_linux_purego.go +++ b/v3/pkg/application/menuitem_linux_purego.go @@ -391,3 +391,13 @@ func newZoomMenuItem() *MenuItem { } }) } + +func newFullScreenMenuItem() *MenuItem { + return newMenuItem("Fullscreen"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Fullscreen() + } + }) +} diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index 16008e63..8cc74460 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -51,6 +51,14 @@ func (s *linuxSystemTray) setIcon(icon []byte) { }) } +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 diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 04d4b9f6..9834c9d1 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -527,7 +527,7 @@ func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { // (*C.struct__GtkWidget)(m.native) //var menubar *C.struct__GtkWidget return &linuxWebviewWindow{ - application: getNativeApplication(), + application: getNativeApplication().application, parent: parent, // menubar: menubar, } diff --git a/v3/pkg/application/webview_window_linux_purego.go b/v3/pkg/application/webview_window_linux_purego.go index bbca7752..11cc992c 100644 --- a/v3/pkg/application/webview_window_linux_purego.go +++ b/v3/pkg/application/webview_window_linux_purego.go @@ -442,7 +442,7 @@ func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { return &linuxWebviewWindow{ - application: getNativeApplication(), + application: getNativeApplication().application, parent: parent, } }