[linux/cgo] initial implementation

This commit is contained in:
Travis McLane
2023-05-10 09:10:03 -05:00
parent 93a4b823c8
commit 68cfd130d3
10 changed files with 2141 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
//go:build linux && !purego
package application
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <webkit2/webkit2.h>
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
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)))
}
+33
View File
@@ -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{}
}
+300
View File
@@ -0,0 +1,300 @@
//go:build linux
package application
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <stdio.h>
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")
}
}
+51
View File
@@ -0,0 +1,51 @@
//go:build linux
package application
/*
#cgo linux pkg-config: gtk+-3.0
#include <stdio.h>
#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()
}
+191
View File
@@ -0,0 +1,191 @@
//go:build linux
package application
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <gtk/gtk.h>
#include <gdk/gdk.h>
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
}
+397
View File
@@ -0,0 +1,397 @@
//go:build linux
package application
import (
"fmt"
"runtime"
"unsafe"
)
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <stdio.h>
#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()
}
})
}
+6
View File
@@ -0,0 +1,6 @@
package application
// LinuxWindow contains macOS specific options
type LinuxWindow struct {
ShowApplicationMenu bool
}
+94
View File
@@ -0,0 +1,94 @@
//go:build linux
package application
/*
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <stdlib.h>
#include <stdbool.h>
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()
}
+82
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff