themes.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. // Copyright 2024 The tk9.0-go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package tk9_0 // import "modernc.org/tk9.0"
  5. import (
  6. "errors"
  7. "fmt"
  8. "reflect"
  9. "sort"
  10. "strings"
  11. )
  12. var (
  13. // Themes register Tk themes. User code must not directly mutate Themes.
  14. Themes = map[ThemeKey]Theme{}
  15. AlreadyActivated = errors.New("Already activated")
  16. AlreadyRegistered = errors.New("Already registered")
  17. Finalized = errors.New("Finalized")
  18. NotActivated = errors.New("Not activated")
  19. NotFound = errors.New("Not found")
  20. currentTheme Theme
  21. currentThemeKey ThemeKey
  22. _ Theme = (*theme)(nil)
  23. _ Theme = (*builtinTheme)(nil)
  24. _ ThemeContext = themeContext{}
  25. )
  26. // https://tkdocs.com/tutorial/styles.html
  27. //
  28. // Besides the built-in themes (alt, default, clam, and classic), macOS
  29. // includes a theme named aqua to match the system-wide style, while Windows
  30. // includes themes named vista, winxpnative, and winnative.
  31. func init() {
  32. RegisterTheme("alt", &builtinTheme{"alt"})
  33. RegisterTheme("default", &builtinTheme{"default"})
  34. RegisterTheme("clam", &builtinTheme{"clam"})
  35. RegisterTheme("classic", &builtinTheme{"classic"})
  36. switch goos {
  37. case "darwin":
  38. RegisterTheme("aqua", &builtinTheme{"aqua"})
  39. case "windows":
  40. RegisterTheme("vista", &builtinTheme{"vista"})
  41. RegisterTheme("winxpnative", &builtinTheme{"winxpnative"})
  42. RegisterTheme("winnative", &builtinTheme{"winnative"})
  43. }
  44. }
  45. type builtinTheme struct {
  46. name string
  47. }
  48. func (t *builtinTheme) Activate(context ThemeContext) error {
  49. StyleThemeUse(t.name)
  50. return nil
  51. }
  52. func (t *builtinTheme) Deactivate(context ThemeContext) error {
  53. StyleThemeUse("default")
  54. return nil
  55. }
  56. func (t *builtinTheme) Finalize(context ThemeContext) error {
  57. return nil
  58. }
  59. func (t *builtinTheme) Initialize(context ThemeContext) error {
  60. return nil
  61. }
  62. // CurrentTheme returns the currently activated theme, if any.
  63. func CurrentTheme() Theme {
  64. return currentTheme
  65. }
  66. // CurrentThemeName returns the name of the currently activated theme, if any.
  67. func CurrentThemeName() (r string) {
  68. return currentThemeKey.Name
  69. }
  70. // ActivateTheme searches [Themes] to find first theme named like 'name' and
  71. // call its Activate method. The search is case insensitive, using
  72. // strings.ToLower, and white space is normalized. If there's no match,
  73. // ActivateTheme returns [NotFound].
  74. //
  75. // Any package can register themes but only the main package can activate a
  76. // theme.
  77. func ActivateTheme(name string) (err error) {
  78. if !isCalledFromMain() {
  79. return NotActivated
  80. }
  81. var keys []ThemeKey
  82. for k := range Themes {
  83. keys = append(keys, k)
  84. }
  85. sort.Slice(keys, func(a, b int) bool {
  86. c, d := keys[a], keys[b]
  87. e, f := matchName(c.Name), matchName(d.Name)
  88. if e < f {
  89. return true
  90. }
  91. if e > f {
  92. return false
  93. }
  94. return c.Type < d.Type
  95. })
  96. name = matchName(name)
  97. for _, k := range keys {
  98. if matchName(k.Name) == name {
  99. return Themes[k].Activate(nil)
  100. }
  101. }
  102. return NotFound
  103. }
  104. func matchName(s string) string {
  105. return strings.Join(strings.Fields(strings.ToLower(s)), " ")
  106. }
  107. // RegisterTheme registers t.
  108. func RegisterTheme(name string, t Theme) (r ThemeKey, err error) {
  109. k := ThemeKey{Type: typeName(t), Name: name}
  110. if _, ok := Themes[k]; ok {
  111. return r, AlreadyRegistered
  112. }
  113. Themes[k] = &theme{inner: t, k: k}
  114. return k, nil
  115. }
  116. // ThemeKey indexes Themes
  117. type ThemeKey struct {
  118. Type string
  119. Name string
  120. }
  121. // ThemeContext provides context to Theme methods.
  122. type ThemeContext interface {
  123. Eval(tcl string) (r string, err error)
  124. }
  125. type themeContext struct{}
  126. func newThemeContext() (r themeContext) {
  127. evalFunc = eval
  128. return r
  129. }
  130. // Eval evaluates 'tcl' and returns a result value and an error, if any.
  131. func (themeContext) Eval(tcl string) (r string, err error) {
  132. return evalFunc(tcl)
  133. }
  134. var evalFunc func(string) (string, error)
  135. // Theme provides handling of a Tk theme. When calling Theme methods registered
  136. // in Theme, the context argument is ignored and an instance is created
  137. // automatically.
  138. type Theme interface {
  139. // Activate makes the theme active/in use. The Activate method of themes in
  140. // Themes automatically call Initialize if it was not called before.
  141. Activate(context ThemeContext) error
  142. // Deactivate makes the theme not active. Deactivate cannot be called before
  143. // Activate().
  144. Deactivate(context ThemeContext) error
  145. // Finalize is called to perform any cleanup. After Finalize returns, the theme
  146. // cannot be used. The Finalize method of themes in Themes automatically remove
  147. // the theme from Themes after Finalize completes.
  148. Finalize(context ThemeContext) error
  149. // Initialize is called to perform any one-time initialization of a theme. The
  150. // Initialize method of themes in Themes can be called multiple times but only
  151. // the first successful call to Initialize will have any effect.
  152. Initialize(context ThemeContext) error
  153. }
  154. type theme struct {
  155. inner Theme
  156. k ThemeKey
  157. activated bool
  158. finalized bool
  159. initialized bool
  160. }
  161. func (t *theme) Activate(context ThemeContext) (err error) {
  162. if currentTheme != nil {
  163. currentTheme.Deactivate(nil)
  164. currentTheme = nil
  165. currentThemeKey = ThemeKey{}
  166. }
  167. context = newThemeContext()
  168. defer func() {
  169. evalFunc = nil
  170. if err == nil {
  171. t.activated = true
  172. currentTheme = t
  173. currentThemeKey = t.k
  174. }
  175. }()
  176. if t.finalized {
  177. return Finalized
  178. }
  179. if t.activated {
  180. return AlreadyActivated
  181. }
  182. if !t.initialized {
  183. if err = t.inner.Initialize(context); err != nil {
  184. return err
  185. }
  186. }
  187. evalFunc = eval
  188. return t.inner.Activate(context)
  189. }
  190. func (t *theme) Deactivate(context ThemeContext) (err error) {
  191. context = newThemeContext()
  192. defer func() {
  193. evalFunc = nil
  194. t.activated = false
  195. currentTheme = nil
  196. currentThemeKey = ThemeKey{}
  197. }()
  198. if t.finalized {
  199. return Finalized
  200. }
  201. if !t.activated {
  202. return NotActivated
  203. }
  204. return t.inner.Deactivate(context)
  205. }
  206. func (t *theme) Finalize(context ThemeContext) (err error) {
  207. context = newThemeContext()
  208. defer func() {
  209. evalFunc = nil
  210. t.finalized = true
  211. delete(Themes, t.k)
  212. currentTheme = nil
  213. currentThemeKey = ThemeKey{}
  214. }()
  215. if !t.finalized {
  216. err = t.inner.Finalize(context)
  217. }
  218. return err
  219. }
  220. func (t *theme) Initialize(context ThemeContext) (err error) {
  221. context = newThemeContext()
  222. defer func() {
  223. evalFunc = nil
  224. if err == nil {
  225. t.initialized = true
  226. }
  227. }()
  228. if t.finalized {
  229. return Finalized
  230. }
  231. if !t.initialized {
  232. err = t.inner.Initialize(context)
  233. }
  234. return err
  235. }
  236. func typeName(th any) string {
  237. t := reflect.TypeOf(th)
  238. if t.Kind() == reflect.Pointer {
  239. t = t.Elem()
  240. }
  241. return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
  242. }