Skip to content

Conversation

@ystepanoff
Copy link
Contributor

@ystepanoff ystepanoff commented Oct 5, 2025

Description:

This addresses #1648, adding the ability to recursively search the menu items

Usage example:

package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Fyne Menu Search Example")

	selectedLabel := widget.NewLabel("Select a submenu item")
	w.SetContent(container.NewCenter(selectedLabel))

	makeItem := func(name string) *fyne.MenuItem {
		return fyne.NewMenuItem(name, func() {
			selectedLabel.SetText("Selected: " + name)
		})
	}

	subProjectA := fyne.NewMenu("Project A",
		makeItem("Subproject A1"),
		makeItem("Subproject A2"),
		makeItem("Subproject A3"),
	)

	fileRecent := fyne.NewMenu("Recent",
		&fyne.MenuItem{
			Label:     "Project A",
			ChildMenu: subProjectA,
		},
		makeItem("Project B"),
		makeItem("Project C"),
	)

	file := fyne.NewMenu("File",
		makeItem("New"),
		makeItem("Open"),
		makeItem("Save"),
		fyne.NewMenuItemSeparator(),
		&fyne.MenuItem{
			Label:     "Open Recent",
			ChildMenu: fileRecent,
		},
		fyne.NewMenuItemSeparator(),
		makeItem("Quit"),
	)

	file.Items = append(file.Items, fyne.NewMenuItem("Search", func() {
		pop := widget.NewPopUpMenuWithSearch(file, w.Canvas())
		pop.ShowAtPosition(fyne.NewPos(20, 40))
	}))

	w.SetMainMenu(fyne.NewMainMenu(file))

	w.Resize(fyne.NewSize(600, 400))
	w.ShowAndRun()
}

Fixes #1648

Checklist:

  • Tests included.
  • Lint and formatter run with no errors.
  • Tests all pass.

@ystepanoff ystepanoff mentioned this pull request Oct 5, 2025
@andydotxyz
Copy link
Member

Thanks so much for looking at this cool feature.

I don't think it should pop up a whole new menu in a different location, we should be able to do it inline in the main menu as it is drawn already.

The main issue with this approach is that the search should cover /all/ menus in a MainMenu not just the one with the search item in it. Rather than filtering a single menu it could show search results that map back to menu items anywhere in the bar. For example https://www.google.com/search?q=macos+menu+search

@coveralls
Copy link

coveralls commented Oct 5, 2025

Coverage Status

coverage: 60.981% (-0.005%) from 60.986%
when pulling 72f79c8 on ystepanoff:menu-search
into e052430 on fyne-io:develop.

@ystepanoff
Copy link
Contributor Author

ystepanoff commented Oct 19, 2025

@andydotxyz one eternity and a lengthy ChatGPT conversation about ObjectiveC/glfw mappings later, I believe I managed to replicate the macOS menu search to some extent. Not sure this is needed in native macOS menus, but it was a good exercise so I included that option in this PR. The main usage example I have is here:

// go run -tags no_native_menus main.go

package main

