menu.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. package widget
  2. import (
  3. "fyne.io/fyne/v2"
  4. "fyne.io/fyne/v2/canvas"
  5. "fyne.io/fyne/v2/internal/widget"
  6. "fyne.io/fyne/v2/layout"
  7. "fyne.io/fyne/v2/theme"
  8. )
  9. var _ fyne.Widget = (*Menu)(nil)
  10. var _ fyne.Tappable = (*Menu)(nil)
  11. // Menu is a widget for displaying a fyne.Menu.
  12. type Menu struct {
  13. BaseWidget
  14. alignment fyne.TextAlign
  15. Items []fyne.CanvasObject
  16. OnDismiss func()
  17. activeItem *menuItem
  18. customSized bool
  19. containsCheck bool
  20. }
  21. // NewMenu creates a new Menu.
  22. func NewMenu(menu *fyne.Menu) *Menu {
  23. m := &Menu{}
  24. m.ExtendBaseWidget(m)
  25. m.setMenu(menu)
  26. return m
  27. }
  28. // ActivateLastSubmenu finds the last active menu item traversing through the open submenus
  29. // and activates its submenu if any.
  30. // It returns `true` if there was a submenu and it was activated and `false` elsewhere.
  31. // Activating a submenu does show it and activate its first item.
  32. func (m *Menu) ActivateLastSubmenu() bool {
  33. if m.activeItem == nil {
  34. return false
  35. }
  36. if !m.activeItem.activateLastSubmenu() {
  37. return false
  38. }
  39. m.Refresh()
  40. return true
  41. }
  42. // ActivateNext activates the menu item following the currently active menu item.
  43. // If there is no menu item active, it activates the first menu item.
  44. // If there is no menu item after the current active one, it does nothing.
  45. // If a submenu is open, it delegates the activation to this submenu.
  46. func (m *Menu) ActivateNext() {
  47. if m.activeItem != nil && m.activeItem.isSubmenuOpen() {
  48. m.activeItem.Child().ActivateNext()
  49. return
  50. }
  51. found := m.activeItem == nil
  52. for _, item := range m.Items {
  53. if mItem, ok := item.(*menuItem); ok {
  54. if found {
  55. m.activateItem(mItem)
  56. return
  57. }
  58. if mItem == m.activeItem {
  59. found = true
  60. }
  61. }
  62. }
  63. }
  64. // ActivatePrevious activates the menu item preceding the currently active menu item.
  65. // If there is no menu item active, it activates the last menu item.
  66. // If there is no menu item before the current active one, it does nothing.
  67. // If a submenu is open, it delegates the activation to this submenu.
  68. func (m *Menu) ActivatePrevious() {
  69. if m.activeItem != nil && m.activeItem.isSubmenuOpen() {
  70. m.activeItem.Child().ActivatePrevious()
  71. return
  72. }
  73. found := m.activeItem == nil
  74. for i := len(m.Items) - 1; i >= 0; i-- {
  75. item := m.Items[i]
  76. if mItem, ok := item.(*menuItem); ok {
  77. if found {
  78. m.activateItem(mItem)
  79. return
  80. }
  81. if mItem == m.activeItem {
  82. found = true
  83. }
  84. }
  85. }
  86. }
  87. // CreateRenderer returns a new renderer for the menu.
  88. //
  89. // Implements: fyne.Widget
  90. func (m *Menu) CreateRenderer() fyne.WidgetRenderer {
  91. m.ExtendBaseWidget(m)
  92. box := newMenuBox(m.Items)
  93. scroll := widget.NewVScroll(box)
  94. scroll.SetMinSize(box.MinSize())
  95. objects := []fyne.CanvasObject{scroll}
  96. for _, i := range m.Items {
  97. if item, ok := i.(*menuItem); ok && item.Child() != nil {
  98. objects = append(objects, item.Child())
  99. }
  100. }
  101. return &menuRenderer{
  102. widget.NewShadowingRenderer(objects, widget.MenuLevel),
  103. box,
  104. m,
  105. scroll,
  106. }
  107. }
  108. // DeactivateChild deactivates the active menu item and hides its submenu if any.
  109. func (m *Menu) DeactivateChild() {
  110. if m.activeItem != nil {
  111. defer m.activeItem.Refresh()
  112. if c := m.activeItem.Child(); c != nil {
  113. c.Hide()
  114. }
  115. m.activeItem = nil
  116. }
  117. }
  118. // DeactivateLastSubmenu finds the last open submenu traversing through the open submenus,
  119. // deactivates its active item and hides it.
  120. // This also deactivates any submenus of the deactivated submenu.
  121. // It returns `true` if there was a submenu open and closed and `false` elsewhere.
  122. func (m *Menu) DeactivateLastSubmenu() bool {
  123. if m.activeItem == nil {
  124. return false
  125. }
  126. return m.activeItem.deactivateLastSubmenu()
  127. }
  128. // MinSize returns the minimal size of the menu.
  129. //
  130. // Implements: fyne.Widget
  131. func (m *Menu) MinSize() fyne.Size {
  132. m.ExtendBaseWidget(m)
  133. return m.BaseWidget.MinSize()
  134. }
  135. // Refresh updates the menu to reflect changes in the data.
  136. //
  137. // Implements: fyne.Widget
  138. func (m *Menu) Refresh() {
  139. for _, item := range m.Items {
  140. item.Refresh()
  141. }
  142. m.BaseWidget.Refresh()
  143. }
  144. func (m *Menu) getContainsCheck() bool {
  145. for _, item := range m.Items {
  146. if mi, ok := item.(*menuItem); ok && mi.Item.Checked {
  147. return true
  148. }
  149. }
  150. return false
  151. }
  152. // Tapped catches taps on separators and the menu background. It doesn't perform any action.
  153. //
  154. // Implements: fyne.Tappable
  155. func (m *Menu) Tapped(*fyne.PointEvent) {
  156. // Hit a separator or padding -> do nothing.
  157. }
  158. // TriggerLast finds the last active menu item traversing through the open submenus and triggers it.
  159. func (m *Menu) TriggerLast() {
  160. if m.activeItem == nil {
  161. m.Dismiss()
  162. return
  163. }
  164. m.activeItem.triggerLast()
  165. }
  166. // Dismiss dismisses the menu by dismissing and hiding the active child and performing OnDismiss.
  167. func (m *Menu) Dismiss() {
  168. if m.activeItem != nil {
  169. if m.activeItem.Child() != nil {
  170. defer m.activeItem.Child().Dismiss()
  171. }
  172. m.DeactivateChild()
  173. }
  174. if m.OnDismiss != nil {
  175. m.OnDismiss()
  176. }
  177. }
  178. func (m *Menu) activateItem(item *menuItem) {
  179. if item.Child() != nil {
  180. item.Child().DeactivateChild()
  181. }
  182. if m.activeItem == item {
  183. return
  184. }
  185. m.DeactivateChild()
  186. m.activeItem = item
  187. m.activeItem.Refresh()
  188. if m.activeItem.child != nil {
  189. m.Refresh()
  190. }
  191. }
  192. func (m *Menu) setMenu(menu *fyne.Menu) {
  193. m.Items = make([]fyne.CanvasObject, len(menu.Items))
  194. for i, item := range menu.Items {
  195. if item.IsSeparator {
  196. m.Items[i] = NewSeparator()
  197. } else {
  198. m.Items[i] = newMenuItem(item, m)
  199. }
  200. }
  201. m.containsCheck = m.getContainsCheck()
  202. }
  203. type menuRenderer struct {
  204. *widget.ShadowingRenderer
  205. box *menuBox
  206. m *Menu
  207. scroll *widget.Scroll
  208. }
  209. func (r *menuRenderer) Layout(s fyne.Size) {
  210. minSize := r.MinSize()
  211. var boxSize fyne.Size
  212. if r.m.customSized {
  213. boxSize = minSize.Max(s)
  214. } else {
  215. boxSize = minSize
  216. }
  217. scrollSize := boxSize
  218. if c := fyne.CurrentApp().Driver().CanvasForObject(r.m.super()); c != nil {
  219. ap := fyne.CurrentApp().Driver().AbsolutePositionForObject(r.m.super())
  220. pos, size := c.InteractiveArea()
  221. bottomPad := c.Size().Height - pos.Y - size.Height
  222. if ah := c.Size().Height - bottomPad - ap.Y; ah < boxSize.Height {
  223. scrollSize = fyne.NewSize(boxSize.Width, ah)
  224. }
  225. }
  226. if scrollSize != r.m.Size() {
  227. r.m.Resize(scrollSize)
  228. return
  229. }
  230. r.LayoutShadow(scrollSize, fyne.NewPos(0, 0))
  231. r.scroll.Resize(scrollSize)
  232. r.box.Resize(boxSize)
  233. r.layoutActiveChild()
  234. }
  235. func (r *menuRenderer) MinSize() fyne.Size {
  236. return r.box.MinSize()
  237. }
  238. func (r *menuRenderer) Refresh() {
  239. r.layoutActiveChild()
  240. r.ShadowingRenderer.RefreshShadow()
  241. for _, i := range r.m.Items {
  242. if txt, ok := i.(*menuItem); ok {
  243. txt.alignment = r.m.alignment
  244. txt.Refresh()
  245. }
  246. }
  247. canvas.Refresh(r.m)
  248. }
  249. func (r *menuRenderer) layoutActiveChild() {
  250. item := r.m.activeItem
  251. if item == nil || item.Child() == nil {
  252. return
  253. }
  254. if item.Child().Size().IsZero() {
  255. item.Child().Resize(item.Child().MinSize())
  256. }
  257. itemSize := item.Size()
  258. cp := fyne.NewPos(itemSize.Width, item.Position().Y)
  259. d := fyne.CurrentApp().Driver()
  260. c := d.CanvasForObject(item)
  261. if c != nil {
  262. absPos := d.AbsolutePositionForObject(item)
  263. childSize := item.Child().Size()
  264. if absPos.X+itemSize.Width+childSize.Width > c.Size().Width {
  265. if absPos.X-childSize.Width >= 0 {
  266. cp.X = -childSize.Width
  267. } else {
  268. cp.X = c.Size().Width - absPos.X - childSize.Width
  269. }
  270. }
  271. requiredHeight := childSize.Height - theme.Padding()
  272. availableHeight := c.Size().Height - absPos.Y
  273. missingHeight := requiredHeight - availableHeight
  274. if missingHeight > 0 {
  275. cp.Y -= missingHeight
  276. }
  277. }
  278. item.Child().Move(cp)
  279. }
  280. type menuBox struct {
  281. BaseWidget
  282. items []fyne.CanvasObject
  283. }
  284. var _ fyne.Widget = (*menuBox)(nil)
  285. func newMenuBox(items []fyne.CanvasObject) *menuBox {
  286. b := &menuBox{items: items}
  287. b.ExtendBaseWidget(b)
  288. return b
  289. }
  290. func (b *menuBox) CreateRenderer() fyne.WidgetRenderer {
  291. background := canvas.NewRectangle(theme.MenuBackgroundColor())
  292. cont := &fyne.Container{Layout: layout.NewVBoxLayout(), Objects: b.items}
  293. return &menuBoxRenderer{
  294. BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{background, cont}),
  295. b: b,
  296. background: background,
  297. cont: cont,
  298. }
  299. }
  300. type menuBoxRenderer struct {
  301. widget.BaseRenderer
  302. b *menuBox
  303. background *canvas.Rectangle
  304. cont *fyne.Container
  305. }
  306. var _ fyne.WidgetRenderer = (*menuBoxRenderer)(nil)
  307. func (r *menuBoxRenderer) Layout(size fyne.Size) {
  308. s := fyne.NewSize(size.Width, size.Height)
  309. r.background.Resize(s)
  310. r.cont.Resize(s)
  311. }
  312. func (r *menuBoxRenderer) MinSize() fyne.Size {
  313. return r.cont.MinSize()
  314. }
  315. func (r *menuBoxRenderer) Refresh() {
  316. r.background.FillColor = theme.MenuBackgroundColor()
  317. r.background.Refresh()
  318. canvas.Refresh(r.b)
  319. }