teek-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +452 -0
  4. data/lib/teek/ui/canvas_item.rb +270 -0
  5. data/lib/teek/ui/component_handle.rb +32 -0
  6. data/lib/teek/ui/document.rb +185 -0
  7. data/lib/teek/ui/errors.rb +41 -0
  8. data/lib/teek/ui/event_binding.rb +22 -0
  9. data/lib/teek/ui/event_bus.rb +53 -0
  10. data/lib/teek/ui/grid_validator.rb +89 -0
  11. data/lib/teek/ui/handle.rb +585 -0
  12. data/lib/teek/ui/image.rb +58 -0
  13. data/lib/teek/ui/keysyms.rb +72 -0
  14. data/lib/teek/ui/menu_builder.rb +130 -0
  15. data/lib/teek/ui/menu_entry_addressing.rb +62 -0
  16. data/lib/teek/ui/modal_stack.rb +99 -0
  17. data/lib/teek/ui/mouse_events.rb +31 -0
  18. data/lib/teek/ui/node.rb +141 -0
  19. data/lib/teek/ui/option_dump_parsing.rb +31 -0
  20. data/lib/teek/ui/overlay_anchors.rb +29 -0
  21. data/lib/teek/ui/overlay_validator.rb +33 -0
  22. data/lib/teek/ui/pane_validator.rb +30 -0
  23. data/lib/teek/ui/realized_node.rb +25 -0
  24. data/lib/teek/ui/realizer.rb +577 -0
  25. data/lib/teek/ui/scope.rb +40 -0
  26. data/lib/teek/ui/screens.rb +141 -0
  27. data/lib/teek/ui/session.rb +417 -0
  28. data/lib/teek/ui/tab_validator.rb +30 -0
  29. data/lib/teek/ui/text_content.rb +321 -0
  30. data/lib/teek/ui/tree_inspector.rb +97 -0
  31. data/lib/teek/ui/validator.rb +131 -0
  32. data/lib/teek/ui/var.rb +98 -0
  33. data/lib/teek/ui/version.rb +7 -0
  34. data/lib/teek/ui/widget_addressing.rb +47 -0
  35. data/lib/teek/ui/widget_dsl.rb +512 -0
  36. data/lib/teek/ui/widget_type.rb +242 -0
  37. data/lib/teek/ui/widget_types/button.rb +7 -0
  38. data/lib/teek/ui/widget_types/canvas.rb +15 -0
  39. data/lib/teek/ui/widget_types/checkbox.rb +7 -0
  40. data/lib/teek/ui/widget_types/column.rb +14 -0
  41. data/lib/teek/ui/widget_types/context_menu.rb +15 -0
  42. data/lib/teek/ui/widget_types/divider.rb +10 -0
  43. data/lib/teek/ui/widget_types/dropdown.rb +7 -0
  44. data/lib/teek/ui/widget_types/grid.rb +12 -0
  45. data/lib/teek/ui/widget_types/group.rb +7 -0
  46. data/lib/teek/ui/widget_types/label.rb +7 -0
  47. data/lib/teek/ui/widget_types/list.rb +7 -0
  48. data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
  49. data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
  50. data/lib/teek/ui/widget_types/menu_item.rb +20 -0
  51. data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
  52. data/lib/teek/ui/widget_types/number_box.rb +7 -0
  53. data/lib/teek/ui/widget_types/pane.rb +37 -0
  54. data/lib/teek/ui/widget_types/panel.rb +7 -0
  55. data/lib/teek/ui/widget_types/progress.rb +7 -0
  56. data/lib/teek/ui/widget_types/radio.rb +7 -0
  57. data/lib/teek/ui/widget_types/row.rb +14 -0
  58. data/lib/teek/ui/widget_types/scrollable.rb +17 -0
  59. data/lib/teek/ui/widget_types/slider.rb +7 -0
  60. data/lib/teek/ui/widget_types/spacer.rb +14 -0
  61. data/lib/teek/ui/widget_types/split.rb +17 -0
  62. data/lib/teek/ui/widget_types/tab.rb +35 -0
  63. data/lib/teek/ui/widget_types/table.rb +9 -0
  64. data/lib/teek/ui/widget_types/tabs.rb +10 -0
  65. data/lib/teek/ui/widget_types/text_area.rb +7 -0
  66. data/lib/teek/ui/widget_types/text_box.rb +7 -0
  67. data/lib/teek/ui/widget_types/tree.rb +9 -0
  68. data/lib/teek/ui/widget_types/window.rb +50 -0
  69. data/lib/teek/ui/widget_types.rb +121 -0
  70. data/lib/teek/ui/widget_validators.rb +62 -0
  71. data/lib/teek/ui.rb +54 -0
  72. data/teek-ui.gemspec +24 -0
  73. data/test/support/fake_app.rb +57 -0
  74. data/test/test_busy.rb +82 -0
  75. data/test/test_canvas_items.rb +551 -0
  76. data/test/test_clipboard.rb +87 -0
  77. data/test/test_close_handling.rb +118 -0
  78. data/test/test_component.rb +151 -0
  79. data/test/test_component_realtk.rb +120 -0
  80. data/test/test_debug_info.rb +158 -0
  81. data/test/test_document.rb +317 -0
  82. data/test/test_event_bus.rb +163 -0
  83. data/test/test_events.rb +164 -0
  84. data/test/test_fake_app_contract.rb +62 -0
  85. data/test/test_grid.rb +97 -0
  86. data/test/test_handle.rb +386 -0
  87. data/test/test_handle_destroy_realtk.rb +344 -0
  88. data/test/test_helper.rb +20 -0
  89. data/test/test_image.rb +24 -0
  90. data/test/test_image_realtk.rb +146 -0
  91. data/test/test_incremental_realize.rb +203 -0
  92. data/test/test_keysyms.rb +76 -0
  93. data/test/test_layout.rb +185 -0
  94. data/test/test_lazy_realize.rb +128 -0
  95. data/test/test_managed_window.rb +304 -0
  96. data/test/test_menu_dsl.rb +230 -0
  97. data/test/test_menu_entry_addressing.rb +73 -0
  98. data/test/test_menu_realize.rb +296 -0
  99. data/test/test_modal_handle.rb +147 -0
  100. data/test/test_modal_stack.rb +219 -0
  101. data/test/test_modal_stack_realtk.rb +131 -0
  102. data/test/test_native_scrollable.rb +250 -0
  103. data/test/test_node.rb +193 -0
  104. data/test/test_overlay.rb +139 -0
  105. data/test/test_raw.rb +58 -0
  106. data/test/test_reactive_vars.rb +118 -0
  107. data/test/test_realizer.rb +264 -0
  108. data/test/test_scope.rb +38 -0
  109. data/test/test_screens.rb +260 -0
  110. data/test/test_screens_realtk.rb +314 -0
  111. data/test/test_scrollable.rb +185 -0
  112. data/test/test_scrollable_wheel.rb +172 -0
  113. data/test/test_scrollbar_auto_hide.rb +127 -0
  114. data/test/test_split.rb +145 -0
  115. data/test/test_tabs.rb +201 -0
  116. data/test/test_text_content.rb +664 -0
  117. data/test/test_toast.rb +126 -0
  118. data/test/test_tree_inspector.rb +183 -0
  119. data/test/test_ui.rb +428 -0
  120. data/test/test_validator.rb +209 -0
  121. data/test/test_var.rb +32 -0
  122. data/test/test_widget_dsl.rb +621 -0
  123. data/test/test_widget_introspection.rb +126 -0
  124. data/test/test_widget_types.rb +429 -0
  125. data/test/test_widget_types_realtk.rb +82 -0
  126. data/test/test_widget_validators.rb +82 -0
  127. metadata +207 -0
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+
6
+ # ui.window/ui.dialog are a DSL primitive for managed toplevels: title/
7
+ # geometry/resizable/transient setup at realize, withdrawn by default,
8
+ # shown/hidden via Handle#show/#hide - which position near their parent,
9
+ # deiconify+raise, and (when declared modal: true) grab+focus/release,
10
+ # reusing the modal primitive already built on the handle rather than
11
+ # reimplementing any of it.
12
+ class TestManagedWindow < Minitest::Test
13
+ include TeekTestHelper
14
+
15
+ def test_window_is_withdrawn_by_default_after_realize
16
+ assert_tk_app("a ui.window should start withdrawn, not visible, until shown") do
17
+ require 'teek/ui'
18
+
19
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings) }
20
+ session.run_async
21
+ session.app.update
22
+
23
+ refute session.app.winfo.ismapped?(session[:settings].path),
24
+ "a freshly realized window should be withdrawn by default"
25
+
26
+ session.app.destroy
27
+ end
28
+ end
29
+
30
+ def test_title_and_geometry_are_applied_at_realize
31
+ assert_tk_app("title:/geometry: opts should be applied via wm at realize") do
32
+ require 'teek/ui'
33
+
34
+ session = Teek::UI.app(title: 'Managed Window Test') do |ui|
35
+ ui.window(:settings, title: 'Settings', geometry: '300x200')
36
+ end
37
+ session.run_async
38
+ session.app.update
39
+
40
+ path = session[:settings].path
41
+ assert_equal 'Settings', session.app.tcl_eval("wm title #{path}")
42
+
43
+ # geometry requested while withdrawn doesn't reliably read back until
44
+ # the window is actually mapped and idle tasks run - same reason
45
+ # base teek's own test_set_window_geometry shows the window first.
46
+ session[:settings].show
47
+ session.app.update_idletasks
48
+ assert_includes session.app.tcl_eval("wm geometry #{path}"), '300x200'
49
+
50
+ session.app.destroy
51
+ end
52
+ end
53
+
54
+ def test_resizable_opt_accepts_a_single_boolean_for_both_axes
55
+ assert_tk_app("resizable: false should set both width and height non-resizable") do
56
+ require 'teek/ui'
57
+
58
+ session = Teek::UI.app(title: 'Managed Window Test') do |ui|
59
+ ui.window(:settings, resizable: false)
60
+ end
61
+ session.run_async
62
+ session.app.update
63
+
64
+ assert_equal '0 0', session.app.tcl_eval("wm resizable #{session[:settings].path}")
65
+
66
+ session.app.destroy
67
+ end
68
+ end
69
+
70
+ def test_resizable_opt_accepts_a_width_height_pair
71
+ assert_tk_app("resizable: [true, false] should set width/height independently") do
72
+ require 'teek/ui'
73
+
74
+ session = Teek::UI.app(title: 'Managed Window Test') do |ui|
75
+ ui.window(:settings, resizable: [true, false])
76
+ end
77
+ session.run_async
78
+ session.app.update
79
+
80
+ assert_equal '1 0', session.app.tcl_eval("wm resizable #{session[:settings].path}")
81
+
82
+ session.app.destroy
83
+ end
84
+ end
85
+
86
+ def test_window_defaults_to_leaving_resizable_untouched
87
+ assert_tk_app("without a resizable: opt, Tk's own default (resizable both ways) should apply") do
88
+ require 'teek/ui'
89
+
90
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings) }
91
+ session.run_async
92
+ session.app.update
93
+
94
+ assert_equal '1 1', session.app.tcl_eval("wm resizable #{session[:settings].path}")
95
+
96
+ session.app.destroy
97
+ end
98
+ end
99
+
100
+ def test_window_is_transient_to_its_parent_by_default
101
+ assert_tk_app("a top-level ui.window should be transient to the root window by default") do
102
+ require 'teek/ui'
103
+
104
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings) }
105
+ session.run_async
106
+ session.app.update
107
+
108
+ assert_equal '.', session.app.tcl_eval("wm transient #{session[:settings].path}")
109
+
110
+ session.app.destroy
111
+ end
112
+ end
113
+
114
+ def test_nested_window_is_transient_to_its_enclosing_window_not_the_root
115
+ assert_tk_app("a ui.window nested inside another ui.window should be transient to that window") do
116
+ require 'teek/ui'
117
+
118
+ session = Teek::UI.app(title: 'Managed Window Test') do |ui|
119
+ ui.window(:outer) { |o| o.window(:inner) }
120
+ end
121
+ session.run_async
122
+ session.app.update
123
+
124
+ outer_path = session[:outer].path
125
+ inner_path = session[:inner].path
126
+ assert_equal outer_path, session.app.tcl_eval("wm transient #{inner_path}")
127
+
128
+ session.app.destroy
129
+ end
130
+ end
131
+
132
+ def test_transient_false_disables_it
133
+ assert_tk_app("transient: false should leave the window with no transient parent") do
134
+ require 'teek/ui'
135
+
136
+ session = Teek::UI.app(title: 'Managed Window Test') do |ui|
137
+ ui.window(:settings, transient: false)
138
+ end
139
+ session.run_async
140
+ session.app.update
141
+
142
+ assert_equal '', session.app.tcl_eval("wm transient #{session[:settings].path}")
143
+
144
+ session.app.destroy
145
+ end
146
+ end
147
+
148
+ def test_show_deiconifies_raises_and_positions_near_its_parent
149
+ assert_tk_app("show should map the window and position it just to the right of its parent") do
150
+ require 'teek/ui'
151
+
152
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings) }
153
+ session.run_async
154
+ session.app.update
155
+
156
+ root_x, root_y, root_w, = session.app.interp.window_geometry('.')
157
+ session[:settings].show
158
+ session.app.update
159
+
160
+ assert session.app.winfo.ismapped?(session[:settings].path)
161
+ settings_x, settings_y, = session.app.interp.window_geometry(session[:settings].path)
162
+ assert_equal root_x + root_w + 12, settings_x
163
+ assert_equal root_y, settings_y
164
+
165
+ session.app.destroy
166
+ end
167
+ end
168
+
169
+ def test_hide_withdraws_the_window
170
+ assert_tk_app("hide should withdraw a previously shown window") do
171
+ require 'teek/ui'
172
+
173
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings) }
174
+ session.run_async
175
+ session.app.update
176
+
177
+ session[:settings].show
178
+ session.app.update
179
+ assert session.app.winfo.ismapped?(session[:settings].path)
180
+
181
+ session[:settings].hide
182
+ session.app.update
183
+ refute session.app.winfo.ismapped?(session[:settings].path)
184
+
185
+ session.app.destroy
186
+ end
187
+ end
188
+
189
+ def test_show_grabs_and_focuses_when_declared_modal
190
+ assert_tk_app("show should grab input and force focus when the window was declared modal: true") do
191
+ require 'teek/ui'
192
+
193
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings, modal: true) }
194
+ session.run_async
195
+ session.app.update
196
+
197
+ session[:settings].show
198
+ session.app.update
199
+
200
+ path = session[:settings].path
201
+ assert_equal path, session.app.tcl_eval("grab current #{path}")
202
+ assert_equal path, session.app.tcl_eval('focus')
203
+
204
+ session.app.destroy
205
+ end
206
+ end
207
+
208
+ def test_show_does_not_grab_when_not_declared_modal
209
+ assert_tk_app("show should not grab input for an ordinary (non-modal) window") do
210
+ require 'teek/ui'
211
+
212
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings) }
213
+ session.run_async
214
+ session.app.update
215
+
216
+ session[:settings].show
217
+ session.app.update
218
+
219
+ assert_equal '', session.app.tcl_eval("grab current #{session[:settings].path}")
220
+
221
+ session.app.destroy
222
+ end
223
+ end
224
+
225
+ def test_hide_releases_the_grab_for_a_modal_window
226
+ assert_tk_app("hide should release the grab a modal show set") do
227
+ require 'teek/ui'
228
+
229
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.window(:settings, modal: true) }
230
+ session.run_async
231
+ session.app.update
232
+
233
+ session[:settings].show
234
+ session.app.update
235
+ path = session[:settings].path
236
+ assert_equal path, session.app.tcl_eval("grab current #{path}")
237
+
238
+ session[:settings].hide
239
+ session.app.update
240
+ assert_equal '', session.app.tcl_eval("grab current #{path}")
241
+
242
+ session.app.destroy
243
+ end
244
+ end
245
+
246
+ def test_dialog_defaults_to_modal_and_non_resizable
247
+ assert_tk_app("ui.dialog should default to modal: true, resizable: false") do
248
+ require 'teek/ui'
249
+
250
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.dialog(:confirm) }
251
+ session.run_async
252
+ session.app.update
253
+
254
+ path = session[:confirm].path
255
+ assert_equal :window, session[:confirm].type
256
+ assert_equal '0 0', session.app.tcl_eval("wm resizable #{path}")
257
+
258
+ session[:confirm].show
259
+ session.app.update
260
+ assert_equal path, session.app.tcl_eval("grab current #{path}")
261
+
262
+ session.app.destroy
263
+ end
264
+ end
265
+
266
+ def test_dialog_defaults_are_overridable
267
+ assert_tk_app("ui.dialog's modal:/resizable: defaults should still be overridable") do
268
+ require 'teek/ui'
269
+
270
+ session = Teek::UI.app(title: 'Managed Window Test') do |ui|
271
+ ui.dialog(:confirm, modal: false, resizable: true)
272
+ end
273
+ session.run_async
274
+ session.app.update
275
+
276
+ path = session[:confirm].path
277
+ assert_equal '1 1', session.app.tcl_eval("wm resizable #{path}")
278
+
279
+ session[:confirm].show
280
+ session.app.update
281
+ assert_equal '', session.app.tcl_eval("grab current #{path}")
282
+
283
+ session.app.destroy
284
+ end
285
+ end
286
+
287
+ def test_show_and_hide_raise_on_a_non_window_handle
288
+ assert_tk_app("show/hide on a non-window handle should raise a clear error") do
289
+ require 'teek/ui'
290
+
291
+ session = Teek::UI.app(title: 'Managed Window Test') { |ui| ui.button(:go, text: 'Go') }
292
+ session.run_async
293
+ session.app.update
294
+
295
+ show_error = assert_raises(ArgumentError) { session[:go].show }
296
+ assert_match(/window/i, show_error.message)
297
+
298
+ hide_error = assert_raises(ArgumentError) { session[:go].hide }
299
+ assert_match(/window/i, hide_error.message)
300
+
301
+ session.app.destroy
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+ require 'teek/ui/session'
5
+
6
+ class TestMenuDsl < Minitest::Test
7
+ def build_session
8
+ Teek::UI::Session.new(title: 'Menu DSL Test')
9
+ end
10
+
11
+ def test_menu_bar_creates_a_menu_bar_node_at_the_top_level
12
+ session = build_session
13
+
14
+ handle = session.menu_bar { |mb| mb.menu(label: 'File') }
15
+
16
+ node = session.document.root.children.first
17
+ assert_equal :menu_bar, node.type
18
+ assert_kind_of Teek::UI::Handle, handle
19
+ assert_equal :menu_bar, handle.type
20
+ end
21
+
22
+ def test_menu_bar_is_addressable_when_named
23
+ session = build_session
24
+
25
+ session.menu_bar(:mb) { |mb| mb.menu(label: 'File') }
26
+
27
+ handle = session[:mb]
28
+ assert_kind_of Teek::UI::Handle, handle
29
+ assert_equal :menu_bar, handle.type
30
+ end
31
+
32
+ def test_menu_bar_raises_when_declared_inside_a_regular_container
33
+ session = build_session
34
+
35
+ error = assert_raises(ArgumentError) do
36
+ session.panel(:p) { |p| p.menu_bar { |mb| mb.menu(label: 'File') } }
37
+ end
38
+ assert_match(/menu_bar/, error.message)
39
+ end
40
+
41
+ def test_menu_bar_is_allowed_directly_inside_a_window
42
+ session = build_session
43
+
44
+ session.window(:settings) { |w| w.menu_bar { |mb| mb.menu(label: 'File') } }
45
+
46
+ window_node = session.document.root.children.first
47
+ menu_bar_node = window_node.children.first
48
+ assert_equal :menu_bar, menu_bar_node.type
49
+ end
50
+
51
+ def test_menu_declares_a_nested_cascade_node_with_a_label
52
+ session = build_session
53
+
54
+ session.menu_bar { |mb| mb.menu(label: 'File') }
55
+
56
+ menu_bar_node = session.document.root.children.first
57
+ file_node = menu_bar_node.children.first
58
+ assert_equal :menu, file_node.type
59
+ assert_equal 'File', file_node.opts[:label]
60
+ end
61
+
62
+ def test_menu_is_addressable_when_named
63
+ session = build_session
64
+
65
+ session.menu_bar { |mb| mb.menu(:file, label: 'File') }
66
+
67
+ handle = session[:file]
68
+ assert_kind_of Teek::UI::Handle, handle
69
+ assert_equal :menu, handle.type
70
+ end
71
+
72
+ def test_menu_nests_recursively_for_submenus
73
+ session = build_session
74
+
75
+ session.menu_bar do |mb|
76
+ mb.menu(label: 'File') do |file|
77
+ file.menu(:recent, label: 'Recent') do |recent|
78
+ recent.item(label: 'doc1.txt') { }
79
+ end
80
+ end
81
+ end
82
+
83
+ file_node = session.document.root.children.first.children.first
84
+ recent_node = file_node.children.first
85
+ assert_equal :menu, recent_node.type
86
+ assert_equal 'Recent', recent_node.opts[:label]
87
+ assert_equal [:menu_item], recent_node.children.map(&:type)
88
+ end
89
+
90
+ def test_item_appends_a_command_entry_with_a_command_block
91
+ session = build_session
92
+ fired = false
93
+
94
+ session.menu_bar { |mb| mb.menu(label: 'File') { |f| f.item(label: 'Open') { fired = true } } }
95
+
96
+ item_node = session.document.root.children.first.children.first.children.first
97
+ assert_equal :menu_item, item_node.type
98
+ assert_equal 'Open', item_node.opts[:label]
99
+ item_node.opts[:command].call
100
+ assert fired, "the item's block should be stashed as opts[:command]"
101
+ end
102
+
103
+ def test_item_is_addressable_when_named
104
+ session = build_session
105
+
106
+ session.menu_bar { |mb| mb.menu(label: 'File') { |f| f.item(:quick_load, label: 'Quick Load') { } } }
107
+
108
+ handle = session[:quick_load]
109
+ assert_kind_of Teek::UI::Handle, handle
110
+ assert_equal :menu_item, handle.type
111
+ end
112
+
113
+ def test_item_returns_a_handle_even_when_unnamed
114
+ session = build_session
115
+ handle = nil
116
+
117
+ session.menu_bar { |mb| mb.menu(label: 'File') { |f| handle = f.item(label: 'Open') { } } }
118
+
119
+ assert_kind_of Teek::UI::Handle, handle
120
+ end
121
+
122
+ def test_item_without_a_block_has_no_command_opt
123
+ session = build_session
124
+
125
+ session.menu_bar { |mb| mb.menu(label: 'File') { |f| f.item(label: 'Disabled') } }
126
+
127
+ item_node = session.document.root.children.first.children.first.children.first
128
+ refute item_node.opts.key?(:command)
129
+ end
130
+
131
+ def test_item_passes_through_extra_opts_like_accelerator
132
+ session = build_session
133
+
134
+ session.menu_bar { |mb| mb.menu(label: 'File') { |f| f.item(label: 'Save', accelerator: 'Ctrl+S') { } } }
135
+
136
+ item_node = session.document.root.children.first.children.first.children.first
137
+ assert_equal 'Ctrl+S', item_node.opts[:accelerator]
138
+ end
139
+
140
+ def test_separator_appends_a_separator_entry
141
+ session = build_session
142
+
143
+ session.menu_bar { |mb| mb.menu(label: 'File') { |f| f.separator } }
144
+
145
+ node = session.document.root.children.first.children.first.children.first
146
+ assert_equal :menu_separator, node.type
147
+ assert_equal({}, node.opts)
148
+ end
149
+
150
+ def test_checkbox_appends_a_checkbutton_entry_bound_to_a_var
151
+ session = build_session
152
+ wrap = session.var(true)
153
+
154
+ session.menu_bar { |mb| mb.menu(label: 'Edit') { |e| e.checkbox(label: 'Word Wrap', bind: wrap) } }
155
+
156
+ node = session.document.root.children.first.children.first.children.first
157
+ assert_equal :menu_checkbox, node.type
158
+ assert_equal 'Word Wrap', node.opts[:label]
159
+ assert_same wrap, node.opts[:bind]
160
+ end
161
+
162
+ def test_checkbox_is_addressable_when_named
163
+ session = build_session
164
+ wrap = session.var(true)
165
+
166
+ session.menu_bar { |mb| mb.menu(label: 'Edit') { |e| e.checkbox(:word_wrap, label: 'Word Wrap', bind: wrap) } }
167
+
168
+ handle = session[:word_wrap]
169
+ assert_kind_of Teek::UI::Handle, handle
170
+ assert_equal :menu_checkbox, handle.type
171
+ end
172
+
173
+ def test_radio_is_addressable_when_named
174
+ session = build_session
175
+ size = session.var('small')
176
+
177
+ session.menu_bar { |mb| mb.menu(label: 'Edit') { |e| e.radio(:small_size, label: 'Small', bind: size, value: 'small') } }
178
+
179
+ handle = session[:small_size]
180
+ assert_kind_of Teek::UI::Handle, handle
181
+ assert_equal :menu_radio, handle.type
182
+ end
183
+
184
+ def test_radio_appends_a_radiobutton_entry_bound_to_a_var_with_a_value
185
+ session = build_session
186
+ size = session.var('small')
187
+
188
+ session.menu_bar { |mb| mb.menu(label: 'Edit') { |e| e.radio(label: 'Small', bind: size, value: 'small') } }
189
+
190
+ node = session.document.root.children.first.children.first.children.first
191
+ assert_equal :menu_radio, node.type
192
+ assert_equal 'small', node.opts[:value]
193
+ assert_same size, node.opts[:bind]
194
+ end
195
+
196
+ def test_context_menu_creates_a_context_menu_node
197
+ session = build_session
198
+
199
+ handle = session.context_menu(:ctx) { |m| m.item(label: 'Delete') { } }
200
+
201
+ node = session.document.root.children.first
202
+ assert_equal :context_menu, node.type
203
+ assert_kind_of Teek::UI::Handle, handle
204
+ assert_equal :ctx, handle.name
205
+ end
206
+
207
+ def test_context_menu_can_hold_submenus_and_entries_like_any_menu
208
+ session = build_session
209
+
210
+ session.context_menu(:ctx) do |m|
211
+ m.item(label: 'Delete') { }
212
+ m.menu(label: 'Send to') { |sm| sm.item(label: 'Archive') { } }
213
+ end
214
+
215
+ ctx_node = session.document.root.children.first
216
+ assert_equal %i[menu_item menu], ctx_node.children.map(&:type)
217
+ end
218
+
219
+ def test_menu_builder_does_not_expose_ordinary_widget_methods
220
+ session = build_session
221
+
222
+ session.menu_bar do |mb|
223
+ # MenuBuilder is a distinct vocabulary, not the top-level widget DSL -
224
+ # #checkbox/#radio here mean menu entries, not ttk::checkbutton/
225
+ # ttk::radiobutton widgets, so it must not carry those methods too.
226
+ refute mb.respond_to?(:button), "MenuBuilder should not carry the top-level widget vocabulary"
227
+ assert mb.respond_to?(:checkbox), "MenuBuilder should define its own (menu-flavored) #checkbox"
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../../test/tk_test_helper'
5
+ require 'teek/ui/node'
6
+ require 'teek/ui/realized_node'
7
+ require 'teek/ui/menu_entry_addressing'
8
+
9
+ # MenuEntryAddressing is the WidgetType#addressing strategy for :menu_item/
10
+ # :menu_checkbox/:menu_radio - a menu entry has no independent Tk path of
11
+ # its own, only the enclosing menu does, so this resolves the entry's
12
+ # CURRENT position fresh on every call via Node#parent instead of caching
13
+ # an index (Tk menu entries are addressed purely by numeric index, and
14
+ # TkMenu.c renumbers every entry after the one that changed).
15
+ class TestMenuEntryAddressing < Minitest::Test
16
+ include TeekTestHelper
17
+
18
+ def test_virtual_path_marks_past_the_real_tk_path
19
+ menu_node = Teek::UI::Node.new(type: :menu, name: :file_menu)
20
+ menu_node.realized = Teek::UI::RealizedNode.new(app: :fake_app, path: '.menu_bar.file_menu')
21
+ entry = menu_node.add_child(Teek::UI::Node.new(type: :menu_item, name: :quick_load))
22
+
23
+ addressing = Teek::UI::MenuEntryAddressing.new(entry)
24
+
25
+ assert_equal '.menu_bar.file_menu!quick_load', addressing.virtual_path
26
+ end
27
+
28
+ def test_configure_raises_before_the_parent_menu_is_realized
29
+ menu_node = Teek::UI::Node.new(type: :menu, name: :file_menu)
30
+ entry = menu_node.add_child(Teek::UI::Node.new(type: :menu_item, name: :quick_load))
31
+
32
+ addressing = Teek::UI::MenuEntryAddressing.new(entry)
33
+
34
+ assert_raises(Teek::UI::NotRealizedError) { addressing.configure(state: :disabled) }
35
+ end
36
+
37
+ def test_configure_targets_the_live_current_index_immune_to_earlier_sibling_removal
38
+ assert_tk_app("configure should entryconfigure the entry at its CURRENT live index, not a stale cached one") do
39
+ require 'teek/ui/node'
40
+ require 'teek/ui/realized_node'
41
+ require 'teek/ui/menu_entry_addressing'
42
+
43
+ menu_path = '.testmenu1'
44
+ app.menu(menu_path)
45
+ app.command(menu_path, :add, :command, label: 'First')
46
+ app.command(menu_path, :add, :command, label: 'Second')
47
+ app.command(menu_path, :add, :command, label: 'Third')
48
+
49
+ menu_node = Teek::UI::Node.new(type: :menu, name: :testmenu1)
50
+ menu_node.realized = Teek::UI::RealizedNode.new(app: app, path: menu_path)
51
+
52
+ first_entry = menu_node.add_child(Teek::UI::Node.new(type: :menu_item, name: :first))
53
+ menu_node.add_child(Teek::UI::Node.new(type: :menu_item, name: :second))
54
+ third_entry = menu_node.add_child(Teek::UI::Node.new(type: :menu_item, name: :third))
55
+
56
+ # Removing First shifts Second and Third down one live index each -
57
+ # Tk itself renumbers around the delete; nothing in teek does the
58
+ # shifting. Node#parent.children.index(node) just re-reads whatever
59
+ # the CURRENT position is, so it's automatically correct with no
60
+ # bookkeeping - a stale index cached at creation time (2, for Third)
61
+ # would now be wrong.
62
+ app.command(menu_path, :delete, 0)
63
+ menu_node.children.delete(first_entry)
64
+
65
+ Teek::UI::MenuEntryAddressing.new(third_entry).configure(state: :disabled)
66
+
67
+ assert_equal 'disabled', app.tcl_eval("#{menu_path} entrycget 1 -state"),
68
+ "Third's live index is now 1 (after First was removed) - entry 1 should be disabled"
69
+ assert_equal 'normal', app.tcl_eval("#{menu_path} entrycget 0 -state"),
70
+ "Second's live index is now 0 - it should be untouched"
71
+ end
72
+ end
73
+ end