menu_item.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package widget
  2. import (
  3. "strings"
  4. "fyne.io/fyne/v2"
  5. "fyne.io/fyne/v2/canvas"
  6. "fyne.io/fyne/v2/driver/desktop"
  7. "fyne.io/fyne/v2/internal/widget"
  8. "fyne.io/fyne/v2/theme"
  9. )
  10. const (
  11. runeModifierAlt = '⌥'
  12. runeModifierControl = '⌃'
  13. runeModifierShift = '⇧'
  14. )
  15. var keySymbols = map[fyne.KeyName]rune{
  16. fyne.KeyBackspace: '⌫',
  17. fyne.KeyDelete: '⌦',
  18. fyne.KeyDown: '↓',
  19. fyne.KeyEnd: '↘',
  20. fyne.KeyEnter: '↩',
  21. fyne.KeyEscape: '⎋',
  22. fyne.KeyHome: '↖',
  23. fyne.KeyLeft: '←',
  24. fyne.KeyPageDown: '⇟',
  25. fyne.KeyPageUp: '⇞',
  26. fyne.KeyReturn: '↩',
  27. fyne.KeyRight: '→',
  28. fyne.KeySpace: '␣',
  29. fyne.KeyTab: '⇥',
  30. fyne.KeyUp: '↑',
  31. }
  32. var _ fyne.Widget = (*menuItem)(nil)
  33. // menuItem is a widget for displaying a fyne.menuItem.
  34. type menuItem struct {
  35. widget.Base
  36. Item *fyne.MenuItem
  37. Parent *Menu
  38. alignment fyne.TextAlign
  39. child *Menu
  40. }
  41. // newMenuItem creates a new menuItem.
  42. func newMenuItem(item *fyne.MenuItem, parent *Menu) *menuItem {
  43. i := &menuItem{Item: item, Parent: parent}
  44. i.alignment = parent.alignment
  45. i.ExtendBaseWidget(i)
  46. return i
  47. }
  48. func (i *menuItem) Child() *Menu {
  49. if i.Item.ChildMenu != nil && i.child == nil {
  50. child := NewMenu(i.Item.ChildMenu)
  51. child.Hide()
  52. child.OnDismiss = i.Parent.Dismiss
  53. i.child = child
  54. }
  55. return i.child
  56. }
  57. // CreateRenderer returns a new renderer for the menu item.
  58. //
  59. // Implements: fyne.Widget
  60. func (i *menuItem) CreateRenderer() fyne.WidgetRenderer {
  61. background := canvas.NewRectangle(theme.HoverColor())
  62. background.Hide()
  63. text := canvas.NewText(i.Item.Label, theme.ForegroundColor())
  64. text.Alignment = i.alignment
  65. objects := []fyne.CanvasObject{background, text}
  66. var expandIcon *canvas.Image
  67. if i.Item.ChildMenu != nil {
  68. expandIcon = canvas.NewImageFromResource(theme.MenuExpandIcon())
  69. objects = append(objects, expandIcon)
  70. }
  71. checkIcon := canvas.NewImageFromResource(theme.ConfirmIcon())
  72. if !i.Item.Checked {
  73. checkIcon.Hide()
  74. }
  75. var icon *canvas.Image
  76. if i.Item.Icon != nil {
  77. icon = canvas.NewImageFromResource(i.Item.Icon)
  78. objects = append(objects, icon)
  79. }
  80. var shortcutTexts []*canvas.Text
  81. if s, ok := i.Item.Shortcut.(fyne.KeyboardShortcut); ok {
  82. shortcutTexts = textsForShortcut(s)
  83. for _, t := range shortcutTexts {
  84. objects = append(objects, t)
  85. }
  86. }
  87. objects = append(objects, checkIcon)
  88. r := &menuItemRenderer{
  89. BaseRenderer: widget.NewBaseRenderer(objects),
  90. i: i,
  91. expandIcon: expandIcon,
  92. checkIcon: checkIcon,
  93. icon: icon,
  94. shortcutTexts: shortcutTexts,
  95. text: text,
  96. background: background,
  97. }
  98. r.Refresh() // ensure text and icon resources match state
  99. return r
  100. }
  101. // MouseIn activates the item which shows the submenu if the item has one.
  102. // The submenu of any sibling of the item will be hidden.
  103. //
  104. // Implements: desktop.Hoverable
  105. func (i *menuItem) MouseIn(*desktop.MouseEvent) {
  106. i.activate()
  107. }
  108. // MouseMoved does nothing.
  109. //
  110. // Implements: desktop.Hoverable
  111. func (i *menuItem) MouseMoved(*desktop.MouseEvent) {
  112. }
  113. // MouseOut deactivates the item unless it has an open submenu.
  114. //
  115. // Implements: desktop.Hoverable
  116. func (i *menuItem) MouseOut() {
  117. if !i.isSubmenuOpen() {
  118. i.deactivate()
  119. }
  120. }
  121. // Tapped performs the action of the item and dismisses the menu.
  122. // It does nothing if the item doesn’t have an action.
  123. //
  124. // Implements: fyne.Tappable
  125. func (i *menuItem) Tapped(*fyne.PointEvent) {
  126. if i.Item.Disabled {
  127. return
  128. }
  129. if i.Item.Action == nil {
  130. if fyne.CurrentDevice().IsMobile() {
  131. i.activate()
  132. }
  133. return
  134. }
  135. i.trigger()
  136. }
  137. func (i *menuItem) activate() {
  138. if i.Item.Disabled {
  139. return
  140. }
  141. if i.Child() != nil {
  142. i.Child().Show()
  143. }
  144. i.Parent.activateItem(i)
  145. }
  146. func (i *menuItem) activateLastSubmenu() bool {
  147. if i.Child() == nil {
  148. return false
  149. }
  150. if i.isSubmenuOpen() {
  151. return i.Child().ActivateLastSubmenu()
  152. }
  153. i.Child().Show()
  154. i.Child().ActivateNext()
  155. return true
  156. }
  157. func (i *menuItem) deactivate() {
  158. if i.Child() != nil {
  159. i.Child().Hide()
  160. }
  161. i.Parent.DeactivateChild()
  162. }
  163. func (i *menuItem) deactivateLastSubmenu() bool {
  164. if !i.isSubmenuOpen() {
  165. return false
  166. }
  167. if !i.Child().DeactivateLastSubmenu() {
  168. i.Child().DeactivateChild()
  169. i.Child().Hide()
  170. }
  171. return true
  172. }
  173. func (i *menuItem) isActive() bool {
  174. return i.Parent.activeItem == i
  175. }
  176. func (i *menuItem) isSubmenuOpen() bool {
  177. return i.Child() != nil && i.Child().Visible()
  178. }
  179. func (i *menuItem) trigger() {
  180. i.Parent.Dismiss()
  181. if i.Item.Action != nil {
  182. i.Item.Action()
  183. }
  184. }
  185. func (i *menuItem) triggerLast() {
  186. if i.isSubmenuOpen() {
  187. i.Child().TriggerLast()
  188. return
  189. }
  190. i.trigger()
  191. }
  192. type menuItemRenderer struct {
  193. widget.BaseRenderer
  194. i *menuItem
  195. background *canvas.Rectangle
  196. checkIcon *canvas.Image
  197. expandIcon *canvas.Image
  198. icon *canvas.Image
  199. lastThemePadding float32
  200. minSize fyne.Size
  201. shortcutTexts []*canvas.Text
  202. text *canvas.Text
  203. }
  204. func (r *menuItemRenderer) Layout(size fyne.Size) {
  205. leftOffset := theme.InnerPadding() + r.checkSpace()
  206. rightOffset := size.Width
  207. iconSize := fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize())
  208. iconTopOffset := (size.Height - theme.IconInlineSize()) / 2
  209. if r.expandIcon != nil {
  210. rightOffset -= theme.IconInlineSize()
  211. r.expandIcon.Resize(iconSize)
  212. r.expandIcon.Move(fyne.NewPos(rightOffset, iconTopOffset))
  213. }
  214. rightOffset -= theme.InnerPadding()
  215. for i := len(r.shortcutTexts) - 1; i >= 0; i-- {
  216. text := r.shortcutTexts[i]
  217. text.Resize(text.MinSize())
  218. rightOffset -= text.MinSize().Width
  219. text.Move(fyne.NewPos(rightOffset, theme.InnerPadding()))
  220. if i == 0 {
  221. rightOffset -= theme.InnerPadding()
  222. }
  223. }
  224. r.checkIcon.Resize(iconSize)
  225. r.checkIcon.Move(fyne.NewPos(theme.InnerPadding(), iconTopOffset))
  226. if r.icon != nil {
  227. r.icon.Resize(iconSize)
  228. r.icon.Move(fyne.NewPos(leftOffset, iconTopOffset))
  229. leftOffset += theme.IconInlineSize()
  230. leftOffset += theme.InnerPadding()
  231. }
  232. r.text.Resize(fyne.NewSize(rightOffset-leftOffset, r.text.MinSize().Height))
  233. r.text.Move(fyne.NewPos(leftOffset, theme.InnerPadding()))
  234. r.background.Resize(size)
  235. }
  236. func (r *menuItemRenderer) MinSize() fyne.Size {
  237. if r.minSizeUnchanged() {
  238. return r.minSize
  239. }
  240. minSize := r.text.MinSize().AddWidthHeight(theme.InnerPadding()*2+r.checkSpace(), theme.InnerPadding()*2)
  241. if r.expandIcon != nil {
  242. minSize = minSize.AddWidthHeight(theme.IconInlineSize(), 0)
  243. }
  244. if r.icon != nil {
  245. minSize = minSize.AddWidthHeight(theme.IconInlineSize()+theme.InnerPadding(), 0)
  246. }
  247. if r.shortcutTexts != nil {
  248. var textWidth float32
  249. for _, text := range r.shortcutTexts {
  250. textWidth += text.MinSize().Width
  251. }
  252. minSize = minSize.AddWidthHeight(textWidth+theme.InnerPadding(), 0)
  253. }
  254. r.minSize = minSize
  255. return r.minSize
  256. }
  257. func (r *menuItemRenderer) Refresh() {
  258. if fyne.CurrentDevice().IsMobile() {
  259. r.background.Hide()
  260. } else if r.i.isActive() {
  261. r.background.FillColor = theme.FocusColor()
  262. r.background.Show()
  263. } else {
  264. r.background.Hide()
  265. }
  266. r.background.Refresh()
  267. r.text.Alignment = r.i.alignment
  268. r.refreshText(r.text)
  269. for _, text := range r.shortcutTexts {
  270. r.refreshText(text)
  271. }
  272. if r.i.Item.Checked {
  273. r.checkIcon.Show()
  274. } else {
  275. r.checkIcon.Hide()
  276. }
  277. r.refreshIcon(r.checkIcon, theme.ConfirmIcon())
  278. r.refreshIcon(r.expandIcon, theme.MenuExpandIcon())
  279. r.refreshIcon(r.icon, r.i.Item.Icon)
  280. canvas.Refresh(r.i)
  281. }
  282. func (r *menuItemRenderer) checkSpace() float32 {
  283. if r.i.Parent.containsCheck {
  284. return theme.IconInlineSize() + theme.InnerPadding()
  285. }
  286. return 0
  287. }
  288. func (r *menuItemRenderer) minSizeUnchanged() bool {
  289. return !r.minSize.IsZero() &&
  290. r.text.TextSize == theme.TextSize() &&
  291. (r.expandIcon == nil || r.expandIcon.Size().Width == theme.IconInlineSize()) &&
  292. r.lastThemePadding == theme.InnerPadding()
  293. }
  294. func (r *menuItemRenderer) refreshIcon(img *canvas.Image, rsc fyne.Resource) {
  295. if img == nil {
  296. return
  297. }
  298. if r.i.Item.Disabled {
  299. img.Resource = theme.NewDisabledResource(rsc)
  300. } else {
  301. img.Resource = rsc
  302. }
  303. img.Refresh()
  304. }
  305. func (r *menuItemRenderer) refreshText(text *canvas.Text) {
  306. text.TextSize = theme.TextSize()
  307. if r.i.Item.Disabled {
  308. text.Color = theme.DisabledColor()
  309. } else {
  310. text.Color = theme.ForegroundColor()
  311. }
  312. text.Refresh()
  313. }
  314. func textsForShortcut(s fyne.KeyboardShortcut) (texts []*canvas.Text) {
  315. b := strings.Builder{}
  316. mods := s.Mod()
  317. if mods&fyne.KeyModifierControl != 0 {
  318. b.WriteRune(runeModifierControl)
  319. }
  320. if mods&fyne.KeyModifierAlt != 0 {
  321. b.WriteRune(runeModifierAlt)
  322. }
  323. if mods&fyne.KeyModifierShift != 0 {
  324. b.WriteRune(runeModifierShift)
  325. }
  326. if mods&fyne.KeyModifierSuper != 0 {
  327. b.WriteRune(runeModifierSuper)
  328. }
  329. r := keySymbols[s.Key()]
  330. if r != 0 {
  331. b.WriteRune(r)
  332. }
  333. t := canvas.NewText(b.String(), theme.ForegroundColor())
  334. t.TextStyle.Symbol = true
  335. texts = append(texts, t)
  336. if r == 0 {
  337. texts = append(texts, canvas.NewText(string(s.Key()), theme.ForegroundColor()))
  338. }
  339. return
  340. }