411 lines
9.6 KiB
Go
411 lines
9.6 KiB
Go
package widget
|
||
|
||
import (
|
||
"image/color"
|
||
"strings"
|
||
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/canvas"
|
||
"fyne.io/fyne/v2/driver/desktop"
|
||
"fyne.io/fyne/v2/internal/svg"
|
||
"fyne.io/fyne/v2/internal/widget"
|
||
"fyne.io/fyne/v2/theme"
|
||
)
|
||
|
||
var _ fyne.Widget = (*menuItem)(nil)
|
||
|
||
// menuItem is a widget for displaying a fyne.menuItem.
|
||
type menuItem struct {
|
||
widget.Base
|
||
Item *fyne.MenuItem
|
||
|
||
alignment fyne.TextAlign
|
||
child, parent *Menu
|
||
}
|
||
|
||
// newMenuItem creates a new menuItem.
|
||
func newMenuItem(item *fyne.MenuItem, parent *Menu) *menuItem {
|
||
i := &menuItem{Item: item, parent: parent}
|
||
i.alignment = parent.alignment
|
||
i.ExtendBaseWidget(i)
|
||
return i
|
||
}
|
||
|
||
func (i *menuItem) Child() *Menu {
|
||
if i.Item.ChildMenu != nil && i.child == nil {
|
||
child := NewMenu(i.Item.ChildMenu)
|
||
child.Hide()
|
||
child.OnDismiss = i.parent.Dismiss
|
||
i.child = child
|
||
}
|
||
return i.child
|
||
}
|
||
|
||
// CreateRenderer returns a new renderer for the menu item.
|
||
//
|
||
// Implements: fyne.Widget
|
||
func (i *menuItem) CreateRenderer() fyne.WidgetRenderer {
|
||
th := i.parent.Theme()
|
||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||
|
||
background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
|
||
background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
||
background.Hide()
|
||
text := canvas.NewText(i.Item.Label, th.Color(theme.ColorNameForeground, v))
|
||
text.Alignment = i.alignment
|
||
objects := []fyne.CanvasObject{background, text}
|
||
var expandIcon *canvas.Image
|
||
if i.Item.ChildMenu != nil {
|
||
expandIcon = canvas.NewImageFromResource(th.Icon(theme.IconNameMenuExpand))
|
||
objects = append(objects, expandIcon)
|
||
}
|
||
checkIcon := canvas.NewImageFromResource(th.Icon(theme.IconNameConfirm))
|
||
if !i.Item.Checked {
|
||
checkIcon.Hide()
|
||
}
|
||
var icon *canvas.Image
|
||
if i.Item.Icon != nil {
|
||
icon = canvas.NewImageFromResource(i.Item.Icon)
|
||
objects = append(objects, icon)
|
||
}
|
||
var shortcutTexts []*canvas.Text
|
||
if s, ok := i.Item.Shortcut.(fyne.KeyboardShortcut); ok {
|
||
shortcutTexts = textsForShortcut(s, th)
|
||
for _, t := range shortcutTexts {
|
||
objects = append(objects, t)
|
||
}
|
||
}
|
||
|
||
objects = append(objects, checkIcon)
|
||
r := &menuItemRenderer{
|
||
BaseRenderer: widget.NewBaseRenderer(objects),
|
||
i: i,
|
||
expandIcon: expandIcon,
|
||
checkIcon: checkIcon,
|
||
icon: icon,
|
||
shortcutTexts: shortcutTexts,
|
||
text: text,
|
||
background: background,
|
||
}
|
||
r.updateVisuals()
|
||
return r
|
||
}
|
||
|
||
// MouseIn activates the item which shows the submenu if the item has one.
|
||
// The submenu of any sibling of the item will be hidden.
|
||
//
|
||
// Implements: desktop.Hoverable
|
||
func (i *menuItem) MouseIn(*desktop.MouseEvent) {
|
||
i.activate()
|
||
}
|
||
|
||
// MouseMoved does nothing.
|
||
//
|
||
// Implements: desktop.Hoverable
|
||
func (i *menuItem) MouseMoved(*desktop.MouseEvent) {
|
||
}
|
||
|
||
// MouseOut deactivates the item unless it has an open submenu.
|
||
//
|
||
// Implements: desktop.Hoverable
|
||
func (i *menuItem) MouseOut() {
|
||
if !i.isSubmenuOpen() {
|
||
i.deactivate()
|
||
}
|
||
}
|
||
|
||
// Tapped performs the action of the item and dismisses the menu.
|
||
// It does nothing if the item doesn’t have an action.
|
||
//
|
||
// Implements: fyne.Tappable
|
||
func (i *menuItem) Tapped(*fyne.PointEvent) {
|
||
if i.Item.Disabled {
|
||
return
|
||
}
|
||
if i.Item.Action == nil {
|
||
if fyne.CurrentDevice().IsMobile() {
|
||
i.activate()
|
||
}
|
||
|
||
return
|
||
} else if i.Item.ChildMenu != nil {
|
||
if fyne.CurrentDevice().IsMobile() {
|
||
i.activate()
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
i.trigger()
|
||
}
|
||
|
||
func (i *menuItem) activate() {
|
||
if i.Item.Disabled {
|
||
return
|
||
}
|
||
if i.Child() != nil {
|
||
i.Child().Show()
|
||
}
|
||
i.parent.activateItem(i)
|
||
}
|
||
|
||
func (i *menuItem) activateLastSubmenu() bool {
|
||
if i.Child() == nil {
|
||
return false
|
||
}
|
||
if i.isSubmenuOpen() {
|
||
return i.Child().ActivateLastSubmenu()
|
||
}
|
||
i.Child().Show()
|
||
i.Child().ActivateNext()
|
||
return true
|
||
}
|
||
|
||
func (i *menuItem) deactivate() {
|
||
if i.Child() != nil {
|
||
i.Child().Hide()
|
||
}
|
||
i.parent.DeactivateChild()
|
||
}
|
||
|
||
func (i *menuItem) deactivateLastSubmenu() bool {
|
||
if !i.isSubmenuOpen() {
|
||
return false
|
||
}
|
||
if !i.Child().DeactivateLastSubmenu() {
|
||
i.Child().DeactivateChild()
|
||
i.Child().Hide()
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (i *menuItem) isActive() bool {
|
||
return i.parent.activeItem == i
|
||
}
|
||
|
||
func (i *menuItem) isSubmenuOpen() bool {
|
||
return i.Child() != nil && i.Child().Visible()
|
||
}
|
||
|
||
func (i *menuItem) trigger() {
|
||
i.parent.Dismiss()
|
||
if i.Item.Action != nil {
|
||
i.Item.Action()
|
||
}
|
||
}
|
||
|
||
func (i *menuItem) triggerLast() {
|
||
if i.isSubmenuOpen() {
|
||
i.Child().TriggerLast()
|
||
return
|
||
}
|
||
i.trigger()
|
||
}
|
||
|
||
type menuItemRenderer struct {
|
||
widget.BaseRenderer
|
||
i *menuItem
|
||
background *canvas.Rectangle
|
||
checkIcon *canvas.Image
|
||
expandIcon *canvas.Image
|
||
icon *canvas.Image
|
||
lastThemePadding float32
|
||
minSize fyne.Size
|
||
shortcutTexts []*canvas.Text
|
||
text *canvas.Text
|
||
}
|
||
|
||
func (r *menuItemRenderer) Layout(size fyne.Size) {
|
||
th := r.i.parent.Theme()
|
||
innerPad := th.Size(theme.SizeNameInnerPadding)
|
||
inlineIcon := th.Size(theme.SizeNameInlineIcon)
|
||
|
||
leftOffset := innerPad + r.checkSpace()
|
||
rightOffset := size.Width
|
||
iconSize := fyne.NewSquareSize(inlineIcon)
|
||
iconTopOffset := (size.Height - inlineIcon) / 2
|
||
|
||
if r.expandIcon != nil {
|
||
rightOffset -= inlineIcon
|
||
r.expandIcon.Resize(iconSize)
|
||
r.expandIcon.Move(fyne.NewPos(rightOffset, iconTopOffset))
|
||
}
|
||
|
||
rightOffset -= innerPad
|
||
textHeight := r.text.MinSize().Height
|
||
for i := len(r.shortcutTexts) - 1; i >= 0; i-- {
|
||
text := r.shortcutTexts[i]
|
||
text.Resize(text.MinSize())
|
||
rightOffset -= text.MinSize().Width
|
||
text.Move(fyne.NewPos(rightOffset, innerPad+(textHeight-text.Size().Height)))
|
||
|
||
if i == 0 {
|
||
rightOffset -= innerPad
|
||
}
|
||
}
|
||
|
||
r.checkIcon.Resize(iconSize)
|
||
r.checkIcon.Move(fyne.NewPos(innerPad, iconTopOffset))
|
||
|
||
if r.icon != nil {
|
||
r.icon.Resize(iconSize)
|
||
r.icon.Move(fyne.NewPos(leftOffset, iconTopOffset))
|
||
leftOffset += inlineIcon
|
||
leftOffset += innerPad
|
||
}
|
||
|
||
r.text.Resize(fyne.NewSize(rightOffset-leftOffset, textHeight))
|
||
r.text.Move(fyne.NewPos(leftOffset, innerPad))
|
||
|
||
r.background.Resize(size)
|
||
}
|
||
|
||
func (r *menuItemRenderer) MinSize() fyne.Size {
|
||
if r.minSizeUnchanged() {
|
||
return r.minSize
|
||
}
|
||
|
||
th := r.i.parent.Theme()
|
||
innerPad := th.Size(theme.SizeNameInnerPadding)
|
||
inlineIcon := th.Size(theme.SizeNameInlineIcon)
|
||
innerPad2 := innerPad * 2
|
||
|
||
minSize := r.text.MinSize().AddWidthHeight(innerPad2+r.checkSpace(), innerPad2)
|
||
if r.expandIcon != nil {
|
||
minSize = minSize.AddWidthHeight(inlineIcon, 0)
|
||
}
|
||
if r.icon != nil {
|
||
minSize = minSize.AddWidthHeight(inlineIcon+innerPad, 0)
|
||
}
|
||
if r.shortcutTexts != nil {
|
||
var textWidth float32
|
||
for _, text := range r.shortcutTexts {
|
||
textWidth += text.MinSize().Width
|
||
}
|
||
minSize = minSize.AddWidthHeight(textWidth+innerPad, 0)
|
||
}
|
||
r.minSize = minSize
|
||
return r.minSize
|
||
}
|
||
|
||
func (r *menuItemRenderer) updateVisuals() {
|
||
th := r.i.parent.Theme()
|
||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||
r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
||
if fyne.CurrentDevice().IsMobile() {
|
||
r.background.Hide()
|
||
} else if r.i.isActive() {
|
||
r.background.FillColor = th.Color(theme.ColorNameFocus, v)
|
||
r.background.Show()
|
||
} else {
|
||
r.background.Hide()
|
||
}
|
||
r.background.Refresh()
|
||
r.text.Alignment = r.i.alignment
|
||
r.refreshText(r.text, false)
|
||
for _, text := range r.shortcutTexts {
|
||
r.refreshText(text, true)
|
||
}
|
||
|
||
if r.i.Item.Checked {
|
||
r.checkIcon.Show()
|
||
} else {
|
||
r.checkIcon.Hide()
|
||
}
|
||
r.updateIcon(r.checkIcon, th.Icon(theme.IconNameConfirm))
|
||
r.updateIcon(r.expandIcon, th.Icon(theme.IconNameMenuExpand))
|
||
r.updateIcon(r.icon, r.i.Item.Icon)
|
||
}
|
||
|
||
func (r *menuItemRenderer) Refresh() {
|
||
r.updateVisuals()
|
||
canvas.Refresh(r.i)
|
||
}
|
||
|
||
func (r *menuItemRenderer) checkSpace() float32 {
|
||
if r.i.parent.containsCheck {
|
||
return theme.IconInlineSize() + theme.InnerPadding()
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func (r *menuItemRenderer) minSizeUnchanged() bool {
|
||
th := r.i.parent.Theme()
|
||
|
||
return !r.minSize.IsZero() &&
|
||
r.text.TextSize == th.Size(theme.SizeNameText) &&
|
||
(r.expandIcon == nil || r.expandIcon.Size().Width == th.Size(theme.SizeNameInlineIcon)) &&
|
||
r.lastThemePadding == th.Size(theme.SizeNameInnerPadding)
|
||
}
|
||
|
||
func (r *menuItemRenderer) updateIcon(img *canvas.Image, rsc fyne.Resource) {
|
||
if img == nil {
|
||
return
|
||
}
|
||
if r.i.Item.Disabled && svg.IsResourceSVG(rsc) {
|
||
img.Resource = theme.NewDisabledResource(rsc)
|
||
} else {
|
||
img.Resource = rsc
|
||
}
|
||
}
|
||
|
||
func (r *menuItemRenderer) refreshText(text *canvas.Text, shortcut bool) {
|
||
th := r.i.parent.Theme()
|
||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||
|
||
text.TextSize = th.Size(theme.SizeNameText)
|
||
if r.i.Item.Disabled {
|
||
text.Color = th.Color(theme.ColorNameDisabled, v)
|
||
} else {
|
||
if shortcut {
|
||
text.Color = shortcutColor(th)
|
||
} else {
|
||
text.Color = th.Color(theme.ColorNameForeground, v)
|
||
}
|
||
}
|
||
text.Refresh()
|
||
}
|
||
|
||
func shortcutColor(th fyne.Theme) color.Color {
|
||
v := fyne.CurrentApp().Settings().ThemeVariant()
|
||
r, g, b, a := th.Color(theme.ColorNameForeground, v).RGBA()
|
||
a = uint32(float32(a) * 0.95)
|
||
return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}
|
||
}
|
||
|
||
func textsForShortcut(sc fyne.KeyboardShortcut, th fyne.Theme) (texts []*canvas.Text) {
|
||
// add modifier
|
||
b := strings.Builder{}
|
||
mods := sc.Mod()
|
||
if mods&fyne.KeyModifierControl != 0 {
|
||
b.WriteString(textModifierControl)
|
||
}
|
||
if mods&fyne.KeyModifierAlt != 0 {
|
||
b.WriteString(textModifierAlt)
|
||
}
|
||
if mods&fyne.KeyModifierShift != 0 {
|
||
b.WriteString(textModifierShift)
|
||
}
|
||
if mods&fyne.KeyModifierSuper != 0 {
|
||
b.WriteString(textModifierSuper)
|
||
}
|
||
shortColor := shortcutColor(th)
|
||
if b.Len() > 0 {
|
||
t := canvas.NewText(b.String(), shortColor)
|
||
t.TextStyle = styleModifiers
|
||
texts = append(texts, t)
|
||
}
|
||
// add key
|
||
style := defaultStyleKeys
|
||
s, ok := keyTexts[sc.Key()]
|
||
if !ok {
|
||
s = string(sc.Key())
|
||
} else if len(s) == 1 {
|
||
style = fyne.TextStyle{Symbol: true}
|
||
}
|
||
t := canvas.NewText(s, shortColor)
|
||
t.TextStyle = style
|
||
texts = append(texts, t)
|
||
return
|
||
}
|