mirror of
https://github.com/Hopiu/micro.git
synced 2026-03-16 22:10:26 +00:00
Implement menu with dropdown functionality
This commit is contained in:
parent
97b5e3506e
commit
6cb830caa4
5 changed files with 1069 additions and 9 deletions
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/clipboard"
|
"github.com/zyedidia/micro/v2/internal/clipboard"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/display"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/shell"
|
"github.com/zyedidia/micro/v2/internal/shell"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
|
|
@ -462,13 +463,35 @@ func DoEvent() {
|
||||||
|
|
||||||
// Display everything
|
// Display everything
|
||||||
screen.Screen.Fill(' ', config.DefStyle)
|
screen.Screen.Fill(' ', config.DefStyle)
|
||||||
|
|
||||||
|
// Check if dropdown menu is open before displaying content
|
||||||
|
dropdownOpen := action.MenuBar != nil && action.MenuBar.IsOpen()
|
||||||
|
|
||||||
|
// Hide cursor initially (will be shown by panes if no dropdown is open)
|
||||||
screen.Screen.HideCursor()
|
screen.Screen.HideCursor()
|
||||||
|
|
||||||
|
// Display menu bar first (at the top) - but not the dropdown yet
|
||||||
|
if action.MenuBar != nil {
|
||||||
|
action.MenuBar.Display()
|
||||||
|
}
|
||||||
|
|
||||||
action.Tabs.Display()
|
action.Tabs.Display()
|
||||||
for _, ep := range action.MainTab().Panes {
|
for _, ep := range action.MainTab().Panes {
|
||||||
ep.Display()
|
ep.Display()
|
||||||
}
|
}
|
||||||
action.MainTab().Display()
|
action.MainTab().Display()
|
||||||
action.InfoBar.Display()
|
action.InfoBar.Display()
|
||||||
|
|
||||||
|
// Display dropdown menus LAST so they appear on top of everything
|
||||||
|
if dropdownOpen {
|
||||||
|
dropdown := action.MenuBar.GetActiveDropdown()
|
||||||
|
if dropdown != nil && dropdown.IsVisible() {
|
||||||
|
dropdown.Display()
|
||||||
|
// Force cursor to be hidden when dropdown is visible
|
||||||
|
screen.Screen.HideCursor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
screen.Screen.Show()
|
screen.Screen.Show()
|
||||||
|
|
||||||
// Check for new events
|
// Check for new events
|
||||||
|
|
@ -510,12 +533,67 @@ func DoEvent() {
|
||||||
if event != nil {
|
if event != nil {
|
||||||
_, resize := event.(*tcell.EventResize)
|
_, resize := event.(*tcell.EventResize)
|
||||||
if resize {
|
if resize {
|
||||||
|
// Handle resize for all components
|
||||||
|
if action.MenuBar != nil {
|
||||||
|
w, _ := screen.Screen.Size()
|
||||||
|
action.MenuBar.Resize(w, 1)
|
||||||
|
}
|
||||||
action.InfoBar.HandleEvent(event)
|
action.InfoBar.HandleEvent(event)
|
||||||
action.Tabs.HandleEvent(event)
|
action.Tabs.HandleEvent(event)
|
||||||
} else if action.InfoBar.HasPrompt {
|
} else if action.InfoBar.HasPrompt {
|
||||||
action.InfoBar.HandleEvent(event)
|
action.InfoBar.HandleEvent(event)
|
||||||
} else {
|
} else {
|
||||||
action.Tabs.HandleEvent(event)
|
// Check if menu bar should handle the event first
|
||||||
|
handled := false
|
||||||
|
if action.MenuBar != nil {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *tcell.EventMouse:
|
||||||
|
mx, my := e.Position()
|
||||||
|
if clickedItem := action.MenuBar.HandleClick(mx, my); clickedItem != nil {
|
||||||
|
// Menu item was clicked, execute the action
|
||||||
|
executeMenuAction(clickedItem.Action)
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
case *tcell.EventKey:
|
||||||
|
// Handle keyboard navigation for menus and dropdowns
|
||||||
|
var selectedItem *display.DropdownItem
|
||||||
|
|
||||||
|
// Only handle special keys if menu is open
|
||||||
|
if action.MenuBar.IsOpen() {
|
||||||
|
// Menu is open - handle navigation keys
|
||||||
|
if e.Key() == tcell.KeyEnter || e.Key() == tcell.KeyEscape ||
|
||||||
|
e.Key() == tcell.KeyUp || e.Key() == tcell.KeyDown ||
|
||||||
|
e.Key() == tcell.KeyLeft || e.Key() == tcell.KeyRight {
|
||||||
|
selectedItem = action.MenuBar.HandleKeyNavigation(e.Rune(), int(e.Key()))
|
||||||
|
handled = true
|
||||||
|
} else {
|
||||||
|
// Menu is open, check for dropdown item hotkeys
|
||||||
|
selectedItem = action.MenuBar.HandleKeyNavigation(e.Rune(), int(e.Key()))
|
||||||
|
if selectedItem != nil {
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if e.Modifiers()&tcell.ModAlt != 0 {
|
||||||
|
// Menu is closed - only handle Alt+key combinations to open menus
|
||||||
|
selectedItem = action.MenuBar.HandleKeyNavigation(e.Rune(), int(e.Key()))
|
||||||
|
if selectedItem != nil || action.MenuBar.IsOpen() {
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute action if a menu item was selected
|
||||||
|
if selectedItem != nil {
|
||||||
|
// Execute the selected action
|
||||||
|
executeMenuAction(selectedItem.Action)
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If menu didn't handle it, pass to tabs
|
||||||
|
if !handled {
|
||||||
|
action.Tabs.HandleEvent(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -524,3 +602,66 @@ func DoEvent() {
|
||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// executeMenuAction executes the specified action from a menu selection
|
||||||
|
func executeMenuAction(actionName string) {
|
||||||
|
// Get the current buffer pane to perform actions on
|
||||||
|
pane := action.MainTab().CurPane()
|
||||||
|
if pane == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the appropriate action based on the action name
|
||||||
|
switch actionName {
|
||||||
|
case "NewTab":
|
||||||
|
pane.NewTabCmd([]string{})
|
||||||
|
case "Open":
|
||||||
|
pane.OpenFile()
|
||||||
|
case "Save":
|
||||||
|
pane.Save()
|
||||||
|
case "SaveAs":
|
||||||
|
pane.SaveAs()
|
||||||
|
case "Quit":
|
||||||
|
pane.Quit()
|
||||||
|
case "Undo":
|
||||||
|
pane.Undo()
|
||||||
|
case "Redo":
|
||||||
|
pane.Redo()
|
||||||
|
case "Cut":
|
||||||
|
pane.Cut()
|
||||||
|
case "Copy":
|
||||||
|
pane.Copy()
|
||||||
|
case "Paste":
|
||||||
|
pane.Paste()
|
||||||
|
case "HSplit":
|
||||||
|
pane.HSplitAction()
|
||||||
|
case "VSplit":
|
||||||
|
pane.VSplitAction()
|
||||||
|
case "ToggleRuler":
|
||||||
|
pane.ToggleRuler()
|
||||||
|
case "Find":
|
||||||
|
pane.Find()
|
||||||
|
case "FindNext":
|
||||||
|
pane.FindNext()
|
||||||
|
case "FindPrevious":
|
||||||
|
pane.FindPrevious()
|
||||||
|
case "Replace":
|
||||||
|
pane.ReplaceCmd([]string{})
|
||||||
|
case "CommandMode":
|
||||||
|
pane.CommandMode()
|
||||||
|
case "PluginInstall":
|
||||||
|
// Open command mode with plugin install command
|
||||||
|
pane.CommandMode()
|
||||||
|
// TODO: Pre-fill with "plugin install " if possible
|
||||||
|
case "ToggleHelp":
|
||||||
|
pane.ToggleHelp()
|
||||||
|
case "ShowKey":
|
||||||
|
pane.ToggleKeyMenu()
|
||||||
|
case "ShowAbout":
|
||||||
|
// Display about information
|
||||||
|
screen.TermMessage("Micro " + util.Version + " - " + util.CommitHash)
|
||||||
|
default:
|
||||||
|
// If action not recognized, try to execute it as a command
|
||||||
|
screen.TermMessage("Unknown action: " + actionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
package action
|
package action
|
||||||
|
|
||||||
import "github.com/zyedidia/micro/v2/internal/buffer"
|
import (
|
||||||
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/display"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
|
)
|
||||||
|
|
||||||
// InfoBar is the global info bar.
|
// InfoBar is the global info bar.
|
||||||
var InfoBar *InfoPane
|
var InfoBar *InfoPane
|
||||||
|
|
||||||
|
// MenuBar is the global menu bar.
|
||||||
|
var MenuBar *display.MenuWindow
|
||||||
|
|
||||||
// LogBufPane is a global log buffer.
|
// LogBufPane is a global log buffer.
|
||||||
var LogBufPane *BufPane
|
var LogBufPane *BufPane
|
||||||
|
|
||||||
|
|
@ -12,6 +19,10 @@ var LogBufPane *BufPane
|
||||||
func InitGlobals() {
|
func InitGlobals() {
|
||||||
InfoBar = NewInfoBar()
|
InfoBar = NewInfoBar()
|
||||||
buffer.LogBuf = buffer.NewBufferFromString("", "Log", buffer.BTLog)
|
buffer.LogBuf = buffer.NewBufferFromString("", "Log", buffer.BTLog)
|
||||||
|
|
||||||
|
// Initialize menu bar at the top
|
||||||
|
w, _ := screen.Screen.Size()
|
||||||
|
MenuBar = display.NewMenuWindow(0, 0, w, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInfoBar returns the infobar pane
|
// GetInfoBar returns the infobar pane
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,21 @@ type TabList struct {
|
||||||
func NewTabList(bufs []*buffer.Buffer) *TabList {
|
func NewTabList(bufs []*buffer.Buffer) *TabList {
|
||||||
w, h := screen.Screen.Size()
|
w, h := screen.Screen.Size()
|
||||||
iOffset := config.GetInfoBarOffset()
|
iOffset := config.GetInfoBarOffset()
|
||||||
|
menuBarHeight := 1 // Reserve space for menu bar
|
||||||
|
|
||||||
tl := new(TabList)
|
tl := new(TabList)
|
||||||
tl.List = make([]*Tab, len(bufs))
|
tl.List = make([]*Tab, len(bufs))
|
||||||
if len(bufs) > 1 {
|
if len(bufs) > 1 {
|
||||||
|
// Multiple tabs: content starts below menu bar + tab bar
|
||||||
for i, b := range bufs {
|
for i, b := range bufs {
|
||||||
tl.List[i] = NewTabFromBuffer(0, 1, w, h-1-iOffset, b)
|
tl.List[i] = NewTabFromBuffer(0, menuBarHeight+1, w, h-1-iOffset-menuBarHeight, b)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tl.List[0] = NewTabFromBuffer(0, 0, w, h-iOffset, bufs[0])
|
// Single tab: content starts directly below menu bar
|
||||||
|
tl.List[0] = NewTabFromBuffer(0, menuBarHeight, w, h-iOffset-menuBarHeight, bufs[0])
|
||||||
}
|
}
|
||||||
tl.TabWindow = display.NewTabWindow(w, 0)
|
// TabWindow positioned below menu bar
|
||||||
|
tl.TabWindow = display.NewTabWindow(w, menuBarHeight)
|
||||||
tl.Names = make([]string, len(bufs))
|
tl.Names = make([]string, len(bufs))
|
||||||
|
|
||||||
return tl
|
return tl
|
||||||
|
|
@ -82,16 +87,28 @@ func (t *TabList) RemoveTab(id uint64) {
|
||||||
func (t *TabList) Resize() {
|
func (t *TabList) Resize() {
|
||||||
w, h := screen.Screen.Size()
|
w, h := screen.Screen.Size()
|
||||||
iOffset := config.GetInfoBarOffset()
|
iOffset := config.GetInfoBarOffset()
|
||||||
|
|
||||||
|
// Reserve space for menu bar at the top
|
||||||
|
menuBarHeight := 1
|
||||||
|
if MenuBar != nil {
|
||||||
|
MenuBar.Resize(w, menuBarHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoBar remains at the bottom
|
||||||
InfoBar.Resize(w, h-1)
|
InfoBar.Resize(w, h-1)
|
||||||
|
|
||||||
if len(t.List) > 1 {
|
if len(t.List) > 1 {
|
||||||
|
// Tab bar is below menu bar
|
||||||
|
tabBarY := menuBarHeight
|
||||||
for _, p := range t.List {
|
for _, p := range t.List {
|
||||||
p.Y = 1
|
p.Y = tabBarY + 1 // Content starts below both menu and tab bars
|
||||||
p.Node.Resize(w, h-1-iOffset)
|
p.Node.Resize(w, h-1-iOffset-menuBarHeight)
|
||||||
p.Resize()
|
p.Resize()
|
||||||
}
|
}
|
||||||
} else if len(t.List) == 1 {
|
} else if len(t.List) == 1 {
|
||||||
t.List[0].Y = 0
|
// Single tab: content starts directly below menu bar
|
||||||
t.List[0].Node.Resize(w, h-iOffset)
|
t.List[0].Y = menuBarHeight
|
||||||
|
t.List[0].Node.Resize(w, h-iOffset-menuBarHeight)
|
||||||
t.List[0].Resize()
|
t.List[0].Resize()
|
||||||
}
|
}
|
||||||
t.TabWindow.Resize(w, h)
|
t.TabWindow.Resize(w, h)
|
||||||
|
|
|
||||||
430
internal/display/dropdown.go
Normal file
430
internal/display/dropdown.go
Normal file
|
|
@ -0,0 +1,430 @@
|
||||||
|
package display
|
||||||
|
|
||||||
|
import (
|
||||||
|
runewidth "github.com/mattn/go-runewidth"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DropdownItem represents a single item in a dropdown menu
|
||||||
|
type DropdownItem struct {
|
||||||
|
Text string
|
||||||
|
Action string
|
||||||
|
Hotkey rune
|
||||||
|
Enabled bool
|
||||||
|
Separator bool // True for separator lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropdownMenu represents a dropdown menu that appears below menu items
|
||||||
|
type DropdownMenu struct {
|
||||||
|
Items []DropdownItem
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Active int // Currently highlighted item (-1 for none)
|
||||||
|
Visible bool // Whether the dropdown is currently shown
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDropdownMenu creates a new dropdown menu
|
||||||
|
func NewDropdownMenu() *DropdownMenu {
|
||||||
|
return &DropdownMenu{
|
||||||
|
Items: []DropdownItem{},
|
||||||
|
Active: -1,
|
||||||
|
Visible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetItems sets the items for this dropdown menu
|
||||||
|
func (d *DropdownMenu) SetItems(items []DropdownItem) {
|
||||||
|
d.Items = items
|
||||||
|
d.calculateSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateSize determines the width and height needed for the dropdown
|
||||||
|
func (d *DropdownMenu) calculateSize() {
|
||||||
|
d.Width = 0
|
||||||
|
d.Height = len(d.Items) + 2 // +2 for top and bottom borders
|
||||||
|
|
||||||
|
// Find the widest item
|
||||||
|
for _, item := range d.Items {
|
||||||
|
if item.Separator {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
itemWidth := util.StringWidth([]byte(item.Text), util.CharacterCountInString(item.Text), 1)
|
||||||
|
if item.Hotkey != 0 {
|
||||||
|
itemWidth += 4 // Space for " (X)" hotkey display
|
||||||
|
}
|
||||||
|
if itemWidth > d.Width {
|
||||||
|
d.Width = itemWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding and border
|
||||||
|
d.Width += 4 // 2 for borders + 2 for padding
|
||||||
|
if d.Width < 8 {
|
||||||
|
d.Width = 8 // Minimum width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show displays the dropdown at the specified position
|
||||||
|
func (d *DropdownMenu) Show(x, y int) {
|
||||||
|
d.X = x
|
||||||
|
d.Y = y
|
||||||
|
d.Visible = true
|
||||||
|
|
||||||
|
// Set the first selectable item as active
|
||||||
|
d.Active = -1
|
||||||
|
for i := 0; i < len(d.Items); i++ {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide hides the dropdown
|
||||||
|
func (d *DropdownMenu) Hide() {
|
||||||
|
d.Visible = false
|
||||||
|
d.Active = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsVisible returns whether the dropdown is currently visible
|
||||||
|
func (d *DropdownMenu) IsVisible() bool {
|
||||||
|
return d.Visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display renders the dropdown menu
|
||||||
|
func (d *DropdownMenu) Display() {
|
||||||
|
if !d.Visible || d.Height == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get terminal size to ensure we don't draw outside bounds
|
||||||
|
termWidth, termHeight := screen.Screen.Size()
|
||||||
|
|
||||||
|
// Adjust position if dropdown would go off screen
|
||||||
|
adjustedX := d.X
|
||||||
|
adjustedY := d.Y
|
||||||
|
|
||||||
|
if adjustedX+d.Width > termWidth {
|
||||||
|
adjustedX = termWidth - d.Width
|
||||||
|
if adjustedX < 0 {
|
||||||
|
adjustedX = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if adjustedY+d.Height > termHeight {
|
||||||
|
adjustedY = termHeight - d.Height
|
||||||
|
if adjustedY < 0 {
|
||||||
|
adjustedY = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw dropdown background and border with proper backdrop
|
||||||
|
// Use normal style for dropdown, reverse for highlighting
|
||||||
|
dropdownStyle := config.DefStyle
|
||||||
|
borderStyle := config.DefStyle
|
||||||
|
shadowStyle := config.DefStyle.Dim(true) // For drop shadow effect
|
||||||
|
|
||||||
|
// Draw shadow effect first (offset by 1 pixel)
|
||||||
|
for row := 1; row <= d.Height; row++ {
|
||||||
|
for col := 1; col <= d.Width; col++ {
|
||||||
|
x := adjustedX + col
|
||||||
|
y := adjustedY + row
|
||||||
|
if x < termWidth && y < termHeight {
|
||||||
|
screen.SetContent(x, y, ' ', nil, shadowStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for row := 0; row < d.Height; row++ {
|
||||||
|
y := adjustedY + row
|
||||||
|
if y >= termHeight {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for col := 0; col < d.Width; col++ {
|
||||||
|
x := adjustedX + col
|
||||||
|
if x >= termWidth {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw border
|
||||||
|
if col == 0 || col == d.Width-1 {
|
||||||
|
if row == 0 {
|
||||||
|
if col == 0 {
|
||||||
|
screen.SetContent(x, y, '┌', nil, borderStyle)
|
||||||
|
} else {
|
||||||
|
screen.SetContent(x, y, '┐', nil, borderStyle)
|
||||||
|
}
|
||||||
|
} else if row == d.Height-1 {
|
||||||
|
if col == 0 {
|
||||||
|
screen.SetContent(x, y, '└', nil, borderStyle)
|
||||||
|
} else {
|
||||||
|
screen.SetContent(x, y, '┘', nil, borderStyle)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
screen.SetContent(x, y, '│', nil, borderStyle)
|
||||||
|
}
|
||||||
|
} else if row == 0 || row == d.Height-1 {
|
||||||
|
screen.SetContent(x, y, '─', nil, borderStyle)
|
||||||
|
} else {
|
||||||
|
screen.SetContent(x, y, ' ', nil, dropdownStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw menu items
|
||||||
|
itemY := 0
|
||||||
|
for i, item := range d.Items {
|
||||||
|
if itemY >= d.Height-2 { // Account for top and bottom borders
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
y := adjustedY + 1 + itemY // +1 for top border
|
||||||
|
if y >= termHeight {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Separator {
|
||||||
|
// Draw separator line
|
||||||
|
for x := adjustedX + 1; x < adjustedX+d.Width-1; x++ {
|
||||||
|
if x < termWidth {
|
||||||
|
screen.SetContent(x, y, '─', nil, borderStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw menu item
|
||||||
|
itemStyle := dropdownStyle
|
||||||
|
if i == d.Active {
|
||||||
|
// Highlight active item
|
||||||
|
itemStyle = itemStyle.Reverse(true)
|
||||||
|
}
|
||||||
|
if !item.Enabled {
|
||||||
|
// Dim disabled items
|
||||||
|
itemStyle = itemStyle.Dim(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the line first
|
||||||
|
for x := adjustedX + 1; x < adjustedX+d.Width-1; x++ {
|
||||||
|
if x < termWidth {
|
||||||
|
screen.SetContent(x, y, ' ', nil, itemStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw item text
|
||||||
|
x := adjustedX + 2 // +2 for border and padding
|
||||||
|
for _, r := range item.Text {
|
||||||
|
if x >= adjustedX+d.Width-2 || x >= termWidth {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
screen.SetContent(x, y, r, nil, itemStyle)
|
||||||
|
x += runewidth.RuneWidth(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw hotkey if present
|
||||||
|
if item.Hotkey != 0 && x < adjustedX+d.Width-4 {
|
||||||
|
hotkeyText := " (" + string(item.Hotkey) + ")"
|
||||||
|
for _, r := range hotkeyText {
|
||||||
|
if x >= adjustedX+d.Width-2 || x >= termWidth {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
screen.SetContent(x, y, r, nil, itemStyle.Dim(true))
|
||||||
|
x += runewidth.RuneWidth(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemY++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleClick handles mouse clicks on the dropdown
|
||||||
|
func (d *DropdownMenu) HandleClick(x, y int) *DropdownItem {
|
||||||
|
if !d.Visible {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if click is inside dropdown bounds
|
||||||
|
if x < d.X || x >= d.X+d.Width || y < d.Y || y >= d.Y+d.Height {
|
||||||
|
// Click outside dropdown - hide it
|
||||||
|
d.Hide()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if click is on border
|
||||||
|
if x == d.X || x == d.X+d.Width-1 || y == d.Y || y == d.Y+d.Height-1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate which item was clicked
|
||||||
|
itemIndex := y - d.Y - 1 // -1 for top border
|
||||||
|
if itemIndex >= 0 && itemIndex < len(d.Items) {
|
||||||
|
item := &d.Items[itemIndex]
|
||||||
|
if !item.Separator && item.Enabled {
|
||||||
|
d.Hide()
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleKey handles keyboard navigation in the dropdown
|
||||||
|
func (d *DropdownMenu) HandleKey(key rune) *DropdownItem {
|
||||||
|
if !d.Visible {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for hotkey matches
|
||||||
|
for _, item := range d.Items {
|
||||||
|
if !item.Separator && item.Enabled {
|
||||||
|
if key == item.Hotkey || (key >= 'A' && key <= 'Z' && key-'A'+'a' == item.Hotkey) {
|
||||||
|
d.Hide()
|
||||||
|
return &item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NavigateUp moves selection up in the dropdown
|
||||||
|
func (d *DropdownMenu) NavigateUp() {
|
||||||
|
if !d.Visible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := d.Active - 1; i >= 0; i-- {
|
||||||
|
if !d.Items[i].Separator && d.Items[i].Enabled {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap to bottom
|
||||||
|
for i := len(d.Items) - 1; i > d.Active; i-- {
|
||||||
|
if !d.Items[i].Separator && d.Items[i].Enabled {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NavigateDown moves selection down in the dropdown
|
||||||
|
func (d *DropdownMenu) NavigateDown() {
|
||||||
|
if !d.Visible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := d.Active + 1; i < len(d.Items); i++ {
|
||||||
|
if !d.Items[i].Separator && d.Items[i].Enabled {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap to top
|
||||||
|
for i := 0; i < d.Active; i++ {
|
||||||
|
if !d.Items[i].Separator && d.Items[i].Enabled {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectActive returns the currently active item and hides the dropdown
|
||||||
|
func (d *DropdownMenu) SelectActive() *DropdownItem {
|
||||||
|
if !d.Visible || d.Active < 0 || d.Active >= len(d.Items) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
item := &d.Items[d.Active]
|
||||||
|
if !item.Separator && item.Enabled {
|
||||||
|
d.Hide()
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveItem returns the currently active item, or nil if none
|
||||||
|
func (d *DropdownMenu) GetActiveItem() *DropdownItem {
|
||||||
|
if d.Active >= 0 && d.Active < len(d.Items) {
|
||||||
|
return &d.Items[d.Active]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveUp moves selection up to previous selectable item
|
||||||
|
func (d *DropdownMenu) MoveUp() {
|
||||||
|
if d.Active < 0 {
|
||||||
|
// No item selected, select the last selectable item
|
||||||
|
for i := len(d.Items) - 1; i >= 0; i-- {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if d.Active == 0 {
|
||||||
|
// At first item, wrap to last selectable item
|
||||||
|
for i := len(d.Items) - 1; i >= 0; i-- {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Move to previous selectable item
|
||||||
|
for i := d.Active - 1; i >= 0; i-- {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no previous selectable item found, wrap to last
|
||||||
|
for i := len(d.Items) - 1; i >= 0; i-- {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveDown moves selection down to next selectable item
|
||||||
|
func (d *DropdownMenu) MoveDown() {
|
||||||
|
if d.Active < 0 {
|
||||||
|
// No item selected, select the first selectable item
|
||||||
|
for i := 0; i < len(d.Items); i++ {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if d.Active >= len(d.Items)-1 {
|
||||||
|
// At last item, wrap to first selectable item
|
||||||
|
for i := 0; i < len(d.Items); i++ {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Move to next selectable item
|
||||||
|
for i := d.Active + 1; i < len(d.Items); i++ {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no next selectable item found, wrap to first
|
||||||
|
for i := 0; i < len(d.Items); i++ {
|
||||||
|
if d.Items[i].Enabled && !d.Items[i].Separator {
|
||||||
|
d.Active = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
461
internal/display/menuwindow.go
Normal file
461
internal/display/menuwindow.go
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue