mirror of
https://github.com/Hopiu/micro.git
synced 2026-03-16 22:10:26 +00:00
461 lines
12 KiB
Go
461 lines
12 KiB
Go
package display
|
|
|
|
import (
|
|
runewidth "github.com/mattn/go-runewidth"
|
|
"github.com/micro-editor/tcell/v2"
|
|
"github.com/zyedidia/micro/v2/internal/config"
|
|
"github.com/zyedidia/micro/v2/internal/screen"
|
|
"github.com/zyedidia/micro/v2/internal/util"
|
|
)
|
|
|
|
// MenuItem represents a single menu item
|
|
type MenuItem struct {
|
|
Name string
|
|
Action string
|
|
Hotkey rune
|
|
Enabled bool
|
|
}
|
|
|
|
// MenuWindow displays a horizontal menu bar at the top of the screen
|
|
type MenuWindow struct {
|
|
MenuItems []MenuItem
|
|
Active int
|
|
Width int
|
|
Height int
|
|
Y int
|
|
open bool // whether a menu is currently open
|
|
dropdownMenus map[string]*DropdownMenu // dropdown menus for each menu item
|
|
}
|
|
|
|
// NewMenuWindow creates a new MenuWindow
|
|
func NewMenuWindow(x, y, w, h int) *MenuWindow {
|
|
mw := new(MenuWindow)
|
|
mw.MenuItems = []MenuItem{
|
|
{Name: "File", Action: "file", Hotkey: 'i', Enabled: true}, // Alt+i (was F)
|
|
{Name: "Edit", Action: "edit", Hotkey: 'd', Enabled: true}, // Alt+d (was E)
|
|
{Name: "View", Action: "view", Hotkey: 'w', Enabled: true}, // Alt+w (was V)
|
|
{Name: "Search", Action: "search", Hotkey: 's', Enabled: true}, // Alt+s (was S)
|
|
{Name: "Tools", Action: "tools", Hotkey: 't', Enabled: true}, // Alt+t (was T)
|
|
{Name: "Help", Action: "help", Hotkey: 'h', Enabled: true}, // Alt+h (was H)
|
|
}
|
|
mw.Active = -1 // No active menu by default
|
|
mw.Width = w
|
|
mw.Height = h
|
|
mw.Y = y
|
|
mw.open = false // Menu is closed by default
|
|
mw.dropdownMenus = make(map[string]*DropdownMenu)
|
|
|
|
// Initialize dropdown menus
|
|
mw.initializeDropdownMenus()
|
|
|
|
return mw
|
|
}
|
|
|
|
// initializeDropdownMenus sets up the dropdown menus for each main menu item
|
|
func (w *MenuWindow) initializeDropdownMenus() {
|
|
// File menu
|
|
fileMenu := NewDropdownMenu()
|
|
fileMenu.SetItems([]DropdownItem{
|
|
{Text: "New", Action: "NewTab", Hotkey: 'N', Enabled: true},
|
|
{Text: "Open", Action: "Open", Hotkey: 'O', Enabled: true},
|
|
{Separator: true},
|
|
{Text: "Save", Action: "Save", Hotkey: 'S', Enabled: true},
|
|
{Text: "Save As", Action: "SaveAs", Hotkey: 'A', Enabled: true},
|
|
{Separator: true},
|
|
{Text: "Quit", Action: "Quit", Hotkey: 'Q', Enabled: true},
|
|
})
|
|
w.dropdownMenus["file"] = fileMenu
|
|
|
|
// Edit menu
|
|
editMenu := NewDropdownMenu()
|
|
editMenu.SetItems([]DropdownItem{
|
|
{Text: "Undo", Action: "Undo", Hotkey: 'U', Enabled: true},
|
|
{Text: "Redo", Action: "Redo", Hotkey: 'R', Enabled: true},
|
|
{Separator: true},
|
|
{Text: "Cut", Action: "Cut", Hotkey: 'X', Enabled: true},
|
|
{Text: "Copy", Action: "Copy", Hotkey: 'C', Enabled: true},
|
|
{Text: "Paste", Action: "Paste", Hotkey: 'V', Enabled: true},
|
|
})
|
|
w.dropdownMenus["edit"] = editMenu
|
|
|
|
// View menu
|
|
viewMenu := NewDropdownMenu()
|
|
viewMenu.SetItems([]DropdownItem{
|
|
{Text: "Split Horizontal", Action: "HSplit", Hotkey: 'H', Enabled: true},
|
|
{Text: "Split Vertical", Action: "VSplit", Hotkey: 'V', Enabled: true},
|
|
{Separator: true},
|
|
{Text: "Toggle Line Numbers", Action: "ToggleRuler", Hotkey: 'L', Enabled: true},
|
|
})
|
|
w.dropdownMenus["view"] = viewMenu
|
|
|
|
// Search menu
|
|
searchMenu := NewDropdownMenu()
|
|
searchMenu.SetItems([]DropdownItem{
|
|
{Text: "Find", Action: "Find", Hotkey: 'F', Enabled: true},
|
|
{Text: "Find Next", Action: "FindNext", Hotkey: 'N', Enabled: true},
|
|
{Text: "Find Previous", Action: "FindPrevious", Hotkey: 'P', Enabled: true},
|
|
{Separator: true},
|
|
{Text: "Replace", Action: "Replace", Hotkey: 'R', Enabled: true},
|
|
})
|
|
w.dropdownMenus["search"] = searchMenu
|
|
|
|
// Tools menu
|
|
toolsMenu := NewDropdownMenu()
|
|
toolsMenu.SetItems([]DropdownItem{
|
|
{Text: "Command Palette", Action: "CommandMode", Hotkey: 'C', Enabled: true},
|
|
{Text: "Plugin Manager", Action: "PluginInstall", Hotkey: 'P', Enabled: true},
|
|
})
|
|
w.dropdownMenus["tools"] = toolsMenu
|
|
|
|
// Help menu
|
|
helpMenu := NewDropdownMenu()
|
|
helpMenu.SetItems([]DropdownItem{
|
|
{Text: "Help", Action: "ToggleHelp", Hotkey: 'H', Enabled: true},
|
|
{Text: "Key Bindings", Action: "ShowKey", Hotkey: 'K', Enabled: true},
|
|
{Separator: true},
|
|
{Text: "About", Action: "ShowAbout", Hotkey: 'A', Enabled: true},
|
|
})
|
|
w.dropdownMenus["help"] = helpMenu
|
|
}
|
|
|
|
// Resize adjusts the menu window size
|
|
func (w *MenuWindow) Resize(width, height int) {
|
|
w.Width = width
|
|
}
|
|
|
|
// SetActive sets the active menu item
|
|
func (w *MenuWindow) SetActive(index int) {
|
|
if index >= 0 && index < len(w.MenuItems) {
|
|
w.Active = index
|
|
} else {
|
|
w.Active = -1
|
|
}
|
|
}
|
|
|
|
// GetActive returns the currently active menu item
|
|
func (w *MenuWindow) GetActive() int {
|
|
return w.Active
|
|
}
|
|
|
|
// IsOpen returns whether a menu is currently open
|
|
func (w *MenuWindow) IsOpen() bool {
|
|
return w.open
|
|
}
|
|
|
|
// SetOpen sets the menu open state
|
|
func (w *MenuWindow) SetOpen(open bool) {
|
|
w.open = open
|
|
|
|
// Show/hide the appropriate dropdown menu
|
|
if open && w.Active >= 0 && w.Active < len(w.MenuItems) {
|
|
activeItem := w.MenuItems[w.Active]
|
|
if dropdown, exists := w.dropdownMenus[activeItem.Action]; exists {
|
|
// Calculate dropdown position
|
|
dropdownX := w.getMenuItemX(w.Active)
|
|
dropdownY := w.Y + 1 // Below the menu bar
|
|
dropdown.Show(dropdownX, dropdownY)
|
|
}
|
|
} else {
|
|
// Hide all dropdown menus
|
|
for _, dropdown := range w.dropdownMenus {
|
|
dropdown.Hide()
|
|
}
|
|
}
|
|
}
|
|
|
|
// getMenuItemX calculates the X position of a menu item
|
|
func (w *MenuWindow) getMenuItemX(index int) int {
|
|
x := 0
|
|
for i := 0; i < index && i < len(w.MenuItems); i++ {
|
|
item := w.MenuItems[i]
|
|
if !item.Enabled {
|
|
continue
|
|
}
|
|
itemWidth := util.StringWidth([]byte(item.Name), util.CharacterCountInString(item.Name), 1)
|
|
x += itemWidth + 2 // +2 for padding
|
|
}
|
|
return x
|
|
}
|
|
|
|
// Display renders the menu bar
|
|
func (w *MenuWindow) Display() {
|
|
if w.Height <= 0 {
|
|
return
|
|
}
|
|
|
|
// Clear the menu bar area
|
|
for x := 0; x < w.Width; x++ {
|
|
screen.SetContent(x, w.Y, ' ', nil, config.DefStyle)
|
|
}
|
|
|
|
x := 0
|
|
for i, item := range w.MenuItems {
|
|
if !item.Enabled {
|
|
continue
|
|
}
|
|
|
|
// Calculate item display text
|
|
displayText := item.Name
|
|
itemWidth := util.StringWidth([]byte(displayText), util.CharacterCountInString(displayText), 1)
|
|
|
|
// Add padding
|
|
padding := 2
|
|
totalWidth := itemWidth + padding
|
|
|
|
// Check if we have space for this item
|
|
if x+totalWidth > w.Width {
|
|
break
|
|
}
|
|
|
|
// Determine style based on active state
|
|
style := config.DefStyle
|
|
if i == w.Active {
|
|
// Highlight active menu item
|
|
style = style.Reverse(true)
|
|
}
|
|
|
|
// Add left padding
|
|
screen.SetContent(x, w.Y, ' ', nil, style)
|
|
x++
|
|
|
|
// Render the menu item text with hotkey highlighting
|
|
for j, r := range displayText {
|
|
charStyle := style
|
|
// Highlight the hotkey character
|
|
if r == item.Hotkey || (r >= 'A' && r <= 'Z' && r-'A'+'a' == item.Hotkey) {
|
|
charStyle = charStyle.Underline(true)
|
|
}
|
|
|
|
screen.SetContent(x, w.Y, r, nil, charStyle)
|
|
x += runewidth.RuneWidth(r)
|
|
|
|
// Handle zero-width characters
|
|
if runewidth.RuneWidth(r) == 0 && j > 0 {
|
|
x = x - 1
|
|
}
|
|
}
|
|
|
|
// Add right padding
|
|
screen.SetContent(x, w.Y, ' ', nil, style)
|
|
x++
|
|
}
|
|
|
|
// Fill remaining space with default style
|
|
for x < w.Width {
|
|
screen.SetContent(x, w.Y, ' ', nil, config.DefStyle)
|
|
x++
|
|
}
|
|
|
|
// Note: Dropdown menus are now displayed separately in the main event loop
|
|
// to ensure they appear on top of all other content
|
|
}
|
|
|
|
// HandleClick handles mouse clicks on the menu bar and dropdowns
|
|
func (w *MenuWindow) HandleClick(x, y int) *DropdownItem {
|
|
// First check if click is on an open dropdown
|
|
if w.open && w.Active >= 0 && w.Active < len(w.MenuItems) {
|
|
activeItem := w.MenuItems[w.Active]
|
|
if dropdown, exists := w.dropdownMenus[activeItem.Action]; exists && dropdown.IsVisible() {
|
|
if clickedItem := dropdown.HandleClick(x, y); clickedItem != nil {
|
|
// A dropdown item was clicked - return it for execution
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
return clickedItem
|
|
}
|
|
// Click might have closed the dropdown, check if we should handle menu bar click
|
|
if !dropdown.IsVisible() {
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if click is on menu bar
|
|
if y != w.Y {
|
|
// Click outside menu bar and dropdown - close any open menu
|
|
if w.open {
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Calculate which menu item was clicked
|
|
currentX := 0
|
|
for i, item := range w.MenuItems {
|
|
if !item.Enabled {
|
|
continue
|
|
}
|
|
|
|
itemWidth := util.StringWidth([]byte(item.Name), util.CharacterCountInString(item.Name), 1) + 2 // +2 for padding
|
|
|
|
if x >= currentX && x < currentX+itemWidth {
|
|
if w.Active == i && w.open {
|
|
// Close if clicking on already open menu
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
} else {
|
|
// Activate and open menu
|
|
w.SetActive(i)
|
|
w.SetOpen(true)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
currentX += itemWidth
|
|
}
|
|
|
|
// Click outside menu items - close any open menu
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
return nil
|
|
}
|
|
|
|
// HandleKey handles keyboard input for menu navigation
|
|
func (w *MenuWindow) HandleKey(key rune) bool {
|
|
// Check for hotkey matches
|
|
for i, item := range w.MenuItems {
|
|
if !item.Enabled {
|
|
continue
|
|
}
|
|
|
|
if key == item.Hotkey || (key >= 'A' && key <= 'Z' && key-'A'+'a' == item.Hotkey) {
|
|
w.SetActive(i)
|
|
w.SetOpen(true)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// HandleKeyNavigation handles keyboard navigation for menu and dropdown
|
|
func (w *MenuWindow) HandleKeyNavigation(key rune, keyCode int) *DropdownItem {
|
|
// If no menu is active, check for Alt+hotkey combinations
|
|
if !w.open || w.Active < 0 {
|
|
// Check for hotkey matches to open menus
|
|
for i, item := range w.MenuItems {
|
|
if !item.Enabled {
|
|
continue
|
|
}
|
|
|
|
if key == item.Hotkey || (key >= 'A' && key <= 'Z' && key-'A'+'a' == item.Hotkey) {
|
|
w.SetActive(i)
|
|
w.SetOpen(true)
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// If a menu is open, handle dropdown navigation
|
|
if w.Active >= 0 && w.Active < len(w.MenuItems) {
|
|
activeItem := w.MenuItems[w.Active]
|
|
if dropdown, exists := w.dropdownMenus[activeItem.Action]; exists && dropdown.IsVisible() {
|
|
// Use tcell key constants for proper key detection
|
|
switch keyCode {
|
|
case int(tcell.KeyEnter):
|
|
selectedItem := dropdown.GetActiveItem()
|
|
if selectedItem != nil && selectedItem.Enabled && !selectedItem.Separator {
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
return selectedItem
|
|
}
|
|
case int(tcell.KeyEscape):
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
return nil
|
|
case int(tcell.KeyUp):
|
|
dropdown.MoveUp()
|
|
return nil
|
|
case int(tcell.KeyDown):
|
|
dropdown.MoveDown()
|
|
return nil
|
|
case int(tcell.KeyLeft):
|
|
w.navigateToPreviousMenu()
|
|
return nil
|
|
case int(tcell.KeyRight):
|
|
w.navigateToNextMenu()
|
|
return nil
|
|
default:
|
|
// Check for dropdown item hotkeys
|
|
for _, item := range dropdown.Items {
|
|
if !item.Separator && item.Enabled {
|
|
if key == item.Hotkey || (key >= 'A' && key <= 'Z' && key-'A'+'a' == item.Hotkey) {
|
|
w.SetActive(-1)
|
|
w.SetOpen(false)
|
|
return &item
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// navigateToPreviousMenu moves to the previous menu item
|
|
func (w *MenuWindow) navigateToPreviousMenu() {
|
|
if w.Active <= 0 {
|
|
// Wrap to last menu
|
|
for i := len(w.MenuItems) - 1; i >= 0; i-- {
|
|
if w.MenuItems[i].Enabled {
|
|
w.SetActive(i)
|
|
w.SetOpen(true)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
// Move to previous enabled menu
|
|
for i := w.Active - 1; i >= 0; i-- {
|
|
if w.MenuItems[i].Enabled {
|
|
w.SetActive(i)
|
|
w.SetOpen(true)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// navigateToNextMenu moves to the next menu item
|
|
func (w *MenuWindow) navigateToNextMenu() {
|
|
if w.Active >= len(w.MenuItems)-1 {
|
|
// Wrap to first menu
|
|
for i := 0; i < len(w.MenuItems); i++ {
|
|
if w.MenuItems[i].Enabled {
|
|
w.SetActive(i)
|
|
w.SetOpen(true)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
// Move to next enabled menu
|
|
for i := w.Active + 1; i < len(w.MenuItems); i++ {
|
|
if w.MenuItems[i].Enabled {
|
|
w.SetActive(i)
|
|
w.SetOpen(true)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetMenuAction returns the action for the currently active menu
|
|
func (w *MenuWindow) GetMenuAction() string {
|
|
if w.Active >= 0 && w.Active < len(w.MenuItems) {
|
|
return w.MenuItems[w.Active].Action
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetActiveDropdown returns the currently active dropdown menu
|
|
func (w *MenuWindow) GetActiveDropdown() *DropdownMenu {
|
|
if w.open && w.Active >= 0 && w.Active < len(w.MenuItems) {
|
|
activeItem := w.MenuItems[w.Active]
|
|
if dropdown, exists := w.dropdownMenus[activeItem.Action]; exists {
|
|
return dropdown
|
|
}
|
|
}
|
|
return nil
|
|
}
|