[v3 linux] initial linux implementation

This commit is contained in:
Travis McLane
2023-06-23 21:50:51 -05:00
parent 94e1ec91ad
commit bef454f950
11 changed files with 2407 additions and 0 deletions
+167
View File
@@ -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)))
}
*/
+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, bool) {
clipboardLock.RLock()
defer clipboardLock.RUnlock()
// clipboardText := C.getClipboardText()
// result := C.GoString(clipboardText)
return "", false
}
func newClipboardImpl() *linuxClipboard {
return &linuxClipboard{}
}
+139
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+18
View File
@@ -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()
}
+114
View File
@@ -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
}
+364
View File
@@ -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()
}
})
}
+6
View File
@@ -0,0 +1,6 @@
package application
// LinuxWindow contains macOS specific options
type LinuxWindow struct {
ShowApplicationMenu bool
}
+29
View File
@@ -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()
}
+90
View File
@@ -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)
}
+492
View File
@@ -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)
}