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,551 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
require 'teek/ui/canvas_item'
|
|
6
|
+
|
|
7
|
+
# ui.canvas gains a real item-drawing vocabulary (Handle#line/oval/polygon/
|
|
8
|
+
# rectangle/text/arc/bitmap, each returning a CanvasItem) instead of being a
|
|
9
|
+
# bare container only reachable via ui.raw + raw app.command(:create, ...).
|
|
10
|
+
# CanvasItem addresses Tk's own tagOrId uniformly, so a shared `tags:` group
|
|
11
|
+
# is movable/configurable/deletable exactly like a single item - see
|
|
12
|
+
# test_tagged_addresses_every_item_sharing_a_tag_as_one_group below.
|
|
13
|
+
class TestCanvasItems < Minitest::Test
|
|
14
|
+
include TeekTestHelper
|
|
15
|
+
|
|
16
|
+
def test_virtual_path_marks_past_the_real_tk_path
|
|
17
|
+
item = Teek::UI::CanvasItem.new(:fake_app, '.board', 'I3')
|
|
18
|
+
|
|
19
|
+
assert_equal '.board!I3', item.virtual_path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_line_creates_a_real_item_addressable_by_the_returned_handle
|
|
23
|
+
assert_tk_app("ui.canvas#line should create a real line item") do
|
|
24
|
+
require 'teek/ui'
|
|
25
|
+
|
|
26
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
27
|
+
session.run_async
|
|
28
|
+
session.app.update
|
|
29
|
+
|
|
30
|
+
item = session[:board].line(10, 10, 50, 50, fill: 'red')
|
|
31
|
+
|
|
32
|
+
assert_equal 'line', session.app.tcl_eval("#{session[:board].path} type #{item.tag_or_id}")
|
|
33
|
+
assert_equal [10.0, 10.0, 50.0, 50.0], item.coords
|
|
34
|
+
|
|
35
|
+
session.app.destroy
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_oval_rectangle_polygon_text_arc_bitmap_all_create_the_right_item_type
|
|
40
|
+
assert_tk_app("every shape method should create the matching Tk item type") do
|
|
41
|
+
require 'teek/ui'
|
|
42
|
+
|
|
43
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
44
|
+
session.run_async
|
|
45
|
+
session.app.update
|
|
46
|
+
|
|
47
|
+
board = session[:board]
|
|
48
|
+
items = {
|
|
49
|
+
'oval' => board.oval(10, 10, 40, 40),
|
|
50
|
+
'rectangle' => board.rectangle(10, 10, 40, 40),
|
|
51
|
+
'polygon' => board.polygon(10, 10, 40, 10, 25, 40),
|
|
52
|
+
'text' => board.text(10, 10, text: 'Hi'),
|
|
53
|
+
'arc' => board.arc(10, 10, 40, 40, start: 0, extent: 90),
|
|
54
|
+
'bitmap' => board.bitmap(10, 10, bitmap: 'gray25'),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
items.each do |expected_type, item|
|
|
58
|
+
assert_equal expected_type, session.app.tcl_eval("#{board.path} type #{item.tag_or_id}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
session.app.destroy
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_coords_accepts_flat_or_nested_arguments_equivalently
|
|
66
|
+
assert_tk_app("flat and nested coordinate arguments should produce the same item") do
|
|
67
|
+
require 'teek/ui'
|
|
68
|
+
|
|
69
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
70
|
+
session.run_async
|
|
71
|
+
session.app.update
|
|
72
|
+
|
|
73
|
+
flat = session[:board].line(10, 10, 50, 50)
|
|
74
|
+
nested = session[:board].line([10, 10], [50, 50])
|
|
75
|
+
|
|
76
|
+
assert_equal flat.coords, nested.coords
|
|
77
|
+
|
|
78
|
+
session.app.destroy
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_move_shifts_relative_to_the_current_position
|
|
83
|
+
assert_tk_app("move should shift by a relative delta") do
|
|
84
|
+
require 'teek/ui'
|
|
85
|
+
|
|
86
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
87
|
+
session.run_async
|
|
88
|
+
session.app.update
|
|
89
|
+
|
|
90
|
+
item = session[:board].oval(10, 10, 30, 30)
|
|
91
|
+
item.move(5, -3)
|
|
92
|
+
|
|
93
|
+
assert_equal [15.0, 7.0, 35.0, 27.0], item.coords
|
|
94
|
+
|
|
95
|
+
session.app.destroy
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_coords_writer_replaces_the_coordinate_list_outright
|
|
100
|
+
assert_tk_app("coords= should replace the coordinate list, not shift it") do
|
|
101
|
+
require 'teek/ui'
|
|
102
|
+
|
|
103
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
104
|
+
session.run_async
|
|
105
|
+
session.app.update
|
|
106
|
+
|
|
107
|
+
item = session[:board].line(0, 0, 10, 10)
|
|
108
|
+
item.coords = [1, 2, 3, 4, 5, 6]
|
|
109
|
+
|
|
110
|
+
assert_equal [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], item.coords
|
|
111
|
+
|
|
112
|
+
session.app.destroy
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_configure_mutates_multiple_options_at_once
|
|
117
|
+
assert_tk_app("configure should mutate item options") do
|
|
118
|
+
require 'teek/ui'
|
|
119
|
+
|
|
120
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
121
|
+
session.run_async
|
|
122
|
+
session.app.update
|
|
123
|
+
|
|
124
|
+
item = session[:board].rectangle(10, 10, 40, 40, fill: 'blue')
|
|
125
|
+
item.configure(fill: 'green', width: 3)
|
|
126
|
+
|
|
127
|
+
assert_equal 'green', item[:fill]
|
|
128
|
+
assert_equal '3.0', item[:width]
|
|
129
|
+
|
|
130
|
+
session.app.destroy
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_bracket_read_and_write_a_single_option
|
|
135
|
+
assert_tk_app("item[:opt] should read, item[:opt] = value should write") do
|
|
136
|
+
require 'teek/ui'
|
|
137
|
+
|
|
138
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
139
|
+
session.run_async
|
|
140
|
+
session.app.update
|
|
141
|
+
|
|
142
|
+
item = session[:board].oval(10, 10, 40, 40, fill: 'red')
|
|
143
|
+
assert_equal 'red', item[:fill]
|
|
144
|
+
|
|
145
|
+
item[:fill] = 'purple'
|
|
146
|
+
assert_equal 'purple', item[:fill]
|
|
147
|
+
|
|
148
|
+
session.app.destroy
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_delete_removes_the_item
|
|
153
|
+
assert_tk_app("delete should remove the item from the canvas") do
|
|
154
|
+
require 'teek/ui'
|
|
155
|
+
|
|
156
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
157
|
+
session.run_async
|
|
158
|
+
session.app.update
|
|
159
|
+
|
|
160
|
+
item = session[:board].oval(10, 10, 40, 40)
|
|
161
|
+
assert item.exists?
|
|
162
|
+
|
|
163
|
+
item.delete
|
|
164
|
+
|
|
165
|
+
refute item.exists?
|
|
166
|
+
assert_nil item.bounds
|
|
167
|
+
|
|
168
|
+
session.app.destroy
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_bring_to_front_and_send_to_back_change_stacking_order
|
|
173
|
+
assert_tk_app("bring_to_front/send_to_back with no target should move all the way") do
|
|
174
|
+
require 'teek/ui'
|
|
175
|
+
|
|
176
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
177
|
+
session.run_async
|
|
178
|
+
session.app.update
|
|
179
|
+
|
|
180
|
+
board = session[:board]
|
|
181
|
+
a = board.oval(10, 10, 20, 20)
|
|
182
|
+
b = board.oval(10, 10, 20, 20)
|
|
183
|
+
c = board.oval(10, 10, 20, 20)
|
|
184
|
+
stacking = -> { session.app.split_list(session.app.tcl_eval("#{board.path} find all")) }
|
|
185
|
+
|
|
186
|
+
assert_equal [a.tag_or_id, b.tag_or_id, c.tag_or_id], stacking.call
|
|
187
|
+
|
|
188
|
+
a.bring_to_front
|
|
189
|
+
assert_equal [b.tag_or_id, c.tag_or_id, a.tag_or_id], stacking.call
|
|
190
|
+
|
|
191
|
+
c.send_to_back
|
|
192
|
+
assert_equal [c.tag_or_id, b.tag_or_id, a.tag_or_id], stacking.call
|
|
193
|
+
|
|
194
|
+
session.app.destroy
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_bring_to_front_with_a_target_stacks_just_above_it
|
|
199
|
+
assert_tk_app("bring_to_front(target) should reposition just above target, not to the very top") do
|
|
200
|
+
require 'teek/ui'
|
|
201
|
+
|
|
202
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
203
|
+
session.run_async
|
|
204
|
+
session.app.update
|
|
205
|
+
|
|
206
|
+
board = session[:board]
|
|
207
|
+
a = board.oval(10, 10, 20, 20)
|
|
208
|
+
b = board.oval(10, 10, 20, 20)
|
|
209
|
+
c = board.oval(10, 10, 20, 20)
|
|
210
|
+
stacking = -> { session.app.split_list(session.app.tcl_eval("#{board.path} find all")) }
|
|
211
|
+
|
|
212
|
+
a.bring_to_front(b)
|
|
213
|
+
assert_equal [b.tag_or_id, a.tag_or_id, c.tag_or_id], stacking.call
|
|
214
|
+
|
|
215
|
+
session.app.destroy
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def test_scale_resizes_coordinates_relative_to_an_origin
|
|
220
|
+
assert_tk_app("scale should resize relative to the given origin") do
|
|
221
|
+
require 'teek/ui'
|
|
222
|
+
|
|
223
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
224
|
+
session.run_async
|
|
225
|
+
session.app.update
|
|
226
|
+
|
|
227
|
+
item = session[:board].rectangle(10, 10, 20, 20)
|
|
228
|
+
item.scale(10, 10, 2, 2)
|
|
229
|
+
|
|
230
|
+
assert_equal [10.0, 10.0, 30.0, 30.0], item.coords
|
|
231
|
+
|
|
232
|
+
session.app.destroy
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_bounds_returns_the_bounding_box
|
|
237
|
+
assert_tk_app("bounds should return the item's bounding box") do
|
|
238
|
+
require 'teek/ui'
|
|
239
|
+
|
|
240
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
241
|
+
session.run_async
|
|
242
|
+
session.app.update
|
|
243
|
+
|
|
244
|
+
item = session[:board].rectangle(10, 10, 40, 40, outline: '', fill: 'red')
|
|
245
|
+
box = item.bounds
|
|
246
|
+
|
|
247
|
+
refute_nil box
|
|
248
|
+
assert_equal 4, box.length
|
|
249
|
+
|
|
250
|
+
session.app.destroy
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def test_tagged_addresses_every_item_sharing_a_tag_as_one_group
|
|
255
|
+
assert_tk_app("tagged should address a shared tag as a single movable/configurable/deletable group") do
|
|
256
|
+
require 'teek/ui'
|
|
257
|
+
|
|
258
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
259
|
+
session.run_async
|
|
260
|
+
session.app.update
|
|
261
|
+
|
|
262
|
+
board = session[:board]
|
|
263
|
+
one = board.oval(10, 10, 20, 20, tags: 'group_a')
|
|
264
|
+
two = board.oval(30, 30, 40, 40, tags: 'group_a')
|
|
265
|
+
board.oval(50, 50, 60, 60, tags: 'group_b')
|
|
266
|
+
|
|
267
|
+
group = board.tagged('group_a')
|
|
268
|
+
assert group.exists?
|
|
269
|
+
|
|
270
|
+
group.move(5, 5)
|
|
271
|
+
assert_equal [15.0, 15.0, 25.0, 25.0], one.coords
|
|
272
|
+
assert_equal [35.0, 35.0, 45.0, 45.0], two.coords
|
|
273
|
+
|
|
274
|
+
group.configure(fill: 'orange')
|
|
275
|
+
assert_equal 'orange', one[:fill]
|
|
276
|
+
assert_equal 'orange', two[:fill]
|
|
277
|
+
|
|
278
|
+
group.delete
|
|
279
|
+
refute one.exists?
|
|
280
|
+
refute two.exists?
|
|
281
|
+
assert board.tagged('group_b').exists?
|
|
282
|
+
|
|
283
|
+
session.app.destroy
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_tagged_on_a_tag_matching_nothing_reports_it_does_not_exist
|
|
288
|
+
assert_tk_app("tagged on an unused tag should report exists? false, not raise") do
|
|
289
|
+
require 'teek/ui'
|
|
290
|
+
|
|
291
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
292
|
+
session.run_async
|
|
293
|
+
session.app.update
|
|
294
|
+
|
|
295
|
+
refute session[:board].tagged('nothing_has_this_tag').exists?
|
|
296
|
+
|
|
297
|
+
session.app.destroy
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Item-level canvas bindings only fire through Tk's "current item"
|
|
302
|
+
# tracking, which real event generate positioning depends on real X11
|
|
303
|
+
# pointer state - unreliable under Xvfb (see teek core's own
|
|
304
|
+
# test_canvas_bindings.rb). Tk has no "invoke this item binding"
|
|
305
|
+
# command either way, so these read back the bound script and eval it
|
|
306
|
+
# directly - the same pattern teek core already uses to verify item
|
|
307
|
+
# binding dispatch without needing a real synthetic click to land.
|
|
308
|
+
|
|
309
|
+
def test_on_click_fires_only_for_the_specific_item_clicked
|
|
310
|
+
assert_tk_app("on_click's bound script should be scoped per-item, not shared") do
|
|
311
|
+
require 'teek/ui'
|
|
312
|
+
|
|
313
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
314
|
+
session.run_async
|
|
315
|
+
session.app.update
|
|
316
|
+
|
|
317
|
+
board = session[:board]
|
|
318
|
+
a = board.oval(10, 10, 30, 30)
|
|
319
|
+
b = board.oval(100, 100, 120, 120)
|
|
320
|
+
|
|
321
|
+
a_hits = 0
|
|
322
|
+
b_hits = 0
|
|
323
|
+
a.on_click { a_hits += 1 }
|
|
324
|
+
b.on_click { b_hits += 1 }
|
|
325
|
+
|
|
326
|
+
a_script = session.app.tcl_eval("#{board.path} bind #{a.tag_or_id} <Button-1>")
|
|
327
|
+
session.app.tcl_eval(a_script)
|
|
328
|
+
assert_equal 1, a_hits, "invoking A's own binding should fire A's handler"
|
|
329
|
+
assert_equal 0, b_hits, "invoking A's own binding should not fire B's handler"
|
|
330
|
+
|
|
331
|
+
b_script = session.app.tcl_eval("#{board.path} bind #{b.tag_or_id} <Button-1>")
|
|
332
|
+
session.app.tcl_eval(b_script)
|
|
333
|
+
assert_equal 1, a_hits, "invoking B's own binding should not fire A's handler again"
|
|
334
|
+
assert_equal 1, b_hits, "invoking B's own binding should fire B's handler"
|
|
335
|
+
|
|
336
|
+
session.app.destroy
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_on_right_click_fires_a_block_on_the_right_click_event
|
|
341
|
+
assert_tk_app("on_right_click should bind a script that fires the given block") do
|
|
342
|
+
require 'teek/ui'
|
|
343
|
+
|
|
344
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
345
|
+
session.run_async
|
|
346
|
+
session.app.update
|
|
347
|
+
|
|
348
|
+
board = session[:board]
|
|
349
|
+
item = board.oval(10, 10, 30, 30)
|
|
350
|
+
fired = false
|
|
351
|
+
item.on_right_click { fired = true }
|
|
352
|
+
|
|
353
|
+
script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <Button-3>")
|
|
354
|
+
session.app.tcl_eval(script)
|
|
355
|
+
|
|
356
|
+
assert fired, "on_right_click's bound script did not fire the given block"
|
|
357
|
+
|
|
358
|
+
session.app.destroy
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def test_on_right_click_with_a_menu_pops_up_at_the_clicks_root_coordinates
|
|
363
|
+
assert_tk_app("on_right_click(menu) should tk_popup the given menu at the click's root coordinates") do
|
|
364
|
+
require 'teek/ui'
|
|
365
|
+
|
|
366
|
+
session = Teek::UI.app(title: 'Canvas Items Test') do |ui|
|
|
367
|
+
ui.context_menu(:ctx) { |m| m.item(label: 'Delete') { } }
|
|
368
|
+
ui.canvas(:board, width: 200, height: 200)
|
|
369
|
+
end
|
|
370
|
+
session.run_async
|
|
371
|
+
session.app.update
|
|
372
|
+
|
|
373
|
+
board = session[:board]
|
|
374
|
+
item = board.oval(10, 10, 30, 30)
|
|
375
|
+
item.on_right_click(session[:ctx])
|
|
376
|
+
|
|
377
|
+
session.app.tcl_eval(<<~TCL)
|
|
378
|
+
proc tk_popup {args} {
|
|
379
|
+
set ::last_popup_call $args
|
|
380
|
+
}
|
|
381
|
+
TCL
|
|
382
|
+
|
|
383
|
+
# %X/%Y are literal %-substitution codes Tk fills in at real dispatch
|
|
384
|
+
# time - substitute them ourselves to simulate a click at (123, 456)
|
|
385
|
+
# root coordinates without needing a real synthetic click to land.
|
|
386
|
+
script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <Button-3>")
|
|
387
|
+
session.app.tcl_eval(script.sub('%X', '123').sub('%Y', '456'))
|
|
388
|
+
|
|
389
|
+
captured = session.app.split_list(session.app.tcl_eval('set ::last_popup_call'))
|
|
390
|
+
assert_equal [session[:ctx].path, '123', '456'], captured
|
|
391
|
+
|
|
392
|
+
session.app.destroy
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def test_on_right_click_with_neither_menu_nor_block_raises
|
|
397
|
+
assert_tk_app("on_right_click with neither a menu nor a block should raise") do
|
|
398
|
+
require 'teek/ui'
|
|
399
|
+
|
|
400
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
401
|
+
session.run_async
|
|
402
|
+
session.app.update
|
|
403
|
+
|
|
404
|
+
item = session[:board].oval(10, 10, 30, 30)
|
|
405
|
+
assert_raises(ArgumentError) { item.on_right_click }
|
|
406
|
+
|
|
407
|
+
session.app.destroy
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def test_on_right_click_with_both_menu_and_block_raises
|
|
412
|
+
assert_tk_app("on_right_click with both a menu and a block should raise") do
|
|
413
|
+
require 'teek/ui'
|
|
414
|
+
|
|
415
|
+
session = Teek::UI.app(title: 'Canvas Items Test') do |ui|
|
|
416
|
+
ui.context_menu(:ctx) { |m| m.item(label: 'Delete') { } }
|
|
417
|
+
ui.canvas(:board, width: 200, height: 200)
|
|
418
|
+
end
|
|
419
|
+
session.run_async
|
|
420
|
+
session.app.update
|
|
421
|
+
|
|
422
|
+
item = session[:board].oval(10, 10, 30, 30)
|
|
423
|
+
error = assert_raises(ArgumentError) { item.on_right_click(session[:ctx]) { } }
|
|
424
|
+
assert_match(/menu/i, error.message)
|
|
425
|
+
|
|
426
|
+
session.app.destroy
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def test_on_right_click_with_a_non_menu_handle_raises
|
|
431
|
+
assert_tk_app("on_right_click given a non-menu handle should raise a clear error") do
|
|
432
|
+
require 'teek/ui'
|
|
433
|
+
|
|
434
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
435
|
+
session.run_async
|
|
436
|
+
session.app.update
|
|
437
|
+
|
|
438
|
+
item = session[:board].oval(10, 10, 30, 30)
|
|
439
|
+
error = assert_raises(ArgumentError) { item.on_right_click(session[:board]) }
|
|
440
|
+
assert_match(/menu/i, error.message)
|
|
441
|
+
|
|
442
|
+
session.app.destroy
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def test_on_drag_delivers_canvas_relative_coordinates
|
|
447
|
+
assert_tk_app("on_drag should deliver canvasx/canvasy-converted coordinates, not raw window coordinates") do
|
|
448
|
+
require 'teek/ui'
|
|
449
|
+
|
|
450
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
451
|
+
session.run_async
|
|
452
|
+
session.app.update
|
|
453
|
+
|
|
454
|
+
board_path = session[:board].path
|
|
455
|
+
# scroll the canvas so canvasx/canvasy meaningfully differ from raw %x/%y -
|
|
456
|
+
# otherwise a bug that skips conversion entirely could still pass by luck.
|
|
457
|
+
session.app.tcl_eval("#{board_path} configure -scrollregion {0 0 1000 1000}")
|
|
458
|
+
session.app.tcl_eval("#{board_path} xview moveto 0.5")
|
|
459
|
+
session.app.update
|
|
460
|
+
|
|
461
|
+
item = session[:board].rectangle(0, 0, 1000, 1000)
|
|
462
|
+
received = nil
|
|
463
|
+
item.on_drag { |x, y| received = [x, y] }
|
|
464
|
+
|
|
465
|
+
# %x/%y are literal %-substitution codes Tk fills in at real dispatch
|
|
466
|
+
# time - substitute them ourselves to simulate a motion at raw window
|
|
467
|
+
# coordinates (50, 60) without needing a real synthetic drag to land.
|
|
468
|
+
script = session.app.tcl_eval("#{board_path} bind #{item.tag_or_id} <B1-Motion>")
|
|
469
|
+
session.app.tcl_eval(script.sub('%x', '50').sub('%y', '60'))
|
|
470
|
+
|
|
471
|
+
expected_x = session.app.command(board_path, :canvasx, 50).to_f.round
|
|
472
|
+
expected_y = session.app.command(board_path, :canvasy, 60).to_f.round
|
|
473
|
+
|
|
474
|
+
refute_nil received
|
|
475
|
+
assert_equal [expected_x, expected_y], received
|
|
476
|
+
refute_equal [50, 60], received, "the canvas is scrolled, so conversion should actually change the coordinates"
|
|
477
|
+
|
|
478
|
+
session.app.destroy
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def test_draggable_moves_the_item_by_the_drag_delta
|
|
483
|
+
assert_tk_app("draggable should move the item by the drag delta, with no coordinate math in app code") do
|
|
484
|
+
require 'teek/ui'
|
|
485
|
+
|
|
486
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
487
|
+
session.run_async
|
|
488
|
+
session.app.update
|
|
489
|
+
|
|
490
|
+
board = session[:board]
|
|
491
|
+
item = board.oval(10, 10, 30, 30)
|
|
492
|
+
item.draggable
|
|
493
|
+
|
|
494
|
+
# Same script-readback approach as the other item-event tests above -
|
|
495
|
+
# substitute %x/%y ourselves to simulate a press at (20, 20) then a
|
|
496
|
+
# drag to (30, 25), a (10, 5) delta.
|
|
497
|
+
press_script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <Button-1>")
|
|
498
|
+
session.app.tcl_eval(press_script.sub('%x', '20').sub('%y', '20'))
|
|
499
|
+
|
|
500
|
+
drag_script = session.app.tcl_eval("#{board.path} bind #{item.tag_or_id} <B1-Motion>")
|
|
501
|
+
session.app.tcl_eval(drag_script.sub('%x', '30').sub('%y', '25'))
|
|
502
|
+
|
|
503
|
+
assert_equal [20.0, 15.0, 40.0, 35.0], item.coords, "the item should have moved by the (10, 5) drag delta"
|
|
504
|
+
|
|
505
|
+
session.app.destroy
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def test_deleting_an_item_releases_its_click_and_drag_callbacks
|
|
510
|
+
assert_tk_app("deleting an item should release its on_click/on_drag callbacks, not leak them") do
|
|
511
|
+
require 'teek/ui'
|
|
512
|
+
|
|
513
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.canvas(:board, width: 200, height: 200) }
|
|
514
|
+
session.run_async
|
|
515
|
+
session.app.update
|
|
516
|
+
|
|
517
|
+
baseline = session.app.interp.callback_ids.length
|
|
518
|
+
|
|
519
|
+
item = session[:board].oval(10, 10, 30, 30)
|
|
520
|
+
item.on_click { }
|
|
521
|
+
item.on_drag { |_x, _y| }
|
|
522
|
+
assert_equal baseline + 2, session.app.interp.callback_ids.length,
|
|
523
|
+
"on_click and on_drag should each register one callback"
|
|
524
|
+
|
|
525
|
+
item.delete
|
|
526
|
+
|
|
527
|
+
assert_equal baseline, session.app.interp.callback_ids.length,
|
|
528
|
+
"deleting the item should release both its click and drag callbacks - on_drag's %-substitution " \
|
|
529
|
+
"args must not stop the leak-tracking regex from recognizing the binding"
|
|
530
|
+
|
|
531
|
+
session.app.destroy
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def test_shape_methods_raise_on_a_non_canvas_handle
|
|
536
|
+
assert_tk_app("shape creation and tagged should raise a clear error on a non-canvas handle") do
|
|
537
|
+
require 'teek/ui'
|
|
538
|
+
|
|
539
|
+
session = Teek::UI.app(title: 'Canvas Items Test') { |ui| ui.panel(:not_a_canvas) }
|
|
540
|
+
session.run_async
|
|
541
|
+
session.app.update
|
|
542
|
+
|
|
543
|
+
error = assert_raises(ArgumentError) { session[:not_a_canvas].line(0, 0, 10, 10) }
|
|
544
|
+
assert_match(/canvas/i, error.message)
|
|
545
|
+
|
|
546
|
+
assert_raises(ArgumentError) { session[:not_a_canvas].tagged('whatever') }
|
|
547
|
+
|
|
548
|
+
session.app.destroy
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# session.clipboard is a thin, realize-only delegation to Teek::Clipboard
|
|
7
|
+
# (see teek core's own test/test_clipboard.rb for the underlying set/get/
|
|
8
|
+
# clear behavior). What's new to verify at this layer: the delegation
|
|
9
|
+
# itself, and that DSL text widgets need no extra wiring at all for
|
|
10
|
+
# platform-standard copy/cut/paste - Tk's own class bindings already
|
|
11
|
+
# cover it.
|
|
12
|
+
class TestClipboard < Minitest::Test
|
|
13
|
+
include TeekTestHelper
|
|
14
|
+
|
|
15
|
+
def test_clipboard_raises_before_realize
|
|
16
|
+
assert_tk_app("session.clipboard should raise a clear error before realize, not queue") do
|
|
17
|
+
require 'teek/ui'
|
|
18
|
+
|
|
19
|
+
session = Teek::UI.app(title: 'Clipboard Test')
|
|
20
|
+
|
|
21
|
+
assert_raises(Teek::UI::NotRealizedError) { session.clipboard }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_clipboard_set_and_get_round_trips_through_the_session
|
|
26
|
+
assert_tk_app("session.clipboard.set/.get should round-trip, delegating to the underlying app") do
|
|
27
|
+
require 'teek/ui'
|
|
28
|
+
|
|
29
|
+
session = Teek::UI.app(title: 'Clipboard Test')
|
|
30
|
+
session.run_async
|
|
31
|
+
|
|
32
|
+
session.clipboard.set('hello from teek-ui')
|
|
33
|
+
|
|
34
|
+
assert_equal 'hello from teek-ui', session.clipboard.get
|
|
35
|
+
|
|
36
|
+
session.app.destroy
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_text_box_supports_platform_copy_with_no_manual_wiring
|
|
41
|
+
assert_tk_app("a DSL text_box should support Ctrl-C copy out of the box - no on_key wiring needed") do
|
|
42
|
+
require 'teek/ui'
|
|
43
|
+
|
|
44
|
+
session = Teek::UI.app(title: 'Clipboard Test') { |ui| ui.text_box(:field) }
|
|
45
|
+
session.run_async
|
|
46
|
+
session.app.update
|
|
47
|
+
|
|
48
|
+
path = session[:field].path
|
|
49
|
+
session.app.command(path, :insert, 0, 'hello world')
|
|
50
|
+
session.app.tcl_eval("#{path} selection range 0 5")
|
|
51
|
+
session.app.tcl_eval("focus -force #{path}")
|
|
52
|
+
session.app.update
|
|
53
|
+
|
|
54
|
+
session.clipboard.clear
|
|
55
|
+
session.app.tcl_eval("event generate #{path} <Control-c>")
|
|
56
|
+
session.app.update
|
|
57
|
+
|
|
58
|
+
assert_equal 'hello', session.clipboard.get
|
|
59
|
+
|
|
60
|
+
session.app.destroy
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_text_area_supports_platform_copy_with_no_manual_wiring
|
|
65
|
+
assert_tk_app("a DSL text_area should support Ctrl-C copy out of the box - no on_key wiring needed") do
|
|
66
|
+
require 'teek/ui'
|
|
67
|
+
|
|
68
|
+
session = Teek::UI.app(title: 'Clipboard Test') { |ui| ui.text_area(:notes) }
|
|
69
|
+
session.run_async
|
|
70
|
+
session.app.update
|
|
71
|
+
|
|
72
|
+
path = session[:notes].path
|
|
73
|
+
session.app.command(path, :insert, '1.0', 'hello world')
|
|
74
|
+
session.app.tcl_eval("#{path} tag add sel 1.0 1.5")
|
|
75
|
+
session.app.tcl_eval("focus -force #{path}")
|
|
76
|
+
session.app.update
|
|
77
|
+
|
|
78
|
+
session.clipboard.clear
|
|
79
|
+
session.app.tcl_eval("event generate #{path} <Control-c>")
|
|
80
|
+
session.app.update
|
|
81
|
+
|
|
82
|
+
assert_equal 'hello', session.clipboard.get
|
|
83
|
+
|
|
84
|
+
session.app.destroy
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|