Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: submenus #905

Merged
merged 18 commits into from
Apr 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion cmd/fyne_demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,17 @@ func main() {

w := a.NewWindow("Fyne Demo")

newItem := fyne.NewMenuItem("New", func() { fmt.Println("Menu New") })
newItem := fyne.NewMenuItem("New", nil)
otherItem := fyne.NewMenuItem("Other", nil)
otherItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("Project", func() { fmt.Println("Menu New->Other->Project") }),
fyne.NewMenuItem("Mail", func() { fmt.Println("Menu New->Other->Mail") }),
)
newItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("File", func() { fmt.Println("Menu New->File") }),
fyne.NewMenuItem("Directory", func() { fmt.Println("Menu New->Directory") }),
otherItem,
)
settingsItem := fyne.NewMenuItem("Settings", func() { fmt.Println("Menu Settings") })

cutItem := fyne.NewMenuItem("Cut", func() { fmt.Println("Menu Cut") })
Expand Down
21 changes: 11 additions & 10 deletions cmd/fyne_demo/screens/advanced.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,20 @@ func AdvancedScreen(win fyne.Window) fyne.CanvasObject {
})
}

return widget.NewHBox(widget.NewVBox(screen,
widget.NewButton("Custom Theme", func() {
fyne.CurrentApp().Settings().SetTheme(newCustomTheme())
}),
widget.NewButton("Fullscreen", func() {
win.SetFullScreen(!win.FullScreen())
}),
),

return widget.NewHBox(
widget.NewVBox(screen,
widget.NewButton("Custom Theme", func() {
fyne.CurrentApp().Settings().SetTheme(newCustomTheme())
}),
widget.NewButton("Fullscreen", func() {
win.SetFullScreen(!win.FullScreen())
}),
),
fyne.NewContainerWithLayout(layout.NewBorderLayout(label, nil, nil, nil),
label,
fyne.NewContainerWithLayout(layout.NewGridLayout(2),
generic, desk,
),
))
),
)
}
27 changes: 27 additions & 0 deletions cmd/fyne_demo/screens/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,28 @@ func makeButtonTab() fyne.Widget {
grid.ShowLineNumbers = true
grid.ShowWhitespace = true

shareItem := fyne.NewMenuItem("Share via", nil)
shareItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("Twitter", func() { fmt.Println("context menu Share->Twitter") }),
fyne.NewMenuItem("Reddit", func() { fmt.Println("context menu Share->Reddit") }),
)
menuLabel := &contextMenuButton{
widget.NewButton("tap me for pop-up menu with submenus", nil),
fyne.NewMenu("",
fyne.NewMenuItem("Copy", func() { fmt.Println("context menu copy") }),
shareItem,
),
}

return widget.NewVBox(
widget.NewButton("Button (text only)", func() { fmt.Println("tapped text button") }),
widget.NewButtonWithIcon("Button (text & icon)", theme.ConfirmIcon(), func() { fmt.Println("tapped text & icon button") }),
disabled,
grid,
layout.NewSpacer(),
layout.NewSpacer(),
menuLabel,
layout.NewSpacer(),
)
}

Expand Down Expand Up @@ -273,3 +290,13 @@ func WidgetScreen() fyne.CanvasObject {
),
)
}

type contextMenuButton struct {
*widget.Button
menu *fyne.Menu
}

// Tapped satisfies the fyne.Tappable interface.
func (b *contextMenuButton) Tapped(e *fyne.PointEvent) {
widget.ShowPopUpMenuAtPosition(b.menu, fyne.CurrentApp().Driver().CanvasForObject(b), e.AbsolutePosition)
}
202 changes: 149 additions & 53 deletions internal/driver/glfw/menu_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,135 @@ import (
#include <AppKit/AppKit.h>

// Using void* as type for pointers is a workaround. See https://github.com/golang/go/issues/12065.
const void* darwinAppMenu();
const void* createDarwinMenu(const char* label);
void insertDarwinMenuItem(const void* menu, const char* label, int id, int index, bool isSeparator);
void assignDarwinSubmenu(const void*, const void*);
void completeDarwinMenu(void* menu, bool prepend);
const void* createDarwinMenu(const char* label);
const void* darwinAppMenu();
const void* insertDarwinMenuItem(const void* menu, const char* label, int id, int index, bool isSeparator);

// Used for tests.
const void* test_darwinMainMenu();
const void* test_NSMenu_itemAtIndex(const void*, NSInteger);
NSInteger test_NSMenu_numberOfItems(const void*);
void test_NSMenu_performActionForItemAtIndex(const void*, NSInteger);
const char* test_NSMenu_title(const void*);
bool test_NSMenu_isSeparatorItem(const void*);
const void* test_NSMenuItem_submenu(const void*);
const char* test_NSMenuItem_title(const void*);
*/
import "C"

var callbacks []func()
var ecb func(string)

//export menuCallback
func menuCallback(id int) {
callbacks[id]()
func addNativeMenu(w *window, menu *fyne.Menu, nextItemID int, prepend bool) int {
menu, nextItemID = handleSpecialItems(w, menu, nextItemID, true)

containsItems := false
for _, item := range menu.Items {
if !item.IsSeparator {
containsItems = true
break
}
}
if !containsItems {
return nextItemID
}

nsMenu, nextItemID := createNativeMenu(w, menu, nextItemID)
C.completeDarwinMenu(nsMenu, C.bool(prepend))
return nextItemID
}

func addNativeSubMenu(w *window, nsParentMenuItem unsafe.Pointer, menu *fyne.Menu, nextItemID int) int {
nsMenu, nextItemID := createNativeMenu(w, menu, nextItemID)
C.assignDarwinSubmenu(nsParentMenuItem, nsMenu)
return nextItemID
}

func createNativeMenu(w *window, menu *fyne.Menu, nextItemID int) (unsafe.Pointer, int) {
nsMenu := C.createDarwinMenu(C.CString(menu.Label))
for _, item := range menu.Items {
nsMenuItem := C.insertDarwinMenuItem(
nsMenu,
C.CString(item.Label),
C.int(nextItemID),
C.int(-1),
C.bool(item.IsSeparator),
)
nextItemID = registerCallback(w, item, nextItemID)
if item.ChildMenu != nil {
nextItemID = addNativeSubMenu(w, nsMenuItem, item.ChildMenu, nextItemID)
}
}
return nsMenu, nextItemID
}

//export exceptionCallback
func exceptionCallback(e *C.char) {
msg := C.GoString(e)
if ecb == nil {
panic("unhandled Obj-C exception: " + msg)
}
ecb(msg)
}

func handleSpecialItems(w *window, menu *fyne.Menu, nextItemID int, addSeparator bool) (*fyne.Menu, int) {
for i, item := range menu.Items {
if item.Label == "Settings" || item.Label == "Settings…" || item.Label == "Preferences" || item.Label == "Preferences…" {
items := make([]*fyne.MenuItem, 0, len(menu.Items)-1)
items = append(items, menu.Items[:i]...)
items = append(items, menu.Items[i+1:]...)
menu, nextItemID = handleSpecialItems(w, fyne.NewMenu(menu.Label, items...), nextItemID, false)

C.insertDarwinMenuItem(
C.darwinAppMenu(),
C.CString(item.Label),
C.int(nextItemID),
C.int(1),
C.bool(false),
)
if addSeparator {
C.insertDarwinMenuItem(
C.darwinAppMenu(),
C.CString(""),
C.int(nextItemID),
C.int(1),
C.bool(true),
)
}
nextItemID = registerCallback(w, item, nextItemID)
break
}
}
return menu, nextItemID
}

func registerCallback(w *window, item *fyne.MenuItem, nextItemID int) int {
if !item.IsSeparator {
callbacks = append(callbacks, func() {
if item.Action != nil {
w.queueEvent(item.Action)
}
})
nextItemID++
}
return nextItemID
}

func setExceptionCallback(cb func(string)) {
ecb = cb
}

func hasNativeMenu() bool {
return true
}

//export menuCallback
func menuCallback(id int) {
callbacks[id]()
}

func setupNativeMenu(w *window, main *fyne.MainMenu) {
nextItemID := 0
var helpMenu *fyne.Menu
Expand All @@ -49,54 +160,39 @@ func setupNativeMenu(w *window, main *fyne.MainMenu) {
}
}

func addNativeMenu(w *window, menu *fyne.Menu, nextItemID int, prepend bool) int {
createMenu := false
for _, item := range menu.Items {
if item.Label != "Settings" {
createMenu = true
break
}
}
//
// Test support methods
// These are needed because CGo is not supported inside test files.
//

var nsMenu unsafe.Pointer
if createMenu {
nsMenu = C.createDarwinMenu(C.CString(menu.Label))
}
func testDarwinMainMenu() unsafe.Pointer {
return C.test_darwinMainMenu()
}

for _, item := range menu.Items {
if item.Label == "Settings" {
C.insertDarwinMenuItem(
C.darwinAppMenu(),
C.CString(""),
C.int(nextItemID),
C.int(1),
C.bool(true),
)
C.insertDarwinMenuItem(
C.darwinAppMenu(),
C.CString(item.Label),
C.int(nextItemID),
C.int(2),
C.bool(false),
)
} else {
C.insertDarwinMenuItem(
nsMenu,
C.CString(item.Label),
C.int(nextItemID),
C.int(-1),
C.bool(item.IsSeparator),
)
}
if !item.IsSeparator {
action := item.Action // catch
callbacks = append(callbacks, func() { w.queueEvent(action) })
nextItemID++
}
}
func testNSMenuItemAtIndex(m unsafe.Pointer, i int) unsafe.Pointer {
return C.test_NSMenu_itemAtIndex(m, C.long(i))
}

if nsMenu != nil {
C.completeDarwinMenu(nsMenu, C.bool(prepend))
}
return nextItemID
func testNSMenuNumberOfItems(m unsafe.Pointer) int {
return int(C.test_NSMenu_numberOfItems(m))
}

func testNSMenuPerformActionForItemAtIndex(m unsafe.Pointer, i int) {
C.test_NSMenu_performActionForItemAtIndex(m, C.long(i))
}

func testNSMenuTitle(m unsafe.Pointer) string {
return C.GoString(C.test_NSMenu_title(m))
}

func testNSMenuIsSeparatorItem(i unsafe.Pointer) bool {
return bool(C.test_NSMenu_isSeparatorItem(i))
}

func testNSMenuItemSubmenu(i unsafe.Pointer) unsafe.Pointer {
return C.test_NSMenuItem_submenu(i)
}

func testNSMenuItemTitle(i unsafe.Pointer) string {
return C.GoString(C.test_NSMenuItem_title(i))
}
Loading