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,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# ui.scrollable wraps arbitrary content in a scrolled frame - a canvas +
|
|
7
|
+
# embedded viewport, since arbitrary widgets have no Tk scrolling protocol
|
|
8
|
+
# of their own to hook a scrollbar into. A bare list/text_area/table/
|
|
9
|
+
# tree/canvas auto-attaches a scrollbar wherever it's declared instead,
|
|
10
|
+
# with no ui.scrollable wrapper needed - see test_native_scrollable.rb.
|
|
11
|
+
# See Realizer#create_scrollable.
|
|
12
|
+
class TestScrollable < Minitest::Test
|
|
13
|
+
include TeekTestHelper
|
|
14
|
+
|
|
15
|
+
def test_wraps_content_in_a_canvas_and_embedded_viewport
|
|
16
|
+
assert_tk_app("a scrollable panel of arbitrary widgets should be wrapped in a canvas + embedded frame") do
|
|
17
|
+
require 'teek/ui'
|
|
18
|
+
|
|
19
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
20
|
+
ui.scrollable(:region) do |s|
|
|
21
|
+
s.column(:rows) { |c| 30.times { |i| c.button(text: "Row #{i}") } }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
session.run_async
|
|
25
|
+
session.app.update
|
|
26
|
+
session.app.update_idletasks
|
|
27
|
+
|
|
28
|
+
region_path = session[:region].path
|
|
29
|
+
canvas_path = "#{region_path}.canvas"
|
|
30
|
+
viewport_path = "#{canvas_path}.viewport"
|
|
31
|
+
rows_path = session[:rows].path
|
|
32
|
+
|
|
33
|
+
assert_equal '1', session.app.tcl_eval("winfo exists #{canvas_path}")
|
|
34
|
+
assert_equal '1', session.app.tcl_eval("winfo exists #{viewport_path}")
|
|
35
|
+
assert_equal viewport_path, rows_path.sub(/\.\w+\z/, ''), "the column should live inside the viewport, not directly under the scrollable"
|
|
36
|
+
assert_equal '1', session.app.tcl_eval("winfo exists #{region_path}.vsb")
|
|
37
|
+
|
|
38
|
+
scrollregion = session.app.tcl_eval("#{canvas_path} cget -scrollregion")
|
|
39
|
+
refute_empty scrollregion, "the scrollregion should track the viewport's content size"
|
|
40
|
+
height = scrollregion.split.last.to_i
|
|
41
|
+
assert_operator height, :>, 200, "30 stacked buttons should produce a tall scrollregion"
|
|
42
|
+
|
|
43
|
+
session.app.destroy
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_y_defaults_to_true_and_x_to_false
|
|
48
|
+
assert_tk_app("a scrollable should default to a vertical scrollbar only") do
|
|
49
|
+
require 'teek/ui'
|
|
50
|
+
|
|
51
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
52
|
+
ui.scrollable(:region) { |s| s.column { |c| c.button(text: 'Row') } }
|
|
53
|
+
end
|
|
54
|
+
session.run_async
|
|
55
|
+
session.app.update
|
|
56
|
+
|
|
57
|
+
region_path = session[:region].path
|
|
58
|
+
assert_equal '1', session.app.tcl_eval("winfo exists #{region_path}.vsb")
|
|
59
|
+
assert_equal '0', session.app.tcl_eval("winfo exists #{region_path}.hsb")
|
|
60
|
+
|
|
61
|
+
session.app.destroy
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_y_false_omits_the_vertical_scrollbar
|
|
66
|
+
assert_tk_app("y: false should leave no scrollbar at all if x: isn't given either") do
|
|
67
|
+
require 'teek/ui'
|
|
68
|
+
|
|
69
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
70
|
+
ui.scrollable(:region, y: false) { |s| s.column { |c| c.button(text: 'Row') } }
|
|
71
|
+
end
|
|
72
|
+
session.run_async
|
|
73
|
+
session.app.update
|
|
74
|
+
|
|
75
|
+
region_path = session[:region].path
|
|
76
|
+
assert_equal '0', session.app.tcl_eval("winfo exists #{region_path}.vsb")
|
|
77
|
+
assert_equal '0', session.app.tcl_eval("winfo exists #{region_path}.hsb")
|
|
78
|
+
|
|
79
|
+
session.app.destroy
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_x_true_also_wires_a_horizontal_scrollbar
|
|
84
|
+
assert_tk_app("x: true should additionally wire a horizontal scrollbar") do
|
|
85
|
+
require 'teek/ui'
|
|
86
|
+
|
|
87
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
88
|
+
ui.scrollable(:region, x: true) { |s| s.column { |c| c.button(text: 'Row') } }
|
|
89
|
+
end
|
|
90
|
+
session.run_async
|
|
91
|
+
session.app.update
|
|
92
|
+
|
|
93
|
+
region_path = session[:region].path
|
|
94
|
+
assert_equal '1', session.app.tcl_eval("winfo exists #{region_path}.hsb")
|
|
95
|
+
|
|
96
|
+
session.app.destroy
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_syncs_the_viewport_width_to_the_canvas_by_default
|
|
101
|
+
assert_tk_app("without x: scrolling, resizing the canvas should resize the embedded viewport to match") do
|
|
102
|
+
require 'teek/ui'
|
|
103
|
+
|
|
104
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
105
|
+
ui.window(:win, geometry: '400x300') do |w|
|
|
106
|
+
w.scrollable(:region) { |s| s.column(:rows) { |c| c.button(text: 'Row') } }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
session.run_async
|
|
110
|
+
session[:win].show
|
|
111
|
+
session.app.update
|
|
112
|
+
session.app.update_idletasks
|
|
113
|
+
|
|
114
|
+
canvas_path = "#{session[:region].path}.canvas"
|
|
115
|
+
viewport_path = "#{canvas_path}.viewport"
|
|
116
|
+
|
|
117
|
+
canvas_width = session.app.tcl_eval("winfo width #{canvas_path}")
|
|
118
|
+
viewport_width = session.app.tcl_eval("winfo width #{viewport_path}")
|
|
119
|
+
assert_equal canvas_width, viewport_width
|
|
120
|
+
|
|
121
|
+
session.app.destroy
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_with_x_true_does_not_force_the_viewport_width_to_match_the_canvas
|
|
126
|
+
assert_tk_app("x: true should leave the viewport free to be wider than the canvas, for horizontal scrolling") do
|
|
127
|
+
require 'teek/ui'
|
|
128
|
+
|
|
129
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
130
|
+
ui.scrollable(:region, x: true) { |s| s.column(:rows) { |c| c.button(text: 'Row') } }
|
|
131
|
+
end
|
|
132
|
+
session.run_async
|
|
133
|
+
session.app.update
|
|
134
|
+
|
|
135
|
+
canvas_path = "#{session[:region].path}.canvas"
|
|
136
|
+
assert_equal '', session.app.tcl_eval("bind #{canvas_path} <Configure>"),
|
|
137
|
+
"no width-sync binding should be wired when x: true"
|
|
138
|
+
|
|
139
|
+
session.app.destroy
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_participates_in_normal_layout_like_any_other_container
|
|
144
|
+
assert_tk_app("a scrollable nested in a column with grow: true should realize and map without error") do
|
|
145
|
+
require 'teek/ui'
|
|
146
|
+
|
|
147
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
148
|
+
ui.column do |c|
|
|
149
|
+
c.label(text: 'Header')
|
|
150
|
+
c.scrollable(:region, grow: true) { |s| s.checkbox(text: 'A checkbox') }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
session.run_async
|
|
154
|
+
session.app.update
|
|
155
|
+
|
|
156
|
+
assert session.app.winfo.ismapped?(session[:region].path)
|
|
157
|
+
|
|
158
|
+
session.app.destroy
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_no_widget_in_the_subtree_has_conflicting_geometry_managers
|
|
163
|
+
assert_tk_app("grid (region) and pack (viewport content) must never collide on the same master") do
|
|
164
|
+
require 'teek/ui'
|
|
165
|
+
|
|
166
|
+
session = Teek::UI.app(title: 'Scrollable Test') do |ui|
|
|
167
|
+
ui.scrollable(:region_a) { |s| s.column { |c| c.button(text: 'A') } }
|
|
168
|
+
ui.scrollable(:region_b, x: true) { |s| s.row { |r| r.button(text: 'B') } }
|
|
169
|
+
end
|
|
170
|
+
session.run_async
|
|
171
|
+
session.app.update
|
|
172
|
+
|
|
173
|
+
check = lambda do |path|
|
|
174
|
+
children = session.app.split_list(session.app.tcl_eval("winfo children #{path}"))
|
|
175
|
+
managers = children.map { |child| session.app.tcl_eval("winfo manager #{child}") }.reject(&:empty?).uniq
|
|
176
|
+
assert_operator managers.length, :<=, 1,
|
|
177
|
+
"#{path} has children managed by more than one geometry manager: #{managers.inspect}"
|
|
178
|
+
children.each { |child| check.call(child) }
|
|
179
|
+
end
|
|
180
|
+
check.call('.')
|
|
181
|
+
|
|
182
|
+
session.app.destroy
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# create_scrollable wires the scrollbar and scrollregion sync (see
|
|
7
|
+
# test_scrollable.rb), but the frame case's canvas + embedded viewport has
|
|
8
|
+
# no wheel handling of its own - a bare canvas doesn't respond to
|
|
9
|
+
# <MouseWheel>, and neither do arbitrary widgets nested inside it. These
|
|
10
|
+
# confirm the fix: a shared bindtag (see Realizer#wire_wheel_scroll)
|
|
11
|
+
# applied to the canvas, the viewport, and everything already inside it,
|
|
12
|
+
# so wheeling over any of them scrolls the same region - and that a
|
|
13
|
+
# widget with no such tag is left alone.
|
|
14
|
+
#
|
|
15
|
+
# yview/xview reading is inlined as a local lambda in every block (not a
|
|
16
|
+
# helper method) - assert_tk_app re-evaluates each block's own source text
|
|
17
|
+
# in a separate worker process, which has no access to methods defined on
|
|
18
|
+
# this class.
|
|
19
|
+
class TestScrollableWheel < Minitest::Test
|
|
20
|
+
include TeekTestHelper
|
|
21
|
+
|
|
22
|
+
def test_wheel_over_the_canvas_scrolls_the_frame_case
|
|
23
|
+
assert_tk_app("MouseWheel over the canvas itself should scroll it") do
|
|
24
|
+
require 'teek/ui'
|
|
25
|
+
|
|
26
|
+
session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
|
|
27
|
+
ui.scrollable(:region) { |s| s.column { |c| 30.times { |i| c.button(text: "Row #{i}") } } }
|
|
28
|
+
end
|
|
29
|
+
session.run_async
|
|
30
|
+
session.app.update
|
|
31
|
+
session.app.update_idletasks
|
|
32
|
+
|
|
33
|
+
canvas_path = "#{session[:region].path}.canvas"
|
|
34
|
+
yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
|
|
35
|
+
assert_equal 0.0, yview_first.call
|
|
36
|
+
|
|
37
|
+
session.app.tcl_eval("event generate #{canvas_path} <MouseWheel> -delta -120")
|
|
38
|
+
session.app.update
|
|
39
|
+
|
|
40
|
+
assert_operator yview_first.call, :>, 0.0
|
|
41
|
+
|
|
42
|
+
session.app.destroy
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_wheel_over_a_nested_widget_also_scrolls_the_region
|
|
47
|
+
assert_tk_app("MouseWheel over a widget nested deep inside the viewport should still scroll the canvas") do
|
|
48
|
+
require 'teek/ui'
|
|
49
|
+
|
|
50
|
+
session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
|
|
51
|
+
ui.scrollable(:region) do |s|
|
|
52
|
+
s.column { |c| 30.times { |i| c.button("row_#{i}".to_sym, text: "Row #{i}") } }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
session.run_async
|
|
56
|
+
session.app.update
|
|
57
|
+
session.app.update_idletasks
|
|
58
|
+
|
|
59
|
+
canvas_path = "#{session[:region].path}.canvas"
|
|
60
|
+
nested_button_path = session[:row_5].path
|
|
61
|
+
yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
|
|
62
|
+
|
|
63
|
+
session.app.tcl_eval("event generate #{nested_button_path} <MouseWheel> -delta -120")
|
|
64
|
+
session.app.update
|
|
65
|
+
|
|
66
|
+
assert_operator yview_first.call, :>, 0.0
|
|
67
|
+
|
|
68
|
+
session.app.destroy
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_wheel_over_an_unrelated_widget_does_not_scroll_the_region
|
|
73
|
+
assert_tk_app("MouseWheel over a widget with no bindtag from the scrollable region should leave it alone") do
|
|
74
|
+
require 'teek/ui'
|
|
75
|
+
|
|
76
|
+
session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
|
|
77
|
+
ui.scrollable(:region) { |s| s.column { |c| 30.times { |i| c.button(text: "Row #{i}") } } }
|
|
78
|
+
ui.button(:elsewhere, text: 'Elsewhere')
|
|
79
|
+
end
|
|
80
|
+
session.run_async
|
|
81
|
+
session.app.update
|
|
82
|
+
session.app.update_idletasks
|
|
83
|
+
|
|
84
|
+
canvas_path = "#{session[:region].path}.canvas"
|
|
85
|
+
yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
|
|
86
|
+
|
|
87
|
+
session.app.tcl_eval("event generate #{session[:elsewhere].path} <MouseWheel> -delta -120")
|
|
88
|
+
session.app.update
|
|
89
|
+
|
|
90
|
+
assert_equal 0.0, yview_first.call
|
|
91
|
+
|
|
92
|
+
session.app.destroy
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_button_4_and_5_also_scroll_the_frame_case
|
|
97
|
+
assert_tk_app("the X11 Button-4/Button-5 wheel fallback should scroll the region too") do
|
|
98
|
+
require 'teek/ui'
|
|
99
|
+
|
|
100
|
+
session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
|
|
101
|
+
ui.scrollable(:region) { |s| s.column { |c| 30.times { |i| c.button(text: "Row #{i}") } } }
|
|
102
|
+
end
|
|
103
|
+
session.run_async
|
|
104
|
+
session.app.update
|
|
105
|
+
session.app.update_idletasks
|
|
106
|
+
|
|
107
|
+
canvas_path = "#{session[:region].path}.canvas"
|
|
108
|
+
yview_first = -> { session.app.tcl_eval("#{canvas_path} yview").split.first.to_f }
|
|
109
|
+
|
|
110
|
+
session.app.tcl_eval("event generate #{canvas_path} <Button-5>")
|
|
111
|
+
session.app.update
|
|
112
|
+
after_down = yview_first.call
|
|
113
|
+
assert_operator after_down, :>, 0.0
|
|
114
|
+
|
|
115
|
+
session.app.tcl_eval("event generate #{canvas_path} <Button-4>")
|
|
116
|
+
session.app.update
|
|
117
|
+
assert_operator yview_first.call, :<, after_down
|
|
118
|
+
|
|
119
|
+
session.app.destroy
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_shift_mouse_wheel_scrolls_horizontally_when_x_is_enabled
|
|
124
|
+
assert_tk_app("Shift-MouseWheel should drive xview when x: true") do
|
|
125
|
+
require 'teek/ui'
|
|
126
|
+
|
|
127
|
+
session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
|
|
128
|
+
ui.scrollable(:region, x: true) { |s| s.row { |r| 30.times { |i| r.button(text: "Col #{i}") } } }
|
|
129
|
+
end
|
|
130
|
+
session.run_async
|
|
131
|
+
session.app.update
|
|
132
|
+
session.app.update_idletasks
|
|
133
|
+
|
|
134
|
+
canvas_path = "#{session[:region].path}.canvas"
|
|
135
|
+
xview_first = -> { session.app.tcl_eval("#{canvas_path} xview").split.first.to_f }
|
|
136
|
+
assert_equal 0.0, xview_first.call
|
|
137
|
+
|
|
138
|
+
session.app.tcl_eval("event generate #{canvas_path} <Shift-MouseWheel> -delta -120")
|
|
139
|
+
session.app.update
|
|
140
|
+
|
|
141
|
+
assert_operator xview_first.call, :>, 0.0
|
|
142
|
+
|
|
143
|
+
session.app.destroy
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def test_native_scrollable_widget_wheel_scrolls_through_the_dsl
|
|
148
|
+
assert_tk_app("MouseWheel over a native scrollable widget (Tk's own class binding) should work through the DSL") do
|
|
149
|
+
require 'teek/ui'
|
|
150
|
+
|
|
151
|
+
session = Teek::UI.app(title: 'Scrollable Wheel Test') do |ui|
|
|
152
|
+
ui.scrollable(:region) { |s| s.list(:items, height: 5) }
|
|
153
|
+
end
|
|
154
|
+
session.run_async
|
|
155
|
+
session.app.update
|
|
156
|
+
|
|
157
|
+
list_path = session[:items].path
|
|
158
|
+
session.app.command(list_path, :insert, :end, *(1..60).map { |i| "Item #{i}" })
|
|
159
|
+
session.app.update_idletasks
|
|
160
|
+
|
|
161
|
+
yview_first = -> { session.app.tcl_eval("#{list_path} yview").split.first.to_f }
|
|
162
|
+
assert_equal 0.0, yview_first.call
|
|
163
|
+
|
|
164
|
+
session.app.tcl_eval("event generate #{list_path} <MouseWheel> -delta -120")
|
|
165
|
+
session.app.update
|
|
166
|
+
|
|
167
|
+
assert_operator yview_first.call, :>, 0.0
|
|
168
|
+
|
|
169
|
+
session.app.destroy
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# wire_scrollbars wires -yscrollcommand/-xscrollcommand through
|
|
7
|
+
# Realizer#auto_hide_scrollbar instead of a plain literal string, so a
|
|
8
|
+
# scrollbar is grid-removed (not just left dangling and useless) whenever
|
|
9
|
+
# its content fully fits, and re-shown the moment it doesn't - real
|
|
10
|
+
# "overflow: auto" rather than a bar that's always there whether it's
|
|
11
|
+
# needed or not. Shared by both the native-widget auto-attach path and
|
|
12
|
+
# ui.scrollable's own frame case, so a couple of each is enough to prove
|
|
13
|
+
# the one underlying mechanism.
|
|
14
|
+
#
|
|
15
|
+
# The initial hide/show state settles via an #after_idle check (see
|
|
16
|
+
# Realizer#auto_hide_scrollbar's own comment - Tk only re-invokes
|
|
17
|
+
# -yscrollcommand on an actual fraction *change*, so an empty widget
|
|
18
|
+
# gaining a few rows never fires it on its own). A single
|
|
19
|
+
# +update_idletasks+ isn't reliably enough to flush that under Xvfb - this
|
|
20
|
+
# project has hit that class of timing gap before (see
|
|
21
|
+
# TeekTestHelper#wait_until's other callers) - so the mapped/unmapped
|
|
22
|
+
# assertions below poll for it instead of asserting immediately after one
|
|
23
|
+
# idle flush.
|
|
24
|
+
class TestScrollbarAutoHide < Minitest::Test
|
|
25
|
+
include TeekTestHelper
|
|
26
|
+
|
|
27
|
+
def test_scrollbar_is_hidden_when_content_fits
|
|
28
|
+
assert_tk_app("a scrollbar should be grid-removed when its content doesn't overflow the visible area") do
|
|
29
|
+
require 'teek/ui'
|
|
30
|
+
|
|
31
|
+
session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 10) }
|
|
32
|
+
session.run_async
|
|
33
|
+
session.app.command(session[:items].path, :insert, :end, 'one', 'two', 'three')
|
|
34
|
+
|
|
35
|
+
wrapper = session[:items].path.sub(/\.widget\z/, '')
|
|
36
|
+
assert_equal '1', session.app.tcl_eval("winfo exists #{wrapper}.vsb"), "the scrollbar widget itself still exists"
|
|
37
|
+
assert wait_until { !session.app.winfo.ismapped?("#{wrapper}.vsb") },
|
|
38
|
+
"should be grid-removed once idle processing settles, since everything fits"
|
|
39
|
+
|
|
40
|
+
session.app.destroy
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_scrollbar_is_shown_when_content_overflows
|
|
45
|
+
assert_tk_app("a scrollbar should be mapped once its content overflows the visible area") do
|
|
46
|
+
require 'teek/ui'
|
|
47
|
+
|
|
48
|
+
session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 5) }
|
|
49
|
+
session.run_async
|
|
50
|
+
session.app.command(session[:items].path, :insert, :end, *(1..50).map { |i| "Item #{i}" })
|
|
51
|
+
|
|
52
|
+
wrapper = session[:items].path.sub(/\.widget\z/, '')
|
|
53
|
+
assert wait_until { session.app.winfo.ismapped?("#{wrapper}.vsb") }
|
|
54
|
+
|
|
55
|
+
session.app.destroy
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_scrollbar_appears_once_content_grows_past_fitting
|
|
60
|
+
assert_tk_app("a scrollbar hidden at realize should reappear once enough content is added later") do
|
|
61
|
+
require 'teek/ui'
|
|
62
|
+
|
|
63
|
+
session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 5) }
|
|
64
|
+
session.run_async
|
|
65
|
+
session.app.command(session[:items].path, :insert, :end, 'one', 'two')
|
|
66
|
+
|
|
67
|
+
wrapper = session[:items].path.sub(/\.widget\z/, '')
|
|
68
|
+
assert wait_until { !session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should start hidden with only 2 items"
|
|
69
|
+
|
|
70
|
+
session.app.command(session[:items].path, :insert, :end, *(1..50).map { |i| "More #{i}" })
|
|
71
|
+
|
|
72
|
+
assert wait_until { session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should reappear once content overflows"
|
|
73
|
+
|
|
74
|
+
session.app.destroy
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_scrollbar_disappears_again_once_content_shrinks_back_to_fitting
|
|
79
|
+
assert_tk_app("a visible scrollbar should hide again once enough content is removed") do
|
|
80
|
+
require 'teek/ui'
|
|
81
|
+
|
|
82
|
+
session = Teek::UI.app(title: 'Auto-Hide Test') { |ui| ui.list(:items, height: 5) }
|
|
83
|
+
session.run_async
|
|
84
|
+
session.app.command(session[:items].path, :insert, :end, *(1..50).map { |i| "Item #{i}" })
|
|
85
|
+
|
|
86
|
+
wrapper = session[:items].path.sub(/\.widget\z/, '')
|
|
87
|
+
assert wait_until { session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should start shown with 50 items"
|
|
88
|
+
|
|
89
|
+
session.app.command(session[:items].path, :delete, 2, :end)
|
|
90
|
+
|
|
91
|
+
assert wait_until { !session.app.winfo.ismapped?("#{wrapper}.vsb") }, "should hide again once only 2 items remain"
|
|
92
|
+
|
|
93
|
+
session.app.destroy
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_the_frame_case_also_auto_hides_its_scrollbar
|
|
98
|
+
assert_tk_app("ui.scrollable's own canvas-driven scrollbar should auto-hide the same way") do
|
|
99
|
+
require 'teek/ui'
|
|
100
|
+
|
|
101
|
+
session = Teek::UI.app(title: 'Auto-Hide Test') do |ui|
|
|
102
|
+
ui.scrollable(:region) { |s| s.column(:rows) { |c| c.button(text: 'One lone button') } }
|
|
103
|
+
end
|
|
104
|
+
session.run_async
|
|
105
|
+
|
|
106
|
+
assert wait_until { !session.app.winfo.ismapped?("#{session[:region].path}.vsb") },
|
|
107
|
+
"one small button shouldn't overflow the canvas's own default size"
|
|
108
|
+
|
|
109
|
+
session.app.destroy
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_the_frame_case_shows_its_scrollbar_once_content_overflows
|
|
114
|
+
assert_tk_app("ui.scrollable's canvas-driven scrollbar should appear once its content overflows") do
|
|
115
|
+
require 'teek/ui'
|
|
116
|
+
|
|
117
|
+
session = Teek::UI.app(title: 'Auto-Hide Test') do |ui|
|
|
118
|
+
ui.scrollable(:region) { |s| s.column(:rows) { |c| 40.times { |i| c.button(text: "Row #{i}") } } }
|
|
119
|
+
end
|
|
120
|
+
session.run_async
|
|
121
|
+
|
|
122
|
+
assert wait_until { session.app.winfo.ismapped?("#{session[:region].path}.vsb") }
|
|
123
|
+
|
|
124
|
+
session.app.destroy
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/test/test_split.rb
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# ui.split/s.pane realize as a working ttk::panedwindow - each pane a real
|
|
7
|
+
# Tk frame added as a managed pane (PaneRealize.post_create), never
|
|
8
|
+
# pack/grid managed on its own (:pane's own arranged: false).
|
|
9
|
+
class TestSplit < Minitest::Test
|
|
10
|
+
include TeekTestHelper
|
|
11
|
+
|
|
12
|
+
def test_split_realizes_as_a_working_panedwindow_with_panes
|
|
13
|
+
assert_tk_app("ui.split should realize as a ttk::panedwindow with each pane added as a managed region") do
|
|
14
|
+
require 'teek/ui'
|
|
15
|
+
|
|
16
|
+
session = Teek::UI.app(title: 'Split Test') do |ui|
|
|
17
|
+
ui.split(:s) do |s|
|
|
18
|
+
s.pane { |a| a.button(:go, text: 'Go') }
|
|
19
|
+
s.pane { |b| b.label(:info, text: 'Info') }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
session.run_async
|
|
23
|
+
session.app.update
|
|
24
|
+
|
|
25
|
+
split_path = session[:s].path
|
|
26
|
+
pane_paths = session.app.split_list(session.app.command(split_path, :panes))
|
|
27
|
+
assert_equal 2, pane_paths.length
|
|
28
|
+
|
|
29
|
+
session.app.destroy
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_pane_content_is_addressable_and_configurable_like_any_other_widget
|
|
34
|
+
assert_tk_app("a widget declared inside a pane should be a normal, addressable, configurable widget") do
|
|
35
|
+
require 'teek/ui'
|
|
36
|
+
|
|
37
|
+
session = Teek::UI.app(title: 'Split Test') do |ui|
|
|
38
|
+
ui.split { |s| s.pane { |a| a.button(:go, text: 'Go') } }
|
|
39
|
+
end
|
|
40
|
+
session.run_async
|
|
41
|
+
session.app.update
|
|
42
|
+
|
|
43
|
+
session[:go].configure(text: 'Changed')
|
|
44
|
+
session.app.update
|
|
45
|
+
|
|
46
|
+
assert_equal 'Changed', session.app.command(session[:go].path, :cget, '-text')
|
|
47
|
+
|
|
48
|
+
session.app.destroy
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_pane_frame_is_not_independently_pack_or_grid_managed
|
|
53
|
+
assert_tk_app("a pane's own frame should be placed only by panedwindow add, not pack/grid") do
|
|
54
|
+
require 'teek/ui'
|
|
55
|
+
|
|
56
|
+
session = Teek::UI.app(title: 'Split Test') do |ui|
|
|
57
|
+
ui.split(:s) { |s| s.pane(:left) { |a| a.button(text: 'Go') } }
|
|
58
|
+
end
|
|
59
|
+
session.run_async
|
|
60
|
+
session.app.update
|
|
61
|
+
|
|
62
|
+
assert_equal 'panedwindow', session.app.tcl_eval("winfo manager #{session[:left].path}"),
|
|
63
|
+
"the panedwindow itself should be the only geometry manager - proves no pack/grid call also ran"
|
|
64
|
+
|
|
65
|
+
session.app.destroy
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_split_defaults_to_horizontal_orientation
|
|
70
|
+
assert_tk_app("ui.split with no orientation given should realize with -orient horizontal") do
|
|
71
|
+
require 'teek/ui'
|
|
72
|
+
|
|
73
|
+
session = Teek::UI.app(title: 'Split Test') do |ui|
|
|
74
|
+
ui.split(:s) { |s| s.pane { } }
|
|
75
|
+
end
|
|
76
|
+
session.run_async
|
|
77
|
+
session.app.update
|
|
78
|
+
|
|
79
|
+
assert_equal 'horizontal', session.app.command(session[:s].path, :cget, '-orient')
|
|
80
|
+
|
|
81
|
+
session.app.destroy
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_split_vertical_orientation_sets_the_real_tk_orient_option
|
|
86
|
+
assert_tk_app("ui.split(orientation: :vertical) should realize with -orient vertical") do
|
|
87
|
+
require 'teek/ui'
|
|
88
|
+
|
|
89
|
+
session = Teek::UI.app(title: 'Split Test') do |ui|
|
|
90
|
+
ui.split(:s, orientation: :vertical) { |s| s.pane { } }
|
|
91
|
+
end
|
|
92
|
+
session.run_async
|
|
93
|
+
session.app.update
|
|
94
|
+
|
|
95
|
+
assert_equal 'vertical', session.app.command(session[:s].path, :cget, '-orient')
|
|
96
|
+
|
|
97
|
+
session.app.destroy
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_pane_weight_is_passed_through_to_panedwindow_add
|
|
102
|
+
assert_tk_app("s.pane(weight:) should set the real -weight panedwindow uses to divide leftover space") do
|
|
103
|
+
require 'teek/ui'
|
|
104
|
+
|
|
105
|
+
session = Teek::UI.app(title: 'Split Test') do |ui|
|
|
106
|
+
ui.split(:s) do |s|
|
|
107
|
+
s.pane(:left, weight: 1) { }
|
|
108
|
+
s.pane(:right, weight: 3) { }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
session.run_async
|
|
112
|
+
session.app.update
|
|
113
|
+
|
|
114
|
+
split_path = session[:s].path
|
|
115
|
+
assert_equal '1', session.app.command(split_path, :pane, session[:left].path, '-weight')
|
|
116
|
+
assert_equal '3', session.app.command(split_path, :pane, session[:right].path, '-weight')
|
|
117
|
+
|
|
118
|
+
session.app.destroy
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_a_new_pane_can_be_added_at_runtime_via_incremental_realize
|
|
123
|
+
assert_tk_app("session.add should be able to add a whole new pane to an already-realized ui.split") do
|
|
124
|
+
require 'teek/ui'
|
|
125
|
+
|
|
126
|
+
session = Teek::UI.app(title: 'Split Test') do |ui|
|
|
127
|
+
ui.split(:s) { |s| s.pane { |a| a.button(text: 'Go') } }
|
|
128
|
+
end
|
|
129
|
+
session.run_async
|
|
130
|
+
session.app.update
|
|
131
|
+
|
|
132
|
+
split_path = session[:s].path
|
|
133
|
+
assert_equal 1, session.app.split_list(session.app.command(split_path, :panes)).length
|
|
134
|
+
|
|
135
|
+
session.add(:s) { |a| a.pane(:new_pane) { |n| n.button(:new_button, text: 'New') } }
|
|
136
|
+
session.app.update
|
|
137
|
+
|
|
138
|
+
pane_paths = session.app.split_list(session.app.command(split_path, :panes))
|
|
139
|
+
assert_equal 2, pane_paths.length
|
|
140
|
+
assert session.app.winfo.ismapped?(session[:new_button].path)
|
|
141
|
+
|
|
142
|
+
session.app.destroy
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|