From 68cfd130d3235e087a4f9b24c645a670a125f47a Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 27 Apr 2023 14:38:50 -0500 Subject: [PATCH] [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 000000000..cf37d9e7e --- /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 000000000..cfbdf4df1 --- /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 000000000..85733bdf1 --- /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 000000000..670f01f9f --- /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 000000000..73a6edd98 --- /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 000000000..5938f9c20 --- /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 000000000..bf3d0c308 --- /dev/null +++ b/v3/pkg/application/options_linux.go @@ -0,0 +1,6 @@ +package application + +// LinuxWindow contains macOS specific options +type LinuxWindow struct { + ShowApplicationMenu bool +} diff --git a/v3/pkg/application/screen_linux.go b/v3/pkg/application/screen_linux.go new file mode 100644 index 000000000..585f7db75 --- /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 000000000..16008e630 --- /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 000000000..79c2e8749 --- /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) +}