import (
	"log"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Menu Search Example")
	myWindow.Resize(fyne.NewSize(800, 600))

	output := widget.NewMultiLineEntry()
	output.SetPlaceHolder("Menu actions will appear here...")

	logAction := func(action string) {
		current := output.Text
		if current != "" {
			current += "\n"
		}
		output.SetText(current + action)
		log.Println(action)
	}

	fileMenu := fyne.NewMenu("File",
		fyne.NewMenuItem("New Document", func() {
			logAction("Created new document")
		}),
		fyne.NewMenuItem("Open...", func() {
			logAction("Open dialog would appear here")
		}),
		&fyne.MenuItem{
			Label: "Recent Files",
			ChildMenu: fyne.NewMenu("",
				fyne.NewMenuItem("project.txt", func() {
					logAction("Opened project.txt")
				}),
				fyne.NewMenuItem("notes.md", func() {
					logAction("Opened notes.md")
				}),
				fyne.NewMenuItem("data.json", func() {
					logAction("Opened data.json")
				}),
			),
		},
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Save", func() {
			logAction("Document saved")
		}),
		fyne.NewMenuItem("Save As...", func() {
			logAction("Save As dialog would appear here")
		}),
		fyne.NewMenuItem("Export to PDF", func() {
			logAction("Exported to PDF")
		}),
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Close", func() {
			logAction("Document closed")
		}),
		fyne.NewMenuItem("Quit", func() {
			myApp.Quit()
		}),
	)

	editMenu := fyne.NewMenu("Edit",
		fyne.NewMenuItem("Undo", func() {
			logAction("Undo last action")
		}),
		fyne.NewMenuItem("Redo", func() {
			logAction("Redo last undone action")
		}),
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Cut", func() {
			logAction("Cut selection to clipboard")
		}),
		fyne.NewMenuItem("Copy", func() {
			logAction("Copied selection to clipboard")
		}),
		fyne.NewMenuItem("Paste", func() {
			logAction("Pasted from clipboard")
		}),
		fyne.NewMenuItem("Select All", func() {
			logAction("Selected all content")
		}),
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Find...", func() {
			logAction("Find dialog would appear here")
		}),
		fyne.NewMenuItem("Find and Replace...", func() {
			logAction("Find and Replace dialog would appear here")
		}),
	)

	viewMenu := fyne.NewMenu("View",
		fyne.NewMenuItem("Zoom In", func() {
			logAction("Zoomed in")
		}),
		fyne.NewMenuItem("Zoom Out", func() {
			logAction("Zoomed out")
		}),
		fyne.NewMenuItem("Actual Size", func() {
			logAction("Reset to actual size")
		}),
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Full Screen", func() {
			myWindow.SetFullScreen(!myWindow.FullScreen())
			if myWindow.FullScreen() {
				logAction("Entered full screen mode")
			} else {
				logAction("Exited full screen mode")
			}
		}),
		fyne.NewMenuItem("Show Toolbar", func() {
			logAction("Toolbar toggled")
		}),
		fyne.NewMenuItem("Show Sidebar", func() {
			logAction("Sidebar toggled")
		}),
		fyne.NewMenuItem("Show Status Bar", func() {
			logAction("Status bar toggled")
		}),
	)

	textFormatMenu := fyne.NewMenu("Text Style",
		fyne.NewMenuItem("Bold", func() {
			logAction("Applied bold formatting")
		}),
		fyne.NewMenuItem("Italic", func() {
			logAction("Applied italic formatting")
		}),
		fyne.NewMenuItem("Underline", func() {
			logAction("Applied underline formatting")
		}),
		fyne.NewMenuItem("Strikethrough", func() {
			logAction("Applied strikethrough formatting")
		}),
	)

	alignmentMenu := fyne.NewMenu("Alignment",
		fyne.NewMenuItem("Align Left", func() {
			logAction("Aligned text to left")
		}),
		fyne.NewMenuItem("Center", func() {
			logAction("Centered text")
		}),
		fyne.NewMenuItem("Align Right", func() {
			logAction("Aligned text to right")
		}),
		fyne.NewMenuItem("Justify", func() {
			logAction("Justified text")
		}),
	)

	formatMenu := fyne.NewMenu("Format",
		fyne.NewMenuItem("Font...", func() {
			logAction("Font selection dialog would appear here")
		}),
		&fyne.MenuItem{Label: "Text Style", ChildMenu: textFormatMenu},
		&fyne.MenuItem{Label: "Alignment", ChildMenu: alignmentMenu},
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Paragraph...", func() {
			logAction("Paragraph settings dialog would appear here")
		}),
		&fyne.MenuItem{
			Label: "Line Spacing",
			ChildMenu: fyne.NewMenu("",
				fyne.NewMenuItem("Single", func() {
					logAction("Set line spacing to single")
				}),
				fyne.NewMenuItem("1.5", func() {
					logAction("Set line spacing to 1.5")
				}),
				fyne.NewMenuItem("Double", func() {
					logAction("Set line spacing to double")
				}),
			),
		},
	)

	toolsMenu := fyne.NewMenu("Tools",
		fyne.NewMenuItem("Spell Check", func() {
			logAction("Running spell check...")
		}),
		fyne.NewMenuItem("Grammar Check", func() {
			logAction("Running grammar check...")
		}),
		fyne.NewMenuItem("Word Count", func() {
			logAction("Word count: 1,234 words")
		}),
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Translate", func() {
			logAction("Translation tool opened")
		}),
		fyne.NewMenuItem("Research", func() {
			logAction("Research panel opened")
		}),
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Preferences...", func() {
			logAction("Preferences window would open here")
		}),
	)

	windowMenu := fyne.NewMenu("Window",
		fyne.NewMenuItem("Minimize", func() {
			logAction("Window minimized")
		}),
		fyne.NewMenuItem("Zoom", func() {
			logAction("Window zoomed")
		}),
		fyne.NewMenuItemSeparator(),
		fyne.NewMenuItem("Bring All to Front", func() {
			logAction("All windows brought to front")
		}),
	)

	mainMenu := fyne.NewMainMenuWithSearch(
		"Search...",
		fileMenu,
		editMenu,
		viewMenu,
		formatMenu,
		toolsMenu,
		windowMenu,
	)

	myWindow.SetMainMenu(mainMenu)

	instructions := widget.NewCard(
		"How to Use Menu Search",
		"Try the search feature to quickly find any menu item",
		container.NewVBox(
			widget.NewLabel("1. Click 'Help' in the menu bar"),
			widget.NewLabel("2. Click 'Search...' at the top"),
			widget.NewLabel("3. Type to search across ALL menus"),
			widget.NewLabel("4. Click a result or press Enter to activate it"),
			widget.NewSeparator(),
			widget.NewLabelWithStyle("Try these searches:", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
			widget.NewLabel("• 'save' - finds all save-related options"),
			widget.NewLabel("• 'zoom' - finds zoom commands in View and Window menus"),
			widget.NewLabel("• 'bold' - finds Format → Text Style → Bold"),
			widget.NewLabel("• 'align' - finds all alignment options"),
			widget.NewLabel("• 'pdf' - finds Export to PDF"),
			widget.NewLabel("• 'spacing' - finds line spacing options"),
		),
	)

	clearButton := widget.NewButton("Clear Output", func() {
		output.SetText("")
	})

	content := container.NewBorder(
		instructions,
		clearButton,
		nil,
		nil,
		container.NewScroll(output),
	)

	myWindow.SetContent(content)

	myWindow.ShowAndRun()
}

@andydotxyz
Copy link
Member

andydotxyz commented Oct 20, 2025

Amazing thanks. I see this is coming along nicely.

However I think there is a misunderstanding about macOS - this feature is included already in the Help menu. What we need is to make that available for all... (see how the "fyne demo" menu shows on macOS already)

Screenshot 2025-10-20 at 12 25 26

So moving this "non-native" menu work to help (from file) and displaying it automatically would be perfect. But for macOS native I think it should not need any new work or feature.

@ystepanoff
Copy link
Contributor Author

However I think there is a misunderstanding about macOS - this feature is included already in the Help menu. What we need is to make that available for all... (see how the "fyne demo" menu shows on macOS already)
So moving this "non-native" menu work to help (from file) and displaying it automatically would be perfect. But for macOS native I think it should not need any new work or feature.

Understood, this also works for non-native menus currently, and also there is AddSearchToMenu() func that allows to add search field into any menu, but I guess we can default it to the Help menu. I'll strip out the native macOS nonsense and then we'll hopefully end up with a neat enough PR 🙂
Screenshot 2025-10-20 at 13 12 48

@ystepanoff
Copy link
Contributor Author

@andydotxyz this now should match your description, can test this by running this example with

go run -tags no_native_menus main.go 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants