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/clipboard"
|
||||
"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/shell"
|
||||
"github.com/zyedidia/micro/v2/internal/util"
|
||||
|
|
@ -462,13 +463,35 @@ func DoEvent() {
|
|||
|
||||
// Display everything
|
||||
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()
|
||||
|
||||
// Display menu bar first (at the top) - but not the dropdown yet
|
||||
if action.MenuBar != nil {
|
||||
action.MenuBar.Display()
|
||||
}
|
||||
|
||||
action.Tabs.Display()
|
||||
for _, ep := range action.MainTab().Panes {
|
||||
ep.Display()
|
||||
}
|
||||
action.MainTab().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()
|
||||
|
||||
// Check for new events
|
||||
|
|
@ -510,12 +533,67 @@ func DoEvent() {
|
|||
if event != nil {
|
||||
_, resize := event.(*tcell.EventResize)
|
||||
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.Tabs.HandleEvent(event)
|
||||
} else if action.InfoBar.HasPrompt {
|
||||
action.InfoBar.HandleEvent(event)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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.
|
||||
var InfoBar *InfoPane
|
||||
|
||||
// MenuBar is the global menu bar.
|
||||
var MenuBar *display.MenuWindow
|
||||
|
||||
// LogBufPane is a global log buffer.
|
||||
var LogBufPane *BufPane
|
||||
|
||||
|
|
@ -12,6 +19,10 @@ var LogBufPane *BufPane
|
|||
func InitGlobals() {
|
||||
InfoBar = NewInfoBar()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -24,16 +24,21 @@ type TabList struct {
|
|||
func NewTabList(bufs []*buffer.Buffer) *TabList {
|
||||
w, h := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
menuBarHeight := 1 // Reserve space for menu bar
|
||||
|
||||
tl := new(TabList)
|
||||
tl.List = make([]*Tab, len(bufs))
|
||||
if len(bufs) > 1 {
|
||||
// Multiple tabs: content starts below menu bar + tab bar
|
||||
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 {
|
||||
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))
|
||||
|
||||
return tl
|
||||
|
|
@ -82,16 +87,28 @@ func (t *TabList) RemoveTab(id uint64) {
|
|||
func (t *TabList) Resize() {
|
||||
w, h := screen.Screen.Size()
|
||||
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)
|
||||
|
||||
if len(t.List) > 1 {
|
||||
// Tab bar is below menu bar
|
||||
tabBarY := menuBarHeight
|
||||
for _, p := range t.List {
|
||||
p.Y = 1
|
||||
p.Node.Resize(w, h-1-iOffset)
|
||||
p.Y = tabBarY + 1 // Content starts below both menu and tab bars
|
||||
p.Node.Resize(w, h-1-iOffset-menuBarHeight)
|
||||
p.Resize()
|
||||
}
|
||||
} else if len(t.List) == 1 {
|
||||
t.List[0].Y = 0
|
||||
t.List[0].Node.Resize(w, h-iOffset)
|
||||
// Single tab: content starts directly below menu bar
|
||||
t.List[0].Y = menuBarHeight
|
||||
t.List[0].Node.Resize(w, h-iOffset-menuBarHeight)
|
||||
t.List[0].Resize()
|
||||
}
|
||||
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