menu_item.go 9.3 KB

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