361 lines
8.5 KiB
Go
361 lines
8.5 KiB
Go
|
package widget
|
||
|
|
||
|
import (
|
||
|
"fyne.io/fyne/v2"
|
||
|
"fyne.io/fyne/v2/canvas"
|
||
|
"fyne.io/fyne/v2/internal/widget"
|
||
|
"fyne.io/fyne/v2/layout"
|
||
|
"fyne.io/fyne/v2/theme"
|
||
|
)
|
||
|
|
||
|
var _ fyne.Widget = (*Menu)(nil)
|
||
|
var _ fyne.Tappable = (*Menu)(nil)
|
||
|
|
||
|
// Menu is a widget for displaying a fyne.Menu.
|
||
|
type Menu struct {
|
||
|
BaseWidget
|
||
|
alignment fyne.TextAlign
|
||
|
Items []fyne.CanvasObject
|
||
|
OnDismiss func()
|
||
|
activeItem *menuItem
|
||
|
customSized bool
|
||
|
containsCheck bool
|
||
|
}
|
||
|
|
||
|
// NewMenu creates a new Menu.
|
||
|
func NewMenu(menu *fyne.Menu) *Menu {
|
||
|
m := &Menu{}
|
||
|
m.ExtendBaseWidget(m)
|
||
|
m.setMenu(menu)
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// ActivateLastSubmenu finds the last active menu item traversing through the open submenus
|
||
|
// and activates its submenu if any.
|
||
|
// It returns `true` if there was a submenu and it was activated and `false` elsewhere.
|
||
|
// Activating a submenu does show it and activate its first item.
|
||
|
func (m *Menu) ActivateLastSubmenu() bool {
|
||
|
if m.activeItem == nil {
|
||
|
return false
|
||
|
}
|
||
|
if !m.activeItem.activateLastSubmenu() {
|
||
|
return false
|
||
|
}
|
||
|
m.Refresh()
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// ActivateNext activates the menu item following the currently active menu item.
|
||
|
// If there is no menu item active, it activates the first menu item.
|
||
|
// If there is no menu item after the current active one, it does nothing.
|
||
|
// If a submenu is open, it delegates the activation to this submenu.
|
||
|
func (m *Menu) ActivateNext() {
|
||
|
if m.activeItem != nil && m.activeItem.isSubmenuOpen() {
|
||
|
m.activeItem.Child().ActivateNext()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
found := m.activeItem == nil
|
||
|
for _, item := range m.Items {
|
||
|
if mItem, ok := item.(*menuItem); ok {
|
||
|
if found {
|
||
|
m.activateItem(mItem)
|
||
|
return
|
||
|
}
|
||
|
if mItem == m.activeItem {
|
||
|
found = true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ActivatePrevious activates the menu item preceding the currently active menu item.
|
||
|
// If there is no menu item active, it activates the last menu item.
|
||
|
// If there is no menu item before the current active one, it does nothing.
|
||
|
// If a submenu is open, it delegates the activation to this submenu.
|
||
|
func (m *Menu) ActivatePrevious() {
|
||
|
if m.activeItem != nil && m.activeItem.isSubmenuOpen() {
|
||
|
m.activeItem.Child().ActivatePrevious()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
found := m.activeItem == nil
|
||
|
for i := len(m.Items) - 1; i >= 0; i-- {
|
||
|
item := m.Items[i]
|
||
|
if mItem, ok := item.(*menuItem); ok {
|
||
|
if found {
|
||
|
m.activateItem(mItem)
|
||
|
return
|
||
|
}
|
||
|
if mItem == m.activeItem {
|
||
|
found = true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// CreateRenderer returns a new renderer for the menu.
|
||
|
//
|
||
|
// Implements: fyne.Widget
|
||
|
func (m *Menu) CreateRenderer() fyne.WidgetRenderer {
|
||
|
m.ExtendBaseWidget(m)
|
||
|
box := newMenuBox(m.Items)
|
||
|
scroll := widget.NewVScroll(box)
|
||
|
scroll.SetMinSize(box.MinSize())
|
||
|
objects := []fyne.CanvasObject{scroll}
|
||
|
for _, i := range m.Items {
|
||
|
if item, ok := i.(*menuItem); ok && item.Child() != nil {
|
||
|
objects = append(objects, item.Child())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return &menuRenderer{
|
||
|
widget.NewShadowingRenderer(objects, widget.MenuLevel),
|
||
|
box,
|
||
|
m,
|
||
|
scroll,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DeactivateChild deactivates the active menu item and hides its submenu if any.
|
||
|
func (m *Menu) DeactivateChild() {
|
||
|
if m.activeItem != nil {
|
||
|
defer m.activeItem.Refresh()
|
||
|
if c := m.activeItem.Child(); c != nil {
|
||
|
c.Hide()
|
||
|
}
|
||
|
m.activeItem = nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DeactivateLastSubmenu finds the last open submenu traversing through the open submenus,
|
||
|
// deactivates its active item and hides it.
|
||
|
// This also deactivates any submenus of the deactivated submenu.
|
||
|
// It returns `true` if there was a submenu open and closed and `false` elsewhere.
|
||
|
func (m *Menu) DeactivateLastSubmenu() bool {
|
||
|
if m.activeItem == nil {
|
||
|
return false
|
||
|
}
|
||
|
return m.activeItem.deactivateLastSubmenu()
|
||
|
}
|
||
|
|
||
|
// MinSize returns the minimal size of the menu.
|
||
|
//
|
||
|
// Implements: fyne.Widget
|
||
|
func (m *Menu) MinSize() fyne.Size {
|
||
|
m.ExtendBaseWidget(m)
|
||
|
return m.BaseWidget.MinSize()
|
||
|
}
|
||
|
|
||
|
// Refresh updates the menu to reflect changes in the data.
|
||
|
//
|
||
|
// Implements: fyne.Widget
|
||
|
func (m *Menu) Refresh() {
|
||
|
for _, item := range m.Items {
|
||
|
item.Refresh()
|
||
|
}
|
||
|
m.BaseWidget.Refresh()
|
||
|
}
|
||
|
|
||
|
func (m *Menu) getContainsCheck() bool {
|
||
|
for _, item := range m.Items {
|
||
|
if mi, ok := item.(*menuItem); ok && mi.Item.Checked {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Tapped catches taps on separators and the menu background. It doesn't perform any action.
|
||
|
//
|
||
|
// Implements: fyne.Tappable
|
||
|
func (m *Menu) Tapped(*fyne.PointEvent) {
|
||
|
// Hit a separator or padding -> do nothing.
|
||
|
}
|
||
|
|
||
|
// TriggerLast finds the last active menu item traversing through the open submenus and triggers it.
|
||
|
func (m *Menu) TriggerLast() {
|
||
|
if m.activeItem == nil {
|
||
|
m.Dismiss()
|
||
|
return
|
||
|
}
|
||
|
m.activeItem.triggerLast()
|
||
|
}
|
||
|
|
||
|
// Dismiss dismisses the menu by dismissing and hiding the active child and performing OnDismiss.
|
||
|
func (m *Menu) Dismiss() {
|
||
|
if m.activeItem != nil {
|
||
|
if m.activeItem.Child() != nil {
|
||
|
defer m.activeItem.Child().Dismiss()
|
||
|
}
|
||
|
m.DeactivateChild()
|
||
|
}
|
||
|
if m.OnDismiss != nil {
|
||
|
m.OnDismiss()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Menu) activateItem(item *menuItem) {
|
||
|
if item.Child() != nil {
|
||
|
item.Child().DeactivateChild()
|
||
|
}
|
||
|
if m.activeItem == item {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
m.DeactivateChild()
|
||
|
m.activeItem = item
|
||
|
m.activeItem.Refresh()
|
||
|
if m.activeItem.child != nil {
|
||
|
m.Refresh()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Menu) setMenu(menu *fyne.Menu) {
|
||
|
m.Items = make([]fyne.CanvasObject, len(menu.Items))
|
||
|
for i, item := range menu.Items {
|
||
|
if item.IsSeparator {
|
||
|
m.Items[i] = NewSeparator()
|
||
|
} else {
|
||
|
m.Items[i] = newMenuItem(item, m)
|
||
|
}
|
||
|
}
|
||
|
m.containsCheck = m.getContainsCheck()
|
||
|
}
|
||
|
|
||
|
type menuRenderer struct {
|
||
|
*widget.ShadowingRenderer
|
||
|
box *menuBox
|
||
|
m *Menu
|
||
|
scroll *widget.Scroll
|
||
|
}
|
||
|
|
||
|
func (r *menuRenderer) Layout(s fyne.Size) {
|
||
|
minSize := r.MinSize()
|
||
|
var boxSize fyne.Size
|
||
|
if r.m.customSized {
|
||
|
boxSize = minSize.Max(s)
|
||
|
} else {
|
||
|
boxSize = minSize
|
||
|
}
|
||
|
scrollSize := boxSize
|
||
|
if c := fyne.CurrentApp().Driver().CanvasForObject(r.m.super()); c != nil {
|
||
|
ap := fyne.CurrentApp().Driver().AbsolutePositionForObject(r.m.super())
|
||
|
pos, size := c.InteractiveArea()
|
||
|
bottomPad := c.Size().Height - pos.Y - size.Height
|
||
|
if ah := c.Size().Height - bottomPad - ap.Y; ah < boxSize.Height {
|
||
|
scrollSize = fyne.NewSize(boxSize.Width, ah)
|
||
|
}
|
||
|
}
|
||
|
if scrollSize != r.m.Size() {
|
||
|
r.m.Resize(scrollSize)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
r.LayoutShadow(scrollSize, fyne.NewPos(0, 0))
|
||
|
r.scroll.Resize(scrollSize)
|
||
|
r.box.Resize(boxSize)
|
||
|
r.layoutActiveChild()
|
||
|
}
|
||
|
|
||
|
func (r *menuRenderer) MinSize() fyne.Size {
|
||
|
return r.box.MinSize()
|
||
|
}
|
||
|
|
||
|
func (r *menuRenderer) Refresh() {
|
||
|
r.layoutActiveChild()
|
||
|
r.ShadowingRenderer.RefreshShadow()
|
||
|
|
||
|
for _, i := range r.m.Items {
|
||
|
if txt, ok := i.(*menuItem); ok {
|
||
|
txt.alignment = r.m.alignment
|
||
|
txt.Refresh()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
canvas.Refresh(r.m)
|
||
|
}
|
||
|
|
||
|
func (r *menuRenderer) layoutActiveChild() {
|
||
|
item := r.m.activeItem
|
||
|
if item == nil || item.Child() == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if item.Child().Size().IsZero() {
|
||
|
item.Child().Resize(item.Child().MinSize())
|
||
|
}
|
||
|
|
||
|
itemSize := item.Size()
|
||
|
cp := fyne.NewPos(itemSize.Width, item.Position().Y)
|
||
|
d := fyne.CurrentApp().Driver()
|
||
|
c := d.CanvasForObject(item)
|
||
|
if c != nil {
|
||
|
absPos := d.AbsolutePositionForObject(item)
|
||
|
childSize := item.Child().Size()
|
||
|
if absPos.X+itemSize.Width+childSize.Width > c.Size().Width {
|
||
|
if absPos.X-childSize.Width >= 0 {
|
||
|
cp.X = -childSize.Width
|
||
|
} else {
|
||
|
cp.X = c.Size().Width - absPos.X - childSize.Width
|
||
|
}
|
||
|
}
|
||
|
requiredHeight := childSize.Height - theme.Padding()
|
||
|
availableHeight := c.Size().Height - absPos.Y
|
||
|
missingHeight := requiredHeight - availableHeight
|
||
|
if missingHeight > 0 {
|
||
|
cp.Y -= missingHeight
|
||
|
}
|
||
|
}
|
||
|
item.Child().Move(cp)
|
||
|
}
|
||
|
|
||
|
type menuBox struct {
|
||
|
BaseWidget
|
||
|
items []fyne.CanvasObject
|
||
|
}
|
||
|
|
||
|
var _ fyne.Widget = (*menuBox)(nil)
|
||
|
|
||
|
func newMenuBox(items []fyne.CanvasObject) *menuBox {
|
||
|
b := &menuBox{items: items}
|
||
|
b.ExtendBaseWidget(b)
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
func (b *menuBox) CreateRenderer() fyne.WidgetRenderer {
|
||
|
background := canvas.NewRectangle(theme.MenuBackgroundColor())
|
||
|
cont := &fyne.Container{Layout: layout.NewVBoxLayout(), Objects: b.items}
|
||
|
return &menuBoxRenderer{
|
||
|
BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{background, cont}),
|
||
|
b: b,
|
||
|
background: background,
|
||
|
cont: cont,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type menuBoxRenderer struct {
|
||
|
widget.BaseRenderer
|
||
|
b *menuBox
|
||
|
background *canvas.Rectangle
|
||
|
cont *fyne.Container
|
||
|
}
|
||
|
|
||
|
var _ fyne.WidgetRenderer = (*menuBoxRenderer)(nil)
|
||
|
|
||
|
func (r *menuBoxRenderer) Layout(size fyne.Size) {
|
||
|
s := fyne.NewSize(size.Width, size.Height)
|
||
|
r.background.Resize(s)
|
||
|
r.cont.Resize(s)
|
||
|
}
|
||
|
|
||
|
func (r *menuBoxRenderer) MinSize() fyne.Size {
|
||
|
return r.cont.MinSize()
|
||
|
}
|
||
|
|
||
|
func (r *menuBoxRenderer) Refresh() {
|
||
|
r.background.FillColor = theme.MenuBackgroundColor()
|
||
|
r.background.Refresh()
|
||
|
canvas.Refresh(r.b)
|
||
|
}
|