adam-gui/vendor/fyne.io/fyne/v2/widget/menu.go
2024-04-29 19:13:50 +02:00

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)
}