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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/README.md +452 -0
- data/lib/teek/ui/canvas_item.rb +270 -0
- data/lib/teek/ui/component_handle.rb +32 -0
- data/lib/teek/ui/document.rb +185 -0
- data/lib/teek/ui/errors.rb +41 -0
- data/lib/teek/ui/event_binding.rb +22 -0
- data/lib/teek/ui/event_bus.rb +53 -0
- data/lib/teek/ui/grid_validator.rb +89 -0
- data/lib/teek/ui/handle.rb +585 -0
- data/lib/teek/ui/image.rb +58 -0
- data/lib/teek/ui/keysyms.rb +72 -0
- data/lib/teek/ui/menu_builder.rb +130 -0
- data/lib/teek/ui/menu_entry_addressing.rb +62 -0
- data/lib/teek/ui/modal_stack.rb +99 -0
- data/lib/teek/ui/mouse_events.rb +31 -0
- data/lib/teek/ui/node.rb +141 -0
- data/lib/teek/ui/option_dump_parsing.rb +31 -0
- data/lib/teek/ui/overlay_anchors.rb +29 -0
- data/lib/teek/ui/overlay_validator.rb +33 -0
- data/lib/teek/ui/pane_validator.rb +30 -0
- data/lib/teek/ui/realized_node.rb +25 -0
- data/lib/teek/ui/realizer.rb +577 -0
- data/lib/teek/ui/scope.rb +40 -0
- data/lib/teek/ui/screens.rb +141 -0
- data/lib/teek/ui/session.rb +417 -0
- data/lib/teek/ui/tab_validator.rb +30 -0
- data/lib/teek/ui/text_content.rb +321 -0
- data/lib/teek/ui/tree_inspector.rb +97 -0
- data/lib/teek/ui/validator.rb +131 -0
- data/lib/teek/ui/var.rb +98 -0
- data/lib/teek/ui/version.rb +7 -0
- data/lib/teek/ui/widget_addressing.rb +47 -0
- data/lib/teek/ui/widget_dsl.rb +512 -0
- data/lib/teek/ui/widget_type.rb +242 -0
- data/lib/teek/ui/widget_types/button.rb +7 -0
- data/lib/teek/ui/widget_types/canvas.rb +15 -0
- data/lib/teek/ui/widget_types/checkbox.rb +7 -0
- data/lib/teek/ui/widget_types/column.rb +14 -0
- data/lib/teek/ui/widget_types/context_menu.rb +15 -0
- data/lib/teek/ui/widget_types/divider.rb +10 -0
- data/lib/teek/ui/widget_types/dropdown.rb +7 -0
- data/lib/teek/ui/widget_types/grid.rb +12 -0
- data/lib/teek/ui/widget_types/group.rb +7 -0
- data/lib/teek/ui/widget_types/label.rb +7 -0
- data/lib/teek/ui/widget_types/list.rb +7 -0
- data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
- data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
- data/lib/teek/ui/widget_types/menu_item.rb +20 -0
- data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
- data/lib/teek/ui/widget_types/number_box.rb +7 -0
- data/lib/teek/ui/widget_types/pane.rb +37 -0
- data/lib/teek/ui/widget_types/panel.rb +7 -0
- data/lib/teek/ui/widget_types/progress.rb +7 -0
- data/lib/teek/ui/widget_types/radio.rb +7 -0
- data/lib/teek/ui/widget_types/row.rb +14 -0
- data/lib/teek/ui/widget_types/scrollable.rb +17 -0
- data/lib/teek/ui/widget_types/slider.rb +7 -0
- data/lib/teek/ui/widget_types/spacer.rb +14 -0
- data/lib/teek/ui/widget_types/split.rb +17 -0
- data/lib/teek/ui/widget_types/tab.rb +35 -0
- data/lib/teek/ui/widget_types/table.rb +9 -0
- data/lib/teek/ui/widget_types/tabs.rb +10 -0
- data/lib/teek/ui/widget_types/text_area.rb +7 -0
- data/lib/teek/ui/widget_types/text_box.rb +7 -0
- data/lib/teek/ui/widget_types/tree.rb +9 -0
- data/lib/teek/ui/widget_types/window.rb +50 -0
- data/lib/teek/ui/widget_types.rb +121 -0
- data/lib/teek/ui/widget_validators.rb +62 -0
- data/lib/teek/ui.rb +54 -0
- data/teek-ui.gemspec +24 -0
- data/test/support/fake_app.rb +57 -0
- data/test/test_busy.rb +82 -0
- data/test/test_canvas_items.rb +551 -0
- data/test/test_clipboard.rb +87 -0
- data/test/test_close_handling.rb +118 -0
- data/test/test_component.rb +151 -0
- data/test/test_component_realtk.rb +120 -0
- data/test/test_debug_info.rb +158 -0
- data/test/test_document.rb +317 -0
- data/test/test_event_bus.rb +163 -0
- data/test/test_events.rb +164 -0
- data/test/test_fake_app_contract.rb +62 -0
- data/test/test_grid.rb +97 -0
- data/test/test_handle.rb +386 -0
- data/test/test_handle_destroy_realtk.rb +344 -0
- data/test/test_helper.rb +20 -0
- data/test/test_image.rb +24 -0
- data/test/test_image_realtk.rb +146 -0
- data/test/test_incremental_realize.rb +203 -0
- data/test/test_keysyms.rb +76 -0
- data/test/test_layout.rb +185 -0
- data/test/test_lazy_realize.rb +128 -0
- data/test/test_managed_window.rb +304 -0
- data/test/test_menu_dsl.rb +230 -0
- data/test/test_menu_entry_addressing.rb +73 -0
- data/test/test_menu_realize.rb +296 -0
- data/test/test_modal_handle.rb +147 -0
- data/test/test_modal_stack.rb +219 -0
- data/test/test_modal_stack_realtk.rb +131 -0
- data/test/test_native_scrollable.rb +250 -0
- data/test/test_node.rb +193 -0
- data/test/test_overlay.rb +139 -0
- data/test/test_raw.rb +58 -0
- data/test/test_reactive_vars.rb +118 -0
- data/test/test_realizer.rb +264 -0
- data/test/test_scope.rb +38 -0
- data/test/test_screens.rb +260 -0
- data/test/test_screens_realtk.rb +314 -0
- data/test/test_scrollable.rb +185 -0
- data/test/test_scrollable_wheel.rb +172 -0
- data/test/test_scrollbar_auto_hide.rb +127 -0
- data/test/test_split.rb +145 -0
- data/test/test_tabs.rb +201 -0
- data/test/test_text_content.rb +664 -0
- data/test/test_toast.rb +126 -0
- data/test/test_tree_inspector.rb +183 -0
- data/test/test_ui.rb +428 -0
- data/test/test_validator.rb +209 -0
- data/test/test_var.rb +32 -0
- data/test/test_widget_dsl.rb +621 -0
- data/test/test_widget_introspection.rb +126 -0
- data/test/test_widget_types.rb +429 -0
- data/test/test_widget_types_realtk.rb +82 -0
- data/test/test_widget_validators.rb +82 -0
- 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
|