diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go new file mode 100644 index 00000000..278ccdab --- /dev/null +++ b/v3/pkg/application/application_linux.go @@ -0,0 +1,167 @@ +//go:build linux + +package application + +import ( + "fmt" + "log" + "os" + "strings" + "sync" + + "github.com/wailsapp/wails/v3/pkg/events" +) + +func init() { + // FIXME: This should be handled appropriately in the individual files most likely. + // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings + _ = os.Setenv("GDK_BACKEND", "x11") +} + +type linuxApp struct { + application pointer + applicationMenu pointer + parent *App + + startupActions []func() + + // Native -> uint + windows map[windowPointer]uint + windowsLock sync.Mutex +} + +func (m *linuxApp) GetFlags(options Options) map[string]any { + if options.Flags == nil { + options.Flags = make(map[string]any) + } + return options.Flags +} + +func getNativeApplication() *linuxApp { + return globalApplication.impl.(*linuxApp) +} + +func (m *linuxApp) hide() { + hideAllWindows(m.application) +} + +func (m *linuxApp) show() { + showAllWindows(m.application) +} + +func (m *linuxApp) on(eventID uint) { + // TODO: What do we need to do here? + log.Println("linuxApp.on()", eventID) +} + +func (m *linuxApp) setIcon(icon []byte) { + fmt.Println("linuxApp.setIcon", "not implemented") +} + +func (m *linuxApp) name() string { + return appName() +} + +func (m *linuxApp) getCurrentWindowID() uint { + return getCurrentWindowID(m.application, m.windows) +} + +func (m *linuxApp) setApplicationMenu(menu *Menu) { + if menu == nil { + // Create a default menu + menu = defaultApplicationMenu() + } + globalApplication.dispatchOnMainThread(func() { + fmt.Println("setApplicationMenu") + + menu.Update() + m.applicationMenu = (menu.impl).(*linuxMenu).native + }) +} + +func (m *linuxApp) run() error { + + // Add a hook to the ApplicationDidFinishLaunching event + // FIXME: add Wails specific events - i.e. Shouldn't platform specific ones be translated to Wails events? + m.parent.On(events.Mac.ApplicationDidFinishLaunching, func() { + // Do we need to do anything now? + fmt.Println("events.Mac.ApplicationDidFinishLaunching received!") + }) + + return appRun(m.application) +} + +func (m *linuxApp) destroy() { + appDestroy(m.application) +} + +// register our window to our parent mapping +func (m *linuxApp) registerWindow(window pointer, id uint) { + m.windowsLock.Lock() + m.windows[windowPointer(window)] = id + m.windowsLock.Unlock() +} + +func newPlatformApp(parent *App) *linuxApp { + name := strings.ToLower(strings.Replace(parent.options.Name, " ", "", -1)) + if name == "" { + name = "undefined" + } + app := &linuxApp{ + parent: parent, + application: appNew(name), + windows: map[windowPointer]uint{}, + } + return app +} + +/* +//export processApplicationEvent +func processApplicationEvent(eventID C.uint) { + // TODO: add translation to Wails events + // currently reusing Mac specific values + applicationEvents <- uint(eventID) +} + +//export processWindowEvent +func processWindowEvent(windowID C.uint, eventID C.uint) { + windowEvents <- &WindowEvent{ + WindowID: uint(windowID), + EventID: uint(eventID), + } +} + +//export processMessage +func processMessage(windowID C.uint, message *C.char) { + windowMessageBuffer <- &windowMessage{ + windowId: uint(windowID), + message: C.GoString(message), + } +} + +//export processDragItems +func processDragItems(windowID C.uint, arr **C.char, length C.int) { + var filenames []string + // Convert the C array to a Go slice + goSlice := (*[1 << 30]*C.char)(unsafe.Pointer(arr))[:length:length] + for _, str := range goSlice { + filenames = append(filenames, C.GoString(str)) + } + windowDragAndDropBuffer <- &dragAndDropMessage{ + windowId: uint(windowID), + filenames: filenames, + } +} + +//export processMenuItemClick +func processMenuItemClick(menuID identifier) { + menuItemClicked <- uint(menuID) +} + +func setIcon(icon []byte) { + if icon == nil { + return + } + //C.setApplicationIcon(unsafe.Pointer(&icon[0]), C.int(len(icon))) +} +*/ diff --git a/v3/pkg/application/clipboard_linux.go b/v3/pkg/application/clipboard_linux.go new file mode 100644 index 00000000..ef7b2560 --- /dev/null +++ b/v3/pkg/application/clipboard_linux.go @@ -0,0 +1,33 @@ +//go:build linux + +package application + +import ( + "sync" +) + +var clipboardLock sync.RWMutex + +type linuxClipboard struct{} + +func (m linuxClipboard) setText(text string) bool { + clipboardLock.Lock() + defer clipboardLock.Unlock() + // cText := C.CString(text) + // success := C.setClipboardText(cText) + // C.free(unsafe.Pointer(cText)) + success := false + return bool(success) +} + +func (m linuxClipboard) text() (string, bool) { + clipboardLock.RLock() + defer clipboardLock.RUnlock() + // clipboardText := C.getClipboardText() + // result := C.GoString(clipboardText) + return "", false +} + +func newClipboardImpl() *linuxClipboard { + return &linuxClipboard{} +} diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go new file mode 100644 index 00000000..d715ef90 --- /dev/null +++ b/v3/pkg/application/dialogs_linux.go @@ -0,0 +1,139 @@ +package application + +import "fmt" + +func (m *linuxApp) showAboutDialog(title string, message string, icon []byte) { + window := globalApplication.getWindowForID(m.getCurrentWindowID()) + var parent pointer + if window != nil { + parent = window.impl.(*linuxWebviewWindow).window + } + about := newMessageDialog(InfoDialog) + about.SetTitle(title). + SetMessage(message). + SetIcon(icon) + runQuestionDialog( + parent, + about, + ) +} + +type linuxDialog struct { + dialog *MessageDialog +} + +func (m *linuxDialog) show() { + windowId := getNativeApplication().getCurrentWindowID() + window := globalApplication.getWindowForID(windowId) + var parent pointer + if window != nil { + parent = window.impl.(*linuxWebviewWindow).window + } + + response := runQuestionDialog(parent, m.dialog) + if response >= 0 { + fmt.Println("Response: ", response) + button := m.dialog.Buttons[response] + if button.Callback != nil { + go button.Callback() + } + } +} + +func newDialogImpl(d *MessageDialog) *linuxDialog { + return &linuxDialog{ + dialog: d, + } +} + +type linuxOpenFileDialog struct { + dialog *OpenFileDialog +} + +func newOpenFileDialogImpl(d *OpenFileDialog) *linuxOpenFileDialog { + return &linuxOpenFileDialog{ + dialog: d, + } +} + +func (m *linuxOpenFileDialog) show() ([]string, error) { + openFileResponses[m.dialog.id] = make(chan string) + // nsWindow := unsafe.Pointer(nil) + if m.dialog.window != nil { + // get NSWindow from window + //nsWindow = m.dialog.window.impl.(*macosWebviewWindow).nsWindow + } + + // Massage filter patterns into macOS format + // We iterate all filter patterns, tidy them up and then join them with a semicolon + // This should produce a single string of extensions like "png;jpg;gif" + // var filterPatterns string + // if len(m.dialog.filters) > 0 { + // var allPatterns []string + // for _, filter := range m.dialog.filters { + // patternComponents := strings.Split(filter.Pattern, ";") + // for i, component := range patternComponents { + // filterPattern := strings.TrimSpace(component) + // filterPattern = strings.TrimPrefix(filterPattern, "*.") + // patternComponents[i] = filterPattern + // } + // allPatterns = append(allPatterns, strings.Join(patternComponents, ";")) + // } + // filterPatterns = strings.Join(allPatterns, ";") + // } + + // C.showOpenFileDialog(C.uint(m.dialog.id), + // C.bool(m.dialog.canChooseFiles), + // C.bool(m.dialog.canChooseDirectories), + // C.bool(m.dialog.canCreateDirectories), + // C.bool(m.dialog.showHiddenFiles), + // C.bool(m.dialog.allowsMultipleSelection), + // C.bool(m.dialog.resolvesAliases), + // C.bool(m.dialog.hideExtension), + // C.bool(m.dialog.treatsFilePackagesAsDirectories), + // C.bool(m.dialog.allowsOtherFileTypes), + // toCString(filterPatterns), + // C.uint(len(filterPatterns)), + // toCString(m.dialog.message), + // toCString(m.dialog.directory), + // toCString(m.dialog.buttonText), + // nsWindow) + var result []string + for filename := range openFileResponses[m.dialog.id] { + result = append(result, filename) + } + return result, nil +} + +type linuxSaveFileDialog struct { + dialog *SaveFileDialog +} + +func newSaveFileDialogImpl(d *SaveFileDialog) *linuxSaveFileDialog { + return &linuxSaveFileDialog{ + dialog: d, + } +} + +func (m *linuxSaveFileDialog) show() (string, error) { + saveFileResponses[m.dialog.id] = make(chan string) + // nsWindow := unsafe.Pointer(nil) + if m.dialog.window != nil { + // get NSWindow from window + // nsWindow = m.dialog.window.impl.(*linuxWebviewWindow).nsWindow + } + + // C.showSaveFileDialog(C.uint(m.dialog.id), + // C.bool(m.dialog.canCreateDirectories), + // C.bool(m.dialog.showHiddenFiles), + // C.bool(m.dialog.canSelectHiddenExtension), + // C.bool(m.dialog.hideExtension), + // C.bool(m.dialog.treatsFilePackagesAsDirectories), + // C.bool(m.dialog.allowOtherFileTypes), + // toCString(m.dialog.message), + // toCString(m.dialog.directory), + // toCString(m.dialog.buttonText), + // toCString(m.dialog.filename), + // nsWindow) + return <-saveFileResponses[m.dialog.id], nil +} diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go new file mode 100644 index 00000000..cf7606f5 --- /dev/null +++ b/v3/pkg/application/linux_cgo.go @@ -0,0 +1,955 @@ +//go:build linux && cgo + +package application + +import ( + "fmt" + "strings" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include +#include +#include +#include +#include +#include + +typedef struct CallbackID +{ + unsigned int value; +} CallbackID; + +extern void dispatchOnMainThreadCallback(unsigned int); + +static gboolean dispatchCallback(gpointer data) { + struct CallbackID *args = data; + unsigned int cid = args->value; + dispatchOnMainThreadCallback(cid); + free(args); + + return G_SOURCE_REMOVE; +}; + +static void dispatchOnMainThread(unsigned int id) { + CallbackID *args = malloc(sizeof(CallbackID)); + args->value = id; + g_idle_add((GSourceFunc)dispatchCallback, (gpointer)args); +} + +// exported below +void activateLinux(gpointer data); +void handleClick(void*); +extern gboolean onButtonEvent(GtkWidget *widget, GdkEventButton *event, gpointer user_data); +extern void onDragNDrop( + void *target, + GdkDragContext* context, + gint x, + gint y, + gpointer seldata, + guint info, + guint time, + gpointer data); +extern void onProcessRequest(void *request, gpointer user_data); +// exported below (end) + +static void signal_connect(GtkWidget *widget, char *event, void *cb, void* data) { + // g_signal_connect is a macro and can't be called directly + g_signal_connect(widget, event, cb, data); +} + +static void* new_message_dialog(GtkWindow *parent, const gchar *msg, int dialogType, bool hasButtons) { + // gtk_message_dialog_new is variadic! Can't call from cgo directly + GtkWidget *dialog; + int buttonMask; + + // buttons will be added after creation + buttonMask = GTK_BUTTONS_OK; + if (hasButtons) { + buttonMask = GTK_BUTTONS_NONE; + } + + dialog = gtk_message_dialog_new( + parent, + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + dialogType, + buttonMask, + msg); + + // g_signal_connect_swapped (dialog, + // "response", + // G_CALLBACK (callback), + // dialog); + return dialog; +}; + +extern void messageDialogCB(gint button); + +typedef struct Screen { + const char* id; + const char* name; + int p_width; + int p_height; + int width; + int height; + int x; + int y; + int w_width; + int w_height; + int w_x; + int w_y; + float scale; + double rotation; + bool isPrimary; +} Screen; + + +static int GetNumScreens(){ + return 0; +} + +*/ +import "C" + +type windowPointer *C.GtkWindow +type identifier C.uint +type pointer unsafe.Pointer +type GSList C.GSList +type GSListPointer *GSList + +var ( + nilRadioGroup GSListPointer = nil + gtkSignalHandlers map[*C.GtkWidget]C.gulong + gtkSignalToMenuItem map[*C.GtkWidget]*MenuItem +) + +func init() { + fmt.Println("linux_cgo") + + gtkSignalHandlers = map[*C.GtkWidget]C.gulong{} + gtkSignalToMenuItem = map[*C.GtkWidget]*MenuItem{} +} + +// mainthread stuff +func dispatchOnMainThread(id uint) { + C.dispatchOnMainThread(C.uint(id)) +} + +//export dispatchOnMainThreadCallback +func dispatchOnMainThreadCallback(callbackID C.uint) { + executeOnMainThread(uint(callbackID)) +} + +//export activateLinux +func activateLinux(data pointer) { + // NOOP: Callback for now +} + +// implementation below +func appName() string { + name := C.g_get_application_name() + defer C.free(unsafe.Pointer(name)) + return C.GoString(name) +} + +func appNew(name string) pointer { + nameC := C.CString(fmt.Sprintf("org.wails.%s", name)) + defer C.free(unsafe.Pointer(nameC)) + return pointer(C.gtk_application_new(nameC, C.G_APPLICATION_DEFAULT_FLAGS)) +} + +func appRun(app pointer) error { + application := (*C.GApplication)(app) + C.g_application_hold(application) // allows it to run without a window + signal := C.CString("activate") + C.g_signal_connect_data(C.gpointer(application), signal, C.GCallback(C.activateLinux), nil, nil, 0) + status := C.g_application_run(application, 0, nil) + C.g_application_release(application) + C.g_object_unref(C.gpointer(app)) + + var err error + if status != 0 { + err = fmt.Errorf("exit code: %d", status) + } + return err +} + +func appDestroy(application pointer) { + C.g_application_quit((*C.GApplication)(application)) +} + +func getCurrentWindowID(application pointer, windows map[windowPointer]uint) uint { + // TODO: Add extra metadata to window and use it! + window := (*C.GtkWindow)(C.gtk_application_get_active_window((*C.GtkApplication)(application))) + if window == nil { + return uint(1) + } + identifier, ok := windows[window] + if ok { + return identifier + } + // FIXME: Should we panic here if not found? + return uint(1) +} + +func getWindows(application pointer) []pointer { + result := []pointer{} + windows := C.gtk_application_get_windows((*C.GtkApplication)(application)) + for { + result = append(result, pointer(windows.data)) + windows = windows.next + if windows == nil { + return result + } + } +} + +func hideAllWindows(application pointer) { + for _, window := range getWindows(application) { + C.gtk_widget_hide((*C.GtkWidget)(window)) + } +} + +func showAllWindows(application pointer) { + for _, window := range getWindows(application) { + C.gtk_widget_show_all((*C.GtkWidget)(window)) + } +} + +// Menu +func menuAddSeparator(menu *Menu) { + C.gtk_menu_shell_append( + (*C.GtkMenuShell)((menu.impl).(*linuxMenu).native), + C.gtk_separator_menu_item_new()) +} + +func menuAppend(parent *Menu, menu *MenuItem) { + C.gtk_menu_shell_append( + (*C.GtkMenuShell)((parent.impl).(*linuxMenu).native), + (*C.GtkWidget)((menu.impl).(*linuxMenuItem).native), + ) + /* gtk4 + C.gtk_menu_item_set_submenu( + (*C.struct__GtkMenuItem)((menu.impl).(*linuxMenuItem).native), + (*C.struct__GtkWidget)((parent.impl).(*linuxMenu).native), + ) + */ +} + +func menuBarNew() pointer { + return pointer(C.gtk_menu_bar_new()) +} + +func menuNew() pointer { + return pointer(C.gtk_menu_new()) +} + +func menuSetSubmenu(item *MenuItem, menu *Menu) { + C.gtk_menu_item_set_submenu( + (*C.GtkMenuItem)((item.impl).(*linuxMenuItem).native), + (*C.GtkWidget)((menu.impl).(*linuxMenu).native)) +} + +func menuGetRadioGroup(item *linuxMenuItem) *GSList { + return (*GSList)(C.gtk_radio_menu_item_get_group((*C.GtkRadioMenuItem)(item.native))) +} + +//export handleClick +func handleClick(idPtr unsafe.Pointer) { + id := (*C.GtkWidget)(idPtr) + item, ok := gtkSignalToMenuItem[id] + if !ok { + return + } + + switch item.itemType { + case text, checkbox: + menuItemClicked <- item.id + case radio: + menuItem := (item.impl).(*linuxMenuItem) + if menuItem.isChecked() { + menuItemClicked <- item.id + } + } +} + +func attachMenuHandler(item *MenuItem) { + signal := C.CString("activate") + defer C.free(unsafe.Pointer(signal)) + + impl := (item.impl).(*linuxMenuItem) + widget := impl.native + flags := C.GConnectFlags(0) + handlerId := C.g_signal_connect_object( + C.gpointer(widget), + signal, + C.GCallback(C.handleClick), + C.gpointer(widget), + flags) + + id := (*C.GtkWidget)(widget) + gtkSignalToMenuItem[id] = item + gtkSignalHandlers[id] = handlerId + impl.handlerId = uint(handlerId) +} + +// menuItem +func menuItemChecked(widget pointer) bool { + if C.gtk_check_menu_item_get_active((*C.GtkCheckMenuItem)(widget)) == C.int(1) { + return true + } + return false +} + +func menuItemNew(label string) pointer { + cLabel := C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + return pointer(C.gtk_menu_item_new_with_label(cLabel)) +} + +func menuCheckItemNew(label string) pointer { + cLabel := C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + return pointer(C.gtk_check_menu_item_new_with_label(cLabel)) +} + +func menuItemSetChecked(widget pointer, checked bool) { + value := C.int(0) + if checked { + value = C.int(1) + } + C.gtk_check_menu_item_set_active( + (*C.GtkCheckMenuItem)(widget), + value) +} + +func menuItemSetDisabled(widget pointer, disabled bool) { + value := C.int(1) + if disabled { + value = C.int(0) + } + C.gtk_widget_set_sensitive( + (*C.GtkWidget)(widget), + value) +} + +func menuItemSetLabel(widget pointer, label string) { + value := C.CString(label) + C.gtk_menu_item_set_label( + (*C.GtkMenuItem)(widget), + value) + C.free(unsafe.Pointer(value)) +} + +func menuItemSetToolTip(widget pointer, tooltip string) { + value := C.CString(tooltip) + C.gtk_widget_set_tooltip_text( + (*C.GtkWidget)(widget), + value) + C.free(unsafe.Pointer(value)) +} + +func menuItemSignalBlock(widget pointer, handlerId uint, block bool) { + if block { + C.g_signal_handler_block(C.gpointer(widget), C.ulong(handlerId)) + } else { + C.g_signal_handler_unblock(C.gpointer(widget), C.ulong(handlerId)) + } +} + +func menuRadioItemNew(group *GSList, label string) pointer { + cLabel := C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + return pointer(C.gtk_radio_menu_item_new_with_label((*C.GSList)(group), cLabel)) +} + +// screen related + +func getScreenByIndex(display *C.struct__GdkDisplay, index int) *Screen { + monitor := C.gdk_display_get_monitor(display, C.int(index)) + // TODO: Do we need to update Screen to contain current info? + // currentMonitor := C.gdk_display_get_monitor_at_window(display, window) + + var geometry C.GdkRectangle + C.gdk_monitor_get_geometry(monitor, &geometry) + primary := false + if C.gdk_monitor_is_primary(monitor) == 1 { + primary = true + } + + return &Screen{ + IsPrimary: primary, + Scale: 1.0, + X: int(geometry.x), + Y: int(geometry.y), + Size: Size{ + Height: int(geometry.height), + Width: int(geometry.width), + }, + } +} + +func getScreens(app pointer) ([]*Screen, error) { + var screens []*Screen + window := C.gtk_application_get_active_window((*C.GtkApplication)(app)) + display := C.gdk_window_get_display((*C.GdkWindow)(unsafe.Pointer(window))) + count := C.gdk_display_get_n_monitors(display) + for i := 0; i < int(count); i++ { + screens = append(screens, getScreenByIndex(display, i)) + } + return screens, nil +} + +// widgets +func widgetSetVisible(widget pointer, hidden bool) { + if hidden { + C.gtk_widget_hide((*C.GtkWidget)(widget)) + } else { + C.gtk_widget_show((*C.GtkWidget)(widget)) + } +} + +// window related functions +func windowClose(window pointer) { + C.gtk_window_close((*C.GtkWindow)(window)) +} + +func windowEnableDND(id uint, webview pointer) { + dnd := C.CString("text/uri-list") + defer C.free(unsafe.Pointer(dnd)) + targetentry := C.gtk_target_entry_new(dnd, 0, C.guint(id)) + defer C.gtk_target_entry_free(targetentry) + C.gtk_drag_dest_set((*C.GtkWidget)(webview), C.GTK_DEST_DEFAULT_DROP, targetentry, 1, C.GDK_ACTION_COPY) + event := C.CString("drag-data-received") + defer C.free(unsafe.Pointer(event)) + windowId := C.uint(id) + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, C.onDragNDrop, unsafe.Pointer(C.gpointer(&windowId))) +} + +func windowExecJS(webview pointer, js string) { + value := C.CString(js) + C.webkit_web_view_evaluate_javascript((*C.WebKitWebView)(webview), + value, + C.long(len(js)), + nil, + C.CString(""), + nil, + nil, + nil) + C.free(unsafe.Pointer(value)) +} + +func windowDestroy(window pointer) { + // Should this truly 'destroy' ? + C.gtk_window_close((*C.GtkWindow)(window)) + //C.gtk_widget_destroy((*C.GtkWidget)(window)) +} + +func windowFullscreen(window pointer) { + C.gtk_window_fullscreen((*C.GtkWindow)(window)) +} + +func windowGetCurrentMonitor(window pointer) *C.GdkMonitor { + // Get the monitor that the window is currently on + display := C.gtk_widget_get_display((*C.GtkWidget)(window)) + gdk_window := C.gtk_widget_get_window((*C.GtkWidget)(window)) + if gdk_window == nil { + return nil + } + return C.gdk_display_get_monitor_at_window(display, gdk_window) +} + +func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, height int, scale int) { + monitor := windowGetCurrentMonitor(window) + if monitor == nil { + return -1, -1, -1, -1, 1 + } + var result C.GdkRectangle + C.gdk_monitor_get_geometry(monitor, &result) + scale = int(C.gdk_monitor_get_scale_factor(monitor)) + return int(result.x), int(result.y), int(result.width), int(result.height), scale +} + +func windowGetSize(window pointer) (int, int) { + var windowWidth C.int + var windowHeight C.int + C.gtk_window_get_size((*C.GtkWindow)(window), &windowWidth, &windowHeight) + return int(windowWidth), int(windowHeight) +} + +func windowGetPosition(window pointer) (int, int) { + var x C.int + var y C.int + C.gtk_window_get_position((*C.GtkWindow)(window), &x, &y) + return int(x), int(y) +} + +func windowHide(window pointer) { + C.gtk_widget_hide((*C.GtkWidget)(window)) +} + +func windowIsFullscreen(window pointer) bool { + gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window)) + state := C.gdk_window_get_state(gdkwindow) + return state&C.GDK_WINDOW_STATE_FULLSCREEN > 0 +} + +func windowIsMaximized(window pointer) bool { + gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window)) + state := C.gdk_window_get_state(gdkwindow) + return state&C.GDK_WINDOW_STATE_MAXIMIZED > 0 && state&C.GDK_WINDOW_STATE_FULLSCREEN == 0 +} + +func windowIsMinimized(window pointer) bool { + gdkwindow := C.gtk_widget_get_window((*C.GtkWidget)(window)) + state := C.gdk_window_get_state(gdkwindow) + return state&C.GDK_WINDOW_STATE_ICONIFIED > 0 +} + +func windowIsVisible(window pointer) bool { + if C.gtk_widget_is_visible((*C.GtkWidget)(window)) == 1 { + return true + } + return false +} + +func windowMaximize(window pointer) { + C.gtk_window_maximize((*C.GtkWindow)(window)) +} + +func windowMinimize(window pointer) { + C.gtk_window_iconify((*C.GtkWindow)(window)) +} + +func windowNew(application pointer, menu pointer, windowId uint, gpuPolicy int) (window pointer, webview pointer) { + window = pointer(C.gtk_application_window_new((*C.GtkApplication)(application))) + C.g_object_ref_sink(C.gpointer(window)) + webview = windowNewWebview(windowId, gpuPolicy) + vbox := pointer(C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)) + C.gtk_container_add((*C.GtkContainer)(window), (*C.GtkWidget)(vbox)) + + if menu != nil { + C.gtk_box_pack_start((*C.GtkBox)(vbox), (*C.GtkWidget)(menu), 0, 0, 0) + } + C.gtk_box_pack_start((*C.GtkBox)(unsafe.Pointer(vbox)), (*C.GtkWidget)(webview), 1, 1, 0) + return +} + +func windowNewWebview(parentId uint, gpuPolicy int) pointer { + manager := C.webkit_user_content_manager_new() + external := C.CString("external") + C.webkit_user_content_manager_register_script_message_handler(manager, external) + C.free(unsafe.Pointer(external)) + webview := C.webkit_web_view_new_with_user_content_manager(manager) + id := C.uint(parentId) + if !registered { + wails := C.CString("wails") + C.webkit_web_context_register_uri_scheme( + C.webkit_web_context_get_default(), + wails, + C.WebKitURISchemeRequestCallback(C.onProcessRequest), + C.gpointer(&id), + nil) + registered = true + C.free(unsafe.Pointer(wails)) + } + settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(unsafe.Pointer(webview))) + wails_io := C.CString("wails.io") + empty := C.CString("") + defer C.free(unsafe.Pointer(wails_io)) + defer C.free(unsafe.Pointer(empty)) + C.webkit_settings_set_user_agent_with_application_details(settings, wails_io, empty) + + switch gpuPolicy { + case 0: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS) + break + case 1: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND) + break + case 2: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER) + break + default: + C.webkit_settings_set_hardware_acceleration_policy(settings, C.WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND) + } + return pointer(webview) +} + +func windowPresent(window pointer) { + C.gtk_window_present((*C.GtkWindow)(window)) + // gtk_window_unminimize ((*C.GtkWindow)(w.window)) /// gtk4 +} + +func windowReload(webview pointer, address string) { + uri := C.CString(address) + C.webkit_web_view_load_uri((*C.WebKitWebView)(webview), uri) + C.free(unsafe.Pointer(uri)) +} + +func windowResize(window pointer, width, height int) { + C.gtk_window_resize( + (*C.GtkWindow)(window), + C.gint(width), + C.gint(height)) +} + +func windowShow(window pointer) { + C.gtk_widget_show_all((*C.GtkWidget)(window)) +} + +func windowSetBackgroundColour(webview pointer, colour RGBA) { + rgba := C.GdkRGBA{C.double(colour.Red) / 255.0, C.double(colour.Green) / 255.0, C.double(colour.Blue) / 255.0, C.double(colour.Alpha) / 255.0} + C.webkit_web_view_set_background_color((*C.WebKitWebView)(webview), &rgba) +} + +func windowSetGeometryHints(window pointer, minWidth, minHeight, maxWidth, maxHeight int) { + size := C.GdkGeometry{ + min_width: C.int(minWidth), + min_height: C.int(minHeight), + max_width: C.int(maxWidth), + max_height: C.int(maxHeight), + } + C.gtk_window_set_geometry_hints((*C.GtkWindow)(window), nil, &size, C.GDK_HINT_MAX_SIZE|C.GDK_HINT_MIN_SIZE) +} + +func windowSetFrameless(window pointer, frameless bool) { + C.gtk_window_set_decorated((*C.GtkWindow)(window), gtkBool(!frameless)) + // TODO: Deal with transparency for the titlebar if possible when !frameless + // Perhaps we just make it undecorated and add a menu bar inside? +} + +// TODO: confirm this is working properly +func windowSetHTML(webview pointer, html string) { + cHTML := C.CString(html) + uri := C.CString("wails://") + empty := C.CString("") + defer C.free(unsafe.Pointer(cHTML)) + defer C.free(unsafe.Pointer(uri)) + defer C.free(unsafe.Pointer(empty)) + C.webkit_web_view_load_alternate_html( + (*C.WebKitWebView)(webview), + cHTML, + uri, + empty) +} + +func windowSetKeepAbove(window pointer, alwaysOnTop bool) { + C.gtk_window_set_keep_above((*C.GtkWindow)(window), gtkBool(alwaysOnTop)) +} + +func windowSetResizable(window pointer, resizable bool) { + C.gtk_window_set_resizable((*C.GtkWindow)(window), gtkBool(resizable)) +} + +func windowSetTitle(window pointer, title string) { + cTitle := C.CString(title) + C.gtk_window_set_title((*C.GtkWindow)(window), cTitle) + C.free(unsafe.Pointer(cTitle)) +} + +func windowSetTransparent(window pointer) { + screen := C.gtk_widget_get_screen((*C.GtkWidget)(window)) + visual := C.gdk_screen_get_rgba_visual(screen) + + if visual != nil && C.gdk_screen_is_composited(screen) == C.int(1) { + C.gtk_widget_set_app_paintable((*C.GtkWidget)(window), C.gboolean(1)) + C.gtk_widget_set_visual((*C.GtkWidget)(window), visual) + } +} + +func windowSetURL(webview pointer, uri string) { + target := C.CString(uri) + C.webkit_web_view_load_uri((*C.WebKitWebView)(webview), target) + C.free(unsafe.Pointer(target)) +} + +func windowSetupSignalHandlers(windowId uint, window, webview pointer, hideOnClose bool) { + event := C.CString("delete-event") + defer C.free(unsafe.Pointer(event)) + + // Window close handler + if hideOnClose { + C.signal_connect((*C.GtkWidget)(window), event, C.gtk_widget_hide_on_delete, C.NULL) + } else { + //FIXME: what event should be emitted? + // C.signal_connect((*C.GtkWidget)(window), event, C.close_button_pressed, w.parent.id) + } + + /* + event = C.CString("load-changed") + defer C.free(unsafe.Pointer(event)) + C.signal_connect(webview, event, C.webviewLoadChanged, unsafe.Pointer(&w.parent.id)) + */ + id := C.uint(windowId) + event = C.CString("button-press-event") + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, C.onButtonEvent, unsafe.Pointer(&id)) + C.free(unsafe.Pointer(event)) + event = C.CString("button-release-event") + defer C.free(unsafe.Pointer(event)) + C.signal_connect((*C.GtkWidget)(unsafe.Pointer(webview)), event, C.onButtonEvent, unsafe.Pointer(&id)) +} + +func windowToggleDevTools(webview pointer) { + settings := C.webkit_web_view_get_settings((*C.WebKitWebView)(webview)) + enabled := C.webkit_settings_get_enable_developer_extras(settings) + switch enabled { + case C.int(0): + enabled = C.int(1) + case C.int(1): + enabled = C.int(0) + } + C.webkit_settings_set_enable_developer_extras(settings, enabled) +} + +func windowUnfullscreen(window pointer) { + C.gtk_window_unfullscreen((*C.GtkWindow)(window)) +} + +func windowUnmaximize(window pointer) { + C.gtk_window_unmaximize((*C.GtkWindow)(window)) +} + +func windowZoom(webview pointer) float64 { + return float64(C.webkit_web_view_get_zoom_level((*C.WebKitWebView)(webview))) +} + +// FIXME: ZoomIn/Out is assumed to be incorrect! +func windowZoomIn(webview pointer) { + ZoomInFactor := 1.10 + windowZoomSet(webview, windowZoom(webview)*ZoomInFactor) +} +func windowZoomOut(webview pointer) { + ZoomOutFactor := -1.10 + windowZoomSet(webview, windowZoom(webview)*ZoomOutFactor) +} + +func windowZoomSet(webview pointer, zoom float64) { + C.webkit_web_view_set_zoom_level((*C.WebKitWebView)(webview), C.double(zoom)) +} + +func windowMove(window pointer, x, y int) { + C.gtk_window_move((*C.GtkWindow)(window), C.int(x), C.int(y)) +} + +//export onButtonEvent +func onButtonEvent(_ *C.GtkWidget, event *C.GdkEventButton, data unsafe.Pointer) C.gboolean { + // Constants (defined here to be easier to use with ) + GdkButtonPress := C.GDK_BUTTON_PRESS // 4 + Gdk2ButtonPress := C.GDK_2BUTTON_PRESS // 5 for double-click + GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7 + + windowId := uint(*((*C.uint)(data))) + window := globalApplication.getWindowForID(windowId) + if window == nil { + return C.gboolean(0) + } + lw, ok := (window.impl).(*linuxWebviewWindow) + if !ok { + return C.gboolean(0) + } + + if event == nil { + return C.gboolean(0) + } + if event.button == 3 { + return C.gboolean(0) + } + + switch int(event._type) { + case GdkButtonPress: + lw.startDrag() //uint(event.button), int(event.x_root), int(event.y_root)) + case Gdk2ButtonPress: + fmt.Printf("%d - button %d - double-clicked\n", windowId, int(event.button)) + case GdkButtonRelease: + lw.endDrag(uint(event.button), int(event.x_root), int(event.y_root)) + } + + return C.gboolean(0) +} + +//export onDragNDrop +func onDragNDrop(target unsafe.Pointer, context *C.GdkDragContext, x C.gint, y C.gint, seldata unsafe.Pointer, info C.guint, time C.guint, data unsafe.Pointer) { + fmt.Println("target", target, info) + var length C.gint + selection := unsafe.Pointer(C.gtk_selection_data_get_data_with_length((*C.GtkSelectionData)(seldata), &length)) + extracted := C.g_uri_list_extract_uris((*C.char)(selection)) + defer C.g_strfreev(extracted) + + uris := unsafe.Slice( + (**C.char)(unsafe.Pointer(extracted)), + int(length)) + + var filenames []string + for _, uri := range uris { + if uri == nil { + break + } + filenames = append(filenames, strings.TrimPrefix(C.GoString(uri), "file://")) + } + windowDragAndDropBuffer <- &dragAndDropMessage{ + windowId: uint(*((*C.uint)(data))), + filenames: filenames, + } + C.gtk_drag_finish(context, C.true, C.false, time) +} + +//export onProcessRequest +func onProcessRequest(request unsafe.Pointer, data unsafe.Pointer) { + windowId := uint(*((*C.uint)(data))) + webviewRequests <- &webViewAssetRequest{ + Request: webview.NewRequest(request), + windowId: windowId, + windowName: globalApplication.getWindowForID(windowId).Name(), + } +} + +func gtkBool(input bool) C.gboolean { + if input { + return C.gboolean(1) + } + return C.gboolean(0) +} + +// dialog related + +func setWindowIcon(window pointer, icon []byte) { + fmt.Println("setWindowIcon", len(icon)) + loader := C.gdk_pixbuf_loader_new() + if loader == nil { + return + } + written := C.gdk_pixbuf_loader_write( + loader, + (*C.uchar)(&icon[0]), + C.ulong(len(icon)), + nil) + if written == 0 { + fmt.Println("failed to write icon") + return + } + C.gdk_pixbuf_loader_close(loader, nil) + pixbuf := C.gdk_pixbuf_loader_get_pixbuf(loader) + if pixbuf != nil { + fmt.Println("gtk_window_set_icon", window) + C.gtk_window_set_icon((*C.GtkWindow)(window), pixbuf) + } + C.g_object_unref(C.gpointer(loader)) +} + +//export messageDialogCB +func messageDialogCB(button C.int) { + fmt.Println("messageDialogCB", button) + +} + +func runQuestionDialog(parent pointer, options *MessageDialog) int { + cMsg := C.CString(options.Message) + cTitle := C.CString(options.Title) + defer C.free(unsafe.Pointer(cMsg)) + defer C.free(unsafe.Pointer(cTitle)) + hasButtons := false + if len(options.Buttons) > 0 { + hasButtons = true + } + + dType, ok := map[DialogType]C.int{ + InfoDialog: C.GTK_MESSAGE_INFO, + QuestionDialog: C.GTK_MESSAGE_QUESTION, + WarningDialog: C.GTK_MESSAGE_WARNING, + }[options.DialogType] + if !ok { + // FIXME: Add logging here! + dType = C.GTK_MESSAGE_INFO + } + + dialog := C.new_message_dialog((*C.GtkWindow)(parent), cMsg, dType, C.bool(hasButtons)) + if options.Title != "" { + C.gtk_window_set_title( + (*C.GtkWindow)(unsafe.Pointer(dialog)), + cTitle) + } + + if img, err := pngToImage(options.Icon); err == nil { + gbytes := C.g_bytes_new_static( + C.gconstpointer(unsafe.Pointer(&img.Pix[0])), + C.ulong(len(img.Pix))) + defer C.g_bytes_unref(gbytes) + pixBuf := C.gdk_pixbuf_new_from_bytes( + gbytes, + C.GDK_COLORSPACE_RGB, + 1, // has_alpha + 8, + C.int(img.Bounds().Dx()), + C.int(img.Bounds().Dy()), + C.int(img.Stride), + ) + image := C.gtk_image_new_from_pixbuf(pixBuf) + C.gtk_widget_set_visible((*C.GtkWidget)(image), C.gboolean(1)) + contentArea := C.gtk_dialog_get_content_area((*C.GtkDialog)(dialog)) + C.gtk_container_add( + (*C.GtkContainer)(unsafe.Pointer(contentArea)), + (*C.GtkWidget)(image)) + } + for i, button := range options.Buttons { + cLabel := C.CString(button.Label) + defer C.free(unsafe.Pointer(cLabel)) + index := C.int(i) + C.gtk_dialog_add_button( + (*C.GtkDialog)(dialog), cLabel, index) + if button.IsDefault { + C.gtk_dialog_set_default_response((*C.GtkDialog)(dialog), index) + } + } + + defer C.gtk_widget_destroy((*C.GtkWidget)(dialog)) + return int(C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dialog)))) +} + +//export openFileDialogCallbackEnd +func openFileDialogCallbackEnd(cid C.uint) { + id := uint(cid) + channel, ok := openFileResponses[id] + if ok { + close(channel) + delete(openFileResponses, id) + freeDialogID(id) + } else { + panic("No channel found for open file dialog") + } +} + +//export openFileDialogCallback +func openFileDialogCallback(cid C.uint, cpath *C.char) { + path := C.GoString(cpath) + id := uint(cid) + channel, ok := openFileResponses[id] + if ok { + channel <- path + } else { + panic("No channel found for open file dialog") + } +} + +//export saveFileDialogCallback +func saveFileDialogCallback(cid C.uint, cpath *C.char) { + // Covert the path to a string + path := C.GoString(cpath) + id := uint(cid) + // put response on channel + channel, ok := saveFileResponses[id] + if ok { + channel <- path + close(channel) + delete(saveFileResponses, id) + freeDialogID(id) + + } else { + panic("No channel found for save file dialog") + } +} diff --git a/v3/pkg/application/mainthread_linux.go b/v3/pkg/application/mainthread_linux.go new file mode 100644 index 00000000..572b081b --- /dev/null +++ b/v3/pkg/application/mainthread_linux.go @@ -0,0 +1,18 @@ +//go:build linux + +package application + +func (m *linuxApp) dispatchOnMainThread(id uint) { + dispatchOnMainThread(id) +} + +func executeOnMainThread(callbackID uint) { + mainThreadFunctionStoreLock.RLock() + fn := mainThreadFunctionStore[callbackID] + if fn == nil { + Fatal("dispatchCallback called with invalid id: %v", callbackID) + } + delete(mainThreadFunctionStore, callbackID) + mainThreadFunctionStoreLock.RUnlock() + fn() +} diff --git a/v3/pkg/application/menu_linux.go b/v3/pkg/application/menu_linux.go new file mode 100644 index 00000000..2bd8684e --- /dev/null +++ b/v3/pkg/application/menu_linux.go @@ -0,0 +1,114 @@ +//go:build linux + +package application + +type linuxMenu struct { + menu *Menu + native pointer +} + +func newMenuImpl(menu *Menu) *linuxMenu { + result := &linuxMenu{ + menu: menu, + native: menuBarNew(), + } + return result +} + +func (m *linuxMenu) update() { + m.processMenu(m.menu) +} + +func (m *linuxMenu) processMenu(menu *Menu) { + if menu.impl == nil { + menu.impl = &linuxMenu{ + menu: menu, + native: menuNew(), + } + } + var currentRadioGroup GSListPointer + + for _, item := range menu.items { + // drop the group if we have run out of radio items + if item.itemType != radio { + currentRadioGroup = nilRadioGroup + } + + switch item.itemType { + case submenu: + menuItem := newMenuItemImpl(item) + item.impl = menuItem + m.processMenu(item.submenu) + m.addSubMenuToItem(item.submenu, item) + m.addMenuItem(menu, item) + case text, checkbox: + menuItem := newMenuItemImpl(item) + item.impl = menuItem + m.addMenuItem(menu, item) + case radio: + menuItem := newRadioItemImpl(item, currentRadioGroup) + item.impl = menuItem + m.addMenuItem(menu, item) + currentRadioGroup = menuGetRadioGroup(menuItem) + case separator: + m.addMenuSeparator(menu) + } + + } + + for _, item := range menu.items { + if item.callback != nil { + m.attachHandler(item) + } + } + +} + +func (m *linuxMenu) attachHandler(item *MenuItem) { + attachMenuHandler(item) +} + +func (m *linuxMenu) addSubMenuToItem(menu *Menu, item *MenuItem) { + if menu.impl == nil { + menu.impl = &linuxMenu{ + menu: menu, + native: menuNew(), + } + } + menuSetSubmenu(item, menu) +} + +func (m *linuxMenu) addMenuItem(parent *Menu, menu *MenuItem) { + menuAppend(parent, menu) +} + +func (m *linuxMenu) addMenuSeparator(menu *Menu) { + menuAddSeparator(menu) + +} + +func (m *linuxMenu) addServicesMenu(menu *Menu) { + // FIXME: Should this be required? +} + +func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { + impl := newMenuImpl(&Menu{label: name}) + menu := &Menu{ + label: name, + items: items, + impl: impl, + } + impl.menu = menu + return menu +} + +func defaultApplicationMenu() *Menu { + menu := NewMenu() + menu.AddRole(AppMenu) + menu.AddRole(FileMenu) + menu.AddRole(EditMenu) + menu.AddRole(ViewMenu) + menu.AddRole(WindowMenu) + menu.AddRole(HelpMenu) + return menu +} diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go new file mode 100644 index 00000000..8b32825f --- /dev/null +++ b/v3/pkg/application/menuitem_linux.go @@ -0,0 +1,364 @@ +//go:build linux + +package application + +import ( + "fmt" + "runtime" +) + +type linuxMenuItem struct { + menuItem *MenuItem + native pointer + handlerId uint +} + +func (l linuxMenuItem) setTooltip(tooltip string) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetToolTip(l.native, tooltip) + }) +} + +func (l linuxMenuItem) blockSignal() { + if l.handlerId != 0 { + menuItemSignalBlock(l.native, l.handlerId, true) + } +} + +func (l linuxMenuItem) unBlockSignal() { + if l.handlerId != 0 { + menuItemSignalBlock(l.native, l.handlerId, false) + } +} + +func (l linuxMenuItem) setLabel(s string) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetLabel(l.native, s) + }) +} + +func (l linuxMenuItem) isChecked() bool { + return menuItemChecked(l.native) +} + +func (l linuxMenuItem) setDisabled(disabled bool) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetDisabled(l.native, disabled) + }) +} + +func (l linuxMenuItem) setChecked(checked bool) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetChecked(l.native, checked) + }) +} + +func (l linuxMenuItem) setHidden(hidden bool) { + globalApplication.dispatchOnMainThread(func() { + l.blockSignal() + defer l.unBlockSignal() + widgetSetVisible(l.native, hidden) + }) +} + +func (l linuxMenuItem) setAccelerator(accelerator *accelerator) { + fmt.Println("setAccelerator", accelerator) + // Set the keyboard shortcut of the menu item + // var modifier C.int + // var key *C.char + if accelerator != nil { + // modifier = C.int(toMacModifier(accelerator.Modifiers)) + // key = C.CString(accelerator.Key) + } + + // Convert the key to a string + // C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) +} + +func newMenuItemImpl(item *MenuItem) *linuxMenuItem { + result := &linuxMenuItem{ + menuItem: item, + } + switch item.itemType { + case text: + result.native = menuItemNew(item.label) + + case checkbox: + result.native = menuCheckItemNew(item.label) + result.setChecked(item.checked) + if item.accelerator != nil { + result.setAccelerator(item.accelerator) + } + case submenu: + result.native = menuItemNew(item.label) + + default: + panic(fmt.Sprintf("Unknown menu type: %v", item.itemType)) + } + result.setDisabled(result.menuItem.disabled) + return result +} + +func newRadioItemImpl(item *MenuItem, group GSListPointer) *linuxMenuItem { + result := &linuxMenuItem{ + menuItem: item, + native: menuRadioItemNew(group, item.label), + } + result.setChecked(item.checked) + result.setDisabled(result.menuItem.disabled) + return result +} + +func newSpeechMenu() *MenuItem { + speechMenu := NewMenu() + speechMenu.Add("Start Speaking"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+."). + OnClick(func(ctx *Context) { + // C.startSpeaking() + }) + speechMenu.Add("Stop Speaking"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+,"). + OnClick(func(ctx *Context) { + // C.stopSpeaking() + }) + subMenu := newSubMenuItem("Speech") + subMenu.submenu = speechMenu + return subMenu +} + +func newHideMenuItem() *MenuItem { + return newMenuItem("Hide " + globalApplication.options.Name). + SetAccelerator("CmdOrCtrl+h"). + OnClick(func(ctx *Context) { + + // C.hideApplication() + }) +} + +func newHideOthersMenuItem() *MenuItem { + return newMenuItem("Hide Others"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+h"). + OnClick(func(ctx *Context) { + // C.hideOthers() + }) +} + +func newUnhideMenuItem() *MenuItem { + return newMenuItem("Show All"). + OnClick(func(ctx *Context) { + // C.showAll() + }) +} + +func newUndoMenuItem() *MenuItem { + return newMenuItem("Undo"). + SetAccelerator("CmdOrCtrl+z"). + OnClick(func(ctx *Context) { + // C.undo() + }) +} + +// newRedoMenuItem creates a new menu item for redoing the last action +func newRedoMenuItem() *MenuItem { + return newMenuItem("Redo"). + SetAccelerator("CmdOrCtrl+Shift+z"). + OnClick(func(ctx *Context) { + // C.redo() + }) +} + +func newCutMenuItem() *MenuItem { + return newMenuItem("Cut"). + SetAccelerator("CmdOrCtrl+x"). + OnClick(func(ctx *Context) { + // C.cut() + }) +} + +func newCopyMenuItem() *MenuItem { + return newMenuItem("Copy"). + SetAccelerator("CmdOrCtrl+c"). + OnClick(func(ctx *Context) { + // C.copy() + }) +} + +func newPasteMenuItem() *MenuItem { + return newMenuItem("Paste"). + SetAccelerator("CmdOrCtrl+v"). + OnClick(func(ctx *Context) { + // C.paste() + }) +} + +func newPasteAndMatchStyleMenuItem() *MenuItem { + return newMenuItem("Paste and Match Style"). + SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+v"). + OnClick(func(ctx *Context) { + // C.pasteAndMatchStyle() + }) +} + +func newDeleteMenuItem() *MenuItem { + return newMenuItem("Delete"). + SetAccelerator("backspace"). + OnClick(func(ctx *Context) { + // C.delete() + }) +} + +func newQuitMenuItem() *MenuItem { + return newMenuItem("Quit " + globalApplication.options.Name). + SetAccelerator("CmdOrCtrl+q"). + OnClick(func(ctx *Context) { + globalApplication.Quit() + }) +} + +func newSelectAllMenuItem() *MenuItem { + return newMenuItem("Select All"). + SetAccelerator("CmdOrCtrl+a"). + OnClick(func(ctx *Context) { + // C.selectAll() + }) +} + +func newAboutMenuItem() *MenuItem { + return newMenuItem("About " + globalApplication.options.Name). + OnClick(func(ctx *Context) { + globalApplication.ShowAboutDialog() + }) +} + +func newCloseMenuItem() *MenuItem { + return newMenuItem("Close"). + SetAccelerator("CmdOrCtrl+w"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Close() + } + }) +} + +func newReloadMenuItem() *MenuItem { + return newMenuItem("Reload"). + SetAccelerator("CmdOrCtrl+r"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Reload() + } + }) +} + +func newForceReloadMenuItem() *MenuItem { + return newMenuItem("Force Reload"). + SetAccelerator("CmdOrCtrl+Shift+r"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ForceReload() + } + }) +} + +func newToggleFullscreenMenuItem() *MenuItem { + result := newMenuItem("Toggle Full Screen"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ToggleFullscreen() + } + }) + if runtime.GOOS == "darwin" { + result.SetAccelerator("Ctrl+Command+F") + } else { + result.SetAccelerator("F11") + } + return result +} + +func newToggleDevToolsMenuItem() *MenuItem { + return newMenuItem("Toggle Developer Tools"). + SetAccelerator("Alt+Command+I"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ToggleDevTools() + } + }) +} + +func newZoomResetMenuItem() *MenuItem { + // reset zoom menu item + return newMenuItem("Actual Size"). + SetAccelerator("CmdOrCtrl+0"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomReset() + } + }) +} + +func newZoomInMenuItem() *MenuItem { + return newMenuItem("Zoom In"). + SetAccelerator("CmdOrCtrl+plus"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomIn() + } + }) +} + +func newZoomOutMenuItem() *MenuItem { + return newMenuItem("Zoom Out"). + SetAccelerator("CmdOrCtrl+-"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.ZoomOut() + } + }) +} + +func newMinimizeMenuItem() *MenuItem { + return newMenuItem("Minimize"). + SetAccelerator("CmdOrCtrl+M"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Minimise() + } + }) +} + +func newZoomMenuItem() *MenuItem { + return newMenuItem("Zoom"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Zoom() + } + }) +} + +func newFullScreenMenuItem() *MenuItem { + return newMenuItem("Fullscreen"). + OnClick(func(ctx *Context) { + currentWindow := globalApplication.CurrentWindow() + if currentWindow != nil { + currentWindow.Fullscreen() + } + }) +} diff --git a/v3/pkg/application/options_linux.go b/v3/pkg/application/options_linux.go new file mode 100644 index 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..6f8026a6 --- /dev/null +++ b/v3/pkg/application/screen_linux.go @@ -0,0 +1,29 @@ +//go:build linux + +package application + +import ( + "fmt" + "sync" +) + +func (m *linuxApp) getPrimaryScreen() (*Screen, error) { + return nil, fmt.Errorf("not implemented") +} + +func (m *linuxApp) getScreens() ([]*Screen, error) { + var wg sync.WaitGroup + var screens []*Screen + var err error + wg.Add(1) + globalApplication.dispatchOnMainThread(func() { + screens, err = getScreens(m.application) + wg.Done() + }) + wg.Wait() + return screens, err +} + +func getScreenForWindow(window *linuxWebviewWindow) (*Screen, error) { + return window.getScreen() +} diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go new file mode 100644 index 00000000..8cc74460 --- /dev/null +++ b/v3/pkg/application/systemtray_linux.go @@ -0,0 +1,90 @@ +//go:build linux + +package application + +type linuxSystemTray struct { + id uint + label string + icon []byte + menu *Menu + + iconPosition int + isTemplateIcon bool +} + +func (s *linuxSystemTray) setIconPosition(position int) { + s.iconPosition = position +} + +func (s *linuxSystemTray) setMenu(menu *Menu) { + s.menu = menu +} + +func (s *linuxSystemTray) run() { + globalApplication.dispatchOnMainThread(func() { + // if s.nsStatusItem != nil { + // Fatal("System tray '%d' already running", s.id) + // } + // s.nsStatusItem = unsafe.Pointer(C.systemTrayNew()) + if s.label != "" { + // C.systemTraySetLabel(s.nsStatusItem, C.CString(s.label)) + } + if s.icon != nil { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&s.icon[0]), C.int(len(s.icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + } + if s.menu != nil { + s.menu.Update() + // Convert impl to macosMenu object + // s.nsMenu = (s.menu.impl).(*macosMenu).nsMenu + // C.systemTraySetMenu(s.nsStatusItem, s.nsMenu) + } + + }) +} + +func (s *linuxSystemTray) setIcon(icon []byte) { + s.icon = icon + globalApplication.dispatchOnMainThread(func() { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + }) +} + +func (s *linuxSystemTray) setDarkModeIcon(icon []byte) { + s.icon = icon + globalApplication.dispatchOnMainThread(func() { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + }) +} + +func (s *linuxSystemTray) setTemplateIcon(icon []byte) { + s.icon = icon + s.isTemplateIcon = true + globalApplication.dispatchOnMainThread(func() { + // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) + // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + }) +} + +func newSystemTrayImpl(s *SystemTray) systemTrayImpl { + return &linuxSystemTray{ + id: s.id, + label: s.label, + icon: s.icon, + menu: s.menu, + iconPosition: s.iconPosition, + isTemplateIcon: s.isTemplateIcon, + } +} + +func (s *linuxSystemTray) setLabel(label string) { + s.label = label + // C.systemTraySetLabel(s.nsStatusItem, C.CString(label)) +} + +func (s *linuxSystemTray) destroy() { + // Remove the status item from the status bar and its associated menu + // C.systemTrayDestroy(s.nsStatusItem) +} diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go new file mode 100644 index 00000000..c1eb7b2e --- /dev/null +++ b/v3/pkg/application/webview_window_linux.go @@ -0,0 +1,492 @@ +//go:build linux + +package application + +import ( + "fmt" + "net/url" + "sync" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v3/pkg/events" +) + +var showDevTools = func(window unsafe.Pointer) {} + +type dragInfo struct { + XRoot int + YRoot int + DragTime int + MouseButton uint +} + +type linuxWebviewWindow struct { + id uint + application pointer + window pointer + webview pointer + parent *WebviewWindow + menubar pointer + vbox pointer + menu *menu.Menu + accels pointer + lastWidth int + lastHeight int + drag dragInfo +} + +var ( + registered bool = false // avoid 'already registered message' about 'wails://' +) + +func (w *linuxWebviewWindow) startDrag() error { + return nil +} + +func (w *linuxWebviewWindow) endDrag(button uint, x, y int) { + fmt.Println("endDrag", button, x, y) +} + +func (w *linuxWebviewWindow) enableDND() { + windowEnableDND(w.parent.id, w.webview) +} + +func (w *linuxWebviewWindow) connectSignals() { + windowSetupSignalHandlers(w.parent.id, w.window, w.webview, w.parent.options.HideOnClose) +} + +func (w *linuxWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) { + // Create the menu + thisMenu := newMenuImpl(menu) + thisMenu.update() + fmt.Println("linux.openContextMenu() - not implemented") + /* void + gtk_menu_popup_at_rect ( + GtkMenu* menu, + GdkWindow* rect_window, + const GdkRectangle* rect, + GdkGravity rect_anchor, + GdkGravity menu_anchor, + const GdkEvent* trigger_event + ) + */ +} + +func (w *linuxWebviewWindow) getZoom() float64 { + return windowZoom(w.webview) +} + +func (w *linuxWebviewWindow) setZoom(zoom float64) { + windowZoomSet(w.webview, zoom) +} + +func (w *linuxWebviewWindow) setFrameless(frameless bool) { + windowSetFrameless(w.window, frameless) +} + +func (w *linuxWebviewWindow) getScreen() (*Screen, error) { + mx, my, width, height, scale := windowGetCurrentMonitorGeometry(w.window) + return &Screen{ + ID: fmt.Sprintf("%d", w.id), // A unique identifier for the display + Name: w.parent.Name(), // The name of the display + Scale: float32(scale), // The scale factor of the display + X: mx, // The x-coordinate of the top-left corner of the rectangle + Y: my, // The y-coordinate of the top-left corner of the rectangle + Size: Size{Width: width, Height: height}, // The size of the display + Bounds: Rect{}, // The bounds of the display + WorkArea: Rect{}, // The work area of the display + IsPrimary: false, // Whether this is the primary display + Rotation: 0.0, // The rotation of the display + }, nil +} + +func (w *linuxWebviewWindow) focus() { + globalApplication.dispatchOnMainThread(func() { + windowPresent(w.window) + }) +} + +func (w *linuxWebviewWindow) show() { + globalApplication.dispatchOnMainThread(func() { + windowShow(w.window) + }) +} + +func (w *linuxWebviewWindow) hide() { + globalApplication.dispatchOnMainThread(func() { + windowHide(w.window) + }) +} + +func (w *linuxWebviewWindow) isNormal() bool { + return !w.isMinimised() && !w.isMaximised() && !w.isFullscreen() +} + +func (w *linuxWebviewWindow) isVisible() bool { + return windowIsVisible(w.window) +} + +func (w *linuxWebviewWindow) setFullscreenButtonEnabled(enabled bool) { + // C.setFullscreenButtonEnabled(w.nsWindow, C.bool(enabled)) + fmt.Println("setFullscreenButtonEnabled - not implemented") +} + +func (w *linuxWebviewWindow) disableSizeConstraints() { + x, y, width, height, scale := windowGetCurrentMonitorGeometry(w.window) + w.setMinMaxSize(x, y, width*scale, height*scale) +} + +func (w *linuxWebviewWindow) unfullscreen() { + fmt.Println("unfullscreen") + globalApplication.dispatchOnMainThread(func() { + windowUnfullscreen(w.window) + w.unmaximise() + }) +} + +func (w *linuxWebviewWindow) fullscreen() { + w.maximise() + w.lastWidth, w.lastHeight = w.size() + globalApplication.dispatchOnMainThread(func() { + x, y, width, height, scale := windowGetCurrentMonitorGeometry(w.window) + if x == -1 && y == -1 && width == -1 && height == -1 { + return + } + w.setMinMaxSize(0, 0, width*scale, height*scale) + w.setSize(width*scale, height*scale) + windowFullscreen(w.window) + w.setPosition(0, 0) + }) +} + +func (w *linuxWebviewWindow) unminimise() { + windowPresent(w.window) +} + +func (w *linuxWebviewWindow) unmaximise() { + windowUnmaximize(w.window) +} + +func (w *linuxWebviewWindow) maximise() { + windowMaximize(w.window) +} + +func (w *linuxWebviewWindow) minimise() { + windowMinimize(w.window) +} + +func (w *linuxWebviewWindow) on(eventID uint) { + // Don't think this is correct! + // GTK Events are strings + fmt.Println("on()", eventID) + //C.registerListener(C.uint(eventID)) +} + +func (w *linuxWebviewWindow) zoom() { + w.zoomIn() +} + +func (w *linuxWebviewWindow) windowZoom() { + w.zoom() // FIXME> This should be removed +} + +func (w *linuxWebviewWindow) close() { + windowClose(w.window) + if !w.parent.options.HideOnClose { + globalApplication.deleteWindowByID(w.parent.id) + } +} + +func (w *linuxWebviewWindow) zoomIn() { + windowZoomIn(w.webview) +} + +func (w *linuxWebviewWindow) zoomOut() { + windowZoomOut(w.webview) +} + +func (w *linuxWebviewWindow) zoomReset() { + windowZoomSet(w.webview, 0.0) +} + +func (w *linuxWebviewWindow) reload() { + windowReload(w.webview, "wails://") +} + +func (w *linuxWebviewWindow) forceReload() { + w.reload() +} + +func (w *linuxWebviewWindow) center() { + globalApplication.dispatchOnMainThread(func() { + x, y, width, height, _ := windowGetCurrentMonitorGeometry(w.window) + if x == -1 && y == -1 && width == -1 && height == -1 { + return + } + windowWidth, windowHeight := windowGetSize(w.window) + + newX := ((width - int(windowWidth)) / 2) + x + newY := ((height - int(windowHeight)) / 2) + y + + // Place the window at the center of the monitor + windowMove(w.window, newX, newY) + }) +} + +func (w *linuxWebviewWindow) isMinimised() bool { + return windowIsMinimized(w.window) +} + +func (w *linuxWebviewWindow) isMaximised() bool { + return w.syncMainThreadReturningBool(func() bool { + return windowIsMaximized(w.window) + }) +} + +func (w *linuxWebviewWindow) isFullscreen() bool { + return w.syncMainThreadReturningBool(func() bool { + return windowIsFullscreen(w.window) + }) +} + +func (w *linuxWebviewWindow) syncMainThreadReturningBool(fn func() bool) bool { + var wg sync.WaitGroup + wg.Add(1) + var result bool + globalApplication.dispatchOnMainThread(func() { + result = fn() + wg.Done() + }) + wg.Wait() + return result +} + +func (w *linuxWebviewWindow) restore() { + // restore window to normal size + // FIXME: never called! - remove from webviewImpl interface +} + +func (w *linuxWebviewWindow) execJS(js string) { + windowExecJS(w.webview, js) +} + +func (w *linuxWebviewWindow) setURL(uri string) { + if uri != "" { + url, err := url.Parse(uri) + if err == nil && url.Scheme == "" && url.Host == "" { + // TODO handle this in a central location, the scheme and host might be platform dependant. + url.Scheme = "wails" + url.Host = "wails" + uri = url.String() + } + } + windowSetURL(w.webview, uri) +} + +func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { + windowSetKeepAbove(w.window, alwaysOnTop) +} + +func newWindowImpl(parent *WebviewWindow) *linuxWebviewWindow { + // (*C.struct__GtkWidget)(m.native) + //var menubar *C.struct__GtkWidget + return &linuxWebviewWindow{ + application: getNativeApplication().application, + parent: parent, + // menubar: menubar, + } +} + +func (w *linuxWebviewWindow) setTitle(title string) { + if !w.parent.options.Frameless { + windowSetTitle(w.window, title) + } +} + +func (w *linuxWebviewWindow) setSize(width, height int) { + windowResize(w.window, width, height) +} + +func (w *linuxWebviewWindow) setMinMaxSize(minWidth, minHeight, maxWidth, maxHeight int) { + if minWidth == 0 { + minWidth = -1 + } + if minHeight == 0 { + minHeight = -1 + } + if maxWidth == 0 { + maxWidth = -1 + } + if maxHeight == 0 { + maxHeight = -1 + } + windowSetGeometryHints(w.window, minWidth, minHeight, maxWidth, maxHeight) +} + +func (w *linuxWebviewWindow) setMinSize(width, height int) { + w.setMinMaxSize(width, height, w.parent.options.MaxWidth, w.parent.options.MaxHeight) +} + +func (w *linuxWebviewWindow) setMaxSize(width, height int) { + w.setMinMaxSize(w.parent.options.MinWidth, w.parent.options.MinHeight, width, height) +} + +func (w *linuxWebviewWindow) setResizable(resizable bool) { + windowSetResizable(w.window, resizable) +} + +func (w *linuxWebviewWindow) toggleDevTools() { + windowToggleDevTools(w.webview) +} + +func (w *linuxWebviewWindow) size() (int, int) { + /* var width, height C.int + var wg sync.WaitGroup + wg.Add(1) + globalApplication.dispatchOnMainThread(func() { + + C.gtk_window_get_size((*C.GtkWindow)(w.window), &width, &height) + wg.Done() + }) + wg.Wait() + return int(width), int(height) + */ + // Does this need to be guarded? + return windowGetSize(w.window) +} + +func (w *linuxWebviewWindow) setPosition(x, y int) { + mx, my, _, _, _ := windowGetCurrentMonitorGeometry(w.window) + globalApplication.dispatchOnMainThread(func() { + windowMove(w.window, x+mx, y+my) + }) +} + +func (w *linuxWebviewWindow) width() int { + width, _ := w.size() + return width +} + +func (w *linuxWebviewWindow) height() int { + _, height := w.size() + return height +} + +func (w *linuxWebviewWindow) run() { + for eventId := range w.parent.eventListeners { + w.on(eventId) + } + + app := getNativeApplication() + menu := app.applicationMenu + + globalApplication.dispatchOnMainThread(func() { + w.window, w.webview = windowNew(app.application, menu, w.parent.id, 1) + app.registerWindow(w.window, w.parent.id) // record our mapping + w.connectSignals() + if w.parent.options.EnableDragAndDrop { + w.enableDND() + } + w.setTitle(w.parent.options.Title) + w.setAlwaysOnTop(w.parent.options.AlwaysOnTop) + w.setResizable(!w.parent.options.DisableResize) + // only set min/max size if actually set + if w.parent.options.MinWidth != 0 && + w.parent.options.MinHeight != 0 && + w.parent.options.MaxWidth != 0 && + w.parent.options.MaxHeight != 0 { + w.setMinMaxSize( + w.parent.options.MinWidth, + w.parent.options.MinHeight, + w.parent.options.MaxWidth, + w.parent.options.MaxHeight, + ) + } + w.setSize(w.parent.options.Width, w.parent.options.Height) + w.setZoom(w.parent.options.Zoom) + w.setBackgroundColour(w.parent.options.BackgroundColour) + w.setFrameless(w.parent.options.Frameless) + + if w.parent.options.X != 0 || w.parent.options.Y != 0 { + w.setPosition(w.parent.options.X, w.parent.options.Y) + } else { + fmt.Println("attempting to set in the center") + w.center() + } + switch w.parent.options.StartState { + case WindowStateMaximised: + w.maximise() + case WindowStateMinimised: + w.minimise() + case WindowStateFullscreen: + w.fullscreen() + } + + if w.parent.options.URL != "" { + w.setURL(w.parent.options.URL) + } + // We need to wait for the HTML to load before we can execute the javascript + // FIXME: What event is this? DomReady? + w.parent.On(events.Mac.WebViewDidFinishNavigation, func(_ *WindowEventContext) { + if w.parent.options.JS != "" { + w.execJS(w.parent.options.JS) + } + if w.parent.options.CSS != "" { + js := fmt.Sprintf("(function() { var style = document.createElement('style'); style.appendChild(document.createTextNode('%s')); document.head.appendChild(style); })();", w.parent.options.CSS) + w.execJS(js) + } + }) + if w.parent.options.HTML != "" { + w.setHTML(w.parent.options.HTML) + } + if !w.parent.options.Hidden { + w.show() + if w.parent.options.X != 0 || w.parent.options.Y != 0 { + w.setPosition(w.parent.options.X, w.parent.options.Y) + } else { + w.center() // needs to be queued until after GTK starts up! + } + } + }) +} + +func (w *linuxWebviewWindow) setTransparent() { + windowSetTransparent(w.window) +} + +func (w *linuxWebviewWindow) setBackgroundColour(colour RGBA) { + if colour.Alpha != 0 { + w.setTransparent() + } + windowSetBackgroundColour(w.webview, colour) +} + +func (w *linuxWebviewWindow) position() (int, int) { + var x, y int + var wg sync.WaitGroup + wg.Add(1) + go globalApplication.dispatchOnMainThread(func() { + x, y = windowGetPosition(w.window) + wg.Done() + }) + wg.Wait() + return x, y +} + +func (w *linuxWebviewWindow) destroy() { + windowDestroy(w.window) +} + +func (w *linuxWebviewWindow) setHTML(html string) { + windowSetHTML(w.webview, html) +} + +func (w *linuxWebviewWindow) startResize(border string) error { + // FIXME: what do we need to do here? + return nil +} + +func (w *linuxWebviewWindow) nativeWindowHandle() uintptr { + return uintptr(w.window) +}