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,664 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# Handle#text_content's rich text API (TextContent) - a companion
|
|
7
|
+
# object, not methods bolted onto Handle; Tk index syntax passed
|
|
8
|
+
# through verbatim; friendly names primary with Tk-named aliases;
|
|
9
|
+
# leak-safe format/tag event bindings; the read-only footgun
|
|
10
|
+
# transparently absorbed.
|
|
11
|
+
class TestTextContent < Minitest::Test
|
|
12
|
+
include TeekTestHelper
|
|
13
|
+
|
|
14
|
+
def test_text_content_raises_before_realize
|
|
15
|
+
assert_tk_app("text_content should raise before realize, matching every other realize-only accessor") do
|
|
16
|
+
require 'teek/ui'
|
|
17
|
+
|
|
18
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
19
|
+
|
|
20
|
+
assert_raises(Teek::UI::NotRealizedError) { session[:notes].text_content }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_text_content_raises_on_a_non_text_area_handle
|
|
25
|
+
assert_tk_app("text_content should raise a clear error on a widget that isn't a text_area") do
|
|
26
|
+
require 'teek/ui'
|
|
27
|
+
|
|
28
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.button(:go, text: 'Go') }
|
|
29
|
+
session.run_async
|
|
30
|
+
|
|
31
|
+
error = assert_raises(ArgumentError) { session[:go].text_content }
|
|
32
|
+
assert_match(/text_area/i, error.message)
|
|
33
|
+
|
|
34
|
+
session.app.destroy
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# -- Content -----------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def test_insert_and_get
|
|
41
|
+
assert_tk_app("insert should add text at an index, get should read it back") do
|
|
42
|
+
require 'teek/ui'
|
|
43
|
+
|
|
44
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
45
|
+
session.run_async
|
|
46
|
+
text = session[:notes].text_content
|
|
47
|
+
|
|
48
|
+
text.insert('1.0', 'hello world')
|
|
49
|
+
|
|
50
|
+
assert_equal 'hello', text.get('1.0', '1.5')
|
|
51
|
+
assert_equal 'hello world', text.value
|
|
52
|
+
|
|
53
|
+
session.app.destroy
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_delete
|
|
58
|
+
assert_tk_app("delete should remove a range") do
|
|
59
|
+
require 'teek/ui'
|
|
60
|
+
|
|
61
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
62
|
+
session.run_async
|
|
63
|
+
text = session[:notes].text_content
|
|
64
|
+
text.insert('1.0', 'hello world')
|
|
65
|
+
|
|
66
|
+
text.delete('1.0', '1.6')
|
|
67
|
+
|
|
68
|
+
assert_equal 'world', text.value
|
|
69
|
+
|
|
70
|
+
session.app.destroy
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_replace
|
|
75
|
+
assert_tk_app("replace should atomically swap a range for new text") do
|
|
76
|
+
require 'teek/ui'
|
|
77
|
+
|
|
78
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
79
|
+
session.run_async
|
|
80
|
+
text = session[:notes].text_content
|
|
81
|
+
text.insert('1.0', 'hello world')
|
|
82
|
+
|
|
83
|
+
text.replace('1.0', '1.5', 'goodbye')
|
|
84
|
+
|
|
85
|
+
assert_equal 'goodbye world', text.value
|
|
86
|
+
|
|
87
|
+
session.app.destroy
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_value_setter_replaces_the_whole_buffer
|
|
92
|
+
assert_tk_app("value= should replace the entire buffer's content") do
|
|
93
|
+
require 'teek/ui'
|
|
94
|
+
|
|
95
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
96
|
+
session.run_async
|
|
97
|
+
text = session[:notes].text_content
|
|
98
|
+
text.insert('1.0', 'old content')
|
|
99
|
+
|
|
100
|
+
text.value = 'new content'
|
|
101
|
+
|
|
102
|
+
assert_equal 'new content', text.value
|
|
103
|
+
|
|
104
|
+
session.app.destroy
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_clear_empties_the_buffer
|
|
109
|
+
assert_tk_app("clear should empty the whole buffer") do
|
|
110
|
+
require 'teek/ui'
|
|
111
|
+
|
|
112
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
113
|
+
session.run_async
|
|
114
|
+
text = session[:notes].text_content
|
|
115
|
+
text.insert('1.0', 'something')
|
|
116
|
+
|
|
117
|
+
text.clear
|
|
118
|
+
|
|
119
|
+
assert_equal '', text.value
|
|
120
|
+
|
|
121
|
+
session.app.destroy
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_end_and_cursor_symbol_aliases_resolve
|
|
126
|
+
assert_tk_app(":end and :cursor should resolve to Tk's own end/insert indices") do
|
|
127
|
+
require 'teek/ui'
|
|
128
|
+
|
|
129
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
130
|
+
session.run_async
|
|
131
|
+
text = session[:notes].text_content
|
|
132
|
+
|
|
133
|
+
text.insert(:end, 'first')
|
|
134
|
+
text.insert(:end, ' second')
|
|
135
|
+
|
|
136
|
+
assert_equal 'first second', text.value
|
|
137
|
+
assert_equal text.index(:end), text.index('end')
|
|
138
|
+
|
|
139
|
+
session.app.destroy
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# -- Read-only footgun ---------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def test_mutating_methods_transparently_lift_and_restore_read_only_state
|
|
146
|
+
assert_tk_app("insert/delete/replace/value=/clear should work on a read-only widget and leave it read-only after") do
|
|
147
|
+
require 'teek/ui'
|
|
148
|
+
|
|
149
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log, state: :disabled) }
|
|
150
|
+
session.run_async
|
|
151
|
+
text = session[:log].text_content
|
|
152
|
+
assert text.read_only, "should have started read-only, matching state: :disabled"
|
|
153
|
+
|
|
154
|
+
text.insert(:end, 'line one\n')
|
|
155
|
+
assert text.read_only, "should still be read-only after insert"
|
|
156
|
+
|
|
157
|
+
text.value = 'replaced'
|
|
158
|
+
assert text.read_only, "should still be read-only after value="
|
|
159
|
+
|
|
160
|
+
text.clear
|
|
161
|
+
assert text.read_only, "should still be read-only after clear"
|
|
162
|
+
assert_equal '', text.value
|
|
163
|
+
|
|
164
|
+
session.app.destroy
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_read_only_accessor_round_trips
|
|
169
|
+
assert_tk_app("read_only/read_only= should read and drive the widget's own -state") do
|
|
170
|
+
require 'teek/ui'
|
|
171
|
+
|
|
172
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
173
|
+
session.run_async
|
|
174
|
+
text = session[:notes].text_content
|
|
175
|
+
refute text.read_only, "should start editable by default"
|
|
176
|
+
|
|
177
|
+
text.read_only = true
|
|
178
|
+
assert text.read_only
|
|
179
|
+
|
|
180
|
+
text.read_only = false
|
|
181
|
+
refute text.read_only
|
|
182
|
+
|
|
183
|
+
session.app.destroy
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# -- Formats (Tk tags) ---------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def test_format_apply_format_and_format_ranges
|
|
190
|
+
assert_tk_app("format should define display properties, apply_format should apply them to a range") do
|
|
191
|
+
require 'teek/ui'
|
|
192
|
+
|
|
193
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
|
|
194
|
+
session.run_async
|
|
195
|
+
text = session[:code].text_content
|
|
196
|
+
text.insert(:end, 'errorline')
|
|
197
|
+
|
|
198
|
+
text.format(:error, foreground: 'red')
|
|
199
|
+
text.apply_format(:error, '1.0', '1.5')
|
|
200
|
+
|
|
201
|
+
assert_equal 'red', session.app.command(session[:code].path, :tag, :cget, :error, '-foreground')
|
|
202
|
+
assert_equal ['1.0', '1.5'], text.format_ranges(:error)
|
|
203
|
+
|
|
204
|
+
session.app.destroy
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_clear_format_removes_from_a_range_without_deleting_the_definition
|
|
209
|
+
assert_tk_app("clear_format should remove the format from a range but keep the definition applyable elsewhere") do
|
|
210
|
+
require 'teek/ui'
|
|
211
|
+
|
|
212
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
|
|
213
|
+
session.run_async
|
|
214
|
+
text = session[:code].text_content
|
|
215
|
+
text.insert(:end, 'abcdef')
|
|
216
|
+
text.format(:hl, foreground: 'red')
|
|
217
|
+
text.apply_format(:hl, '1.0', '1.3')
|
|
218
|
+
|
|
219
|
+
text.clear_format(:hl, '1.0', '1.3')
|
|
220
|
+
|
|
221
|
+
assert_equal [], text.format_ranges(:hl)
|
|
222
|
+
|
|
223
|
+
text.apply_format(:hl, '1.3', '1.6')
|
|
224
|
+
assert_equal ['1.3', '1.6'], text.format_ranges(:hl)
|
|
225
|
+
|
|
226
|
+
session.app.destroy
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_delete_format_removes_the_definition_entirely
|
|
231
|
+
assert_tk_app("delete_format should remove the format definition and every range it was applied to") do
|
|
232
|
+
require 'teek/ui'
|
|
233
|
+
|
|
234
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
|
|
235
|
+
session.run_async
|
|
236
|
+
text = session[:code].text_content
|
|
237
|
+
text.insert(:end, 'abcdef')
|
|
238
|
+
text.format(:hl, foreground: 'red')
|
|
239
|
+
text.apply_format(:hl, '1.0', '1.3')
|
|
240
|
+
|
|
241
|
+
text.delete_format(:hl)
|
|
242
|
+
|
|
243
|
+
names = session.app.split_list(session.app.command(session[:code].path, :tag, :names))
|
|
244
|
+
refute_includes names, 'hl'
|
|
245
|
+
|
|
246
|
+
session.app.destroy
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_tk_named_format_aliases_work_identically
|
|
251
|
+
assert_tk_app("tag_configure/tag_add/tag_remove/tag_delete/tag_ranges should alias the friendly names") do
|
|
252
|
+
require 'teek/ui'
|
|
253
|
+
|
|
254
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
|
|
255
|
+
session.run_async
|
|
256
|
+
text = session[:code].text_content
|
|
257
|
+
text.insert(:end, 'abcdef')
|
|
258
|
+
|
|
259
|
+
text.tag_configure(:hl, foreground: 'blue')
|
|
260
|
+
text.tag_add(:hl, '1.0', '1.3')
|
|
261
|
+
assert_equal ['1.0', '1.3'], text.tag_ranges(:hl)
|
|
262
|
+
|
|
263
|
+
text.tag_remove(:hl, '1.0', '1.3')
|
|
264
|
+
assert_equal [], text.tag_ranges(:hl)
|
|
265
|
+
|
|
266
|
+
text.tag_add(:hl, '1.0', '1.3')
|
|
267
|
+
text.tag_delete(:hl)
|
|
268
|
+
names = session.app.split_list(session.app.command(session[:code].path, :tag, :names))
|
|
269
|
+
refute_includes names, 'hl'
|
|
270
|
+
|
|
271
|
+
session.app.destroy
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# -- Leak-safe format/tag event bindings ---------------------------------
|
|
276
|
+
|
|
277
|
+
def test_on_format_click_fires
|
|
278
|
+
assert_tk_app("on_format_click should fire when text carrying that format is clicked") do
|
|
279
|
+
require 'teek/ui'
|
|
280
|
+
|
|
281
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log) }
|
|
282
|
+
session.run_async
|
|
283
|
+
session.app.update
|
|
284
|
+
text = session[:log].text_content
|
|
285
|
+
text.insert(:end, 'click me')
|
|
286
|
+
text.format(:link, foreground: 'blue')
|
|
287
|
+
text.apply_format(:link, '1.0', '1.8')
|
|
288
|
+
session.app.update
|
|
289
|
+
|
|
290
|
+
clicked = false
|
|
291
|
+
text.on_format_click(:link) { clicked = true }
|
|
292
|
+
|
|
293
|
+
# tag bind hit-tests by pixel position, unlike a widget-level bind -
|
|
294
|
+
# query the real bbox of a character inside the tagged range rather
|
|
295
|
+
# than guess a pixel offset that may land in padding/margin instead.
|
|
296
|
+
bbox = session.app.split_list(session.app.command(session[:log].path, :bbox, '1.2')).map(&:to_i)
|
|
297
|
+
x, y = bbox[0] + 2, bbox[1] + 2
|
|
298
|
+
session.app.tcl_eval("focus -force #{session[:log].path}")
|
|
299
|
+
session.app.update
|
|
300
|
+
# Tk's "current tag under the pointer" is motion-tracked, same as
|
|
301
|
+
# canvas's own "current item" - a bare synthetic Button-1 with no
|
|
302
|
+
# prior Motion to that position never updates it, so the click
|
|
303
|
+
# dispatches as if nothing were under the pointer.
|
|
304
|
+
session.app.tcl_eval("event generate #{session[:log].path} <Motion> -x #{x} -y #{y}")
|
|
305
|
+
session.app.update
|
|
306
|
+
session.app.tcl_eval("event generate #{session[:log].path} <Button-1> -x #{x} -y #{y}")
|
|
307
|
+
|
|
308
|
+
assert wait_until { clicked }, "on_format_click did not fire"
|
|
309
|
+
|
|
310
|
+
session.app.destroy
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def test_on_format_click_is_leak_safe_via_the_registered_callback_count
|
|
315
|
+
assert_tk_app("removing a formatted range's callback should release it, not leak, proving this routes through app.command (not tcl_eval)") do
|
|
316
|
+
require 'teek/ui'
|
|
317
|
+
|
|
318
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log) }
|
|
319
|
+
session.run_async
|
|
320
|
+
text = session[:log].text_content
|
|
321
|
+
text.insert(:end, 'click me')
|
|
322
|
+
text.format(:link, foreground: 'blue')
|
|
323
|
+
text.apply_format(:link, '1.0', '1.8')
|
|
324
|
+
|
|
325
|
+
baseline = session.debug_info[:tag_binds] || 0
|
|
326
|
+
text.on_format_click(:link) { }
|
|
327
|
+
assert_equal baseline + 1, session.debug_info[:tag_binds]
|
|
328
|
+
|
|
329
|
+
# Rebinding the SAME tag+event replaces (not stacks) the callback -
|
|
330
|
+
# Tk's own tag bind semantics - so the count should stay at +1,
|
|
331
|
+
# never climb, across repeated rebinds.
|
|
332
|
+
3.times { text.on_format_click(:link) { } }
|
|
333
|
+
assert_equal baseline + 1, session.debug_info[:tag_binds],
|
|
334
|
+
"rebinding the same format+event should replace, not accumulate"
|
|
335
|
+
|
|
336
|
+
text.delete_format(:link)
|
|
337
|
+
session.app.update
|
|
338
|
+
|
|
339
|
+
assert_equal baseline, session.debug_info[:tag_binds] || 0,
|
|
340
|
+
"deleting the format should release its callback via the leak-safe tag_bind reconcile"
|
|
341
|
+
|
|
342
|
+
session.app.destroy
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def test_on_format_with_a_custom_event_pattern
|
|
347
|
+
assert_tk_app("on_format should accept an arbitrary Tk event pattern, auto-wrapped in angle brackets") do
|
|
348
|
+
require 'teek/ui'
|
|
349
|
+
|
|
350
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log) }
|
|
351
|
+
session.run_async
|
|
352
|
+
session.app.update
|
|
353
|
+
text = session[:log].text_content
|
|
354
|
+
text.insert(:end, 'ctrl click me')
|
|
355
|
+
text.format(:special, foreground: 'green')
|
|
356
|
+
text.apply_format(:special, '1.0', '1.13')
|
|
357
|
+
session.app.update
|
|
358
|
+
|
|
359
|
+
fired = false
|
|
360
|
+
# Double-/Triple-/Quadruple- click patterns can't be synthetically
|
|
361
|
+
# generated via `event generate` (Tk only synthesizes those from
|
|
362
|
+
# real, timed events) - a modifier+button combo like this one can be.
|
|
363
|
+
text.on_format('special', 'Control-Button-1') { fired = true }
|
|
364
|
+
|
|
365
|
+
bbox = session.app.split_list(session.app.command(session[:log].path, :bbox, '1.2')).map(&:to_i)
|
|
366
|
+
x, y = bbox[0] + 2, bbox[1] + 2
|
|
367
|
+
session.app.tcl_eval("focus -force #{session[:log].path}")
|
|
368
|
+
session.app.update
|
|
369
|
+
session.app.tcl_eval("event generate #{session[:log].path} <Motion> -x #{x} -y #{y}")
|
|
370
|
+
session.app.update
|
|
371
|
+
session.app.tcl_eval("event generate #{session[:log].path} <Control-Button-1> -x #{x} -y #{y}")
|
|
372
|
+
|
|
373
|
+
assert wait_until { fired }, "on_format with a custom event pattern did not fire"
|
|
374
|
+
|
|
375
|
+
session.app.destroy
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# -- Markers -------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
def test_add_marker_remove_marker_and_markers
|
|
382
|
+
assert_tk_app("add_marker/remove_marker/markers should manage named floating positions") do
|
|
383
|
+
require 'teek/ui'
|
|
384
|
+
|
|
385
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
386
|
+
session.run_async
|
|
387
|
+
text = session[:notes].text_content
|
|
388
|
+
text.insert(:end, 'hello world')
|
|
389
|
+
|
|
390
|
+
text.add_marker(:checkpoint, at: '1.6')
|
|
391
|
+
|
|
392
|
+
assert_includes text.markers, 'checkpoint'
|
|
393
|
+
assert_equal '1.6', text.index(:checkpoint)
|
|
394
|
+
|
|
395
|
+
text.remove_marker(:checkpoint)
|
|
396
|
+
refute_includes text.markers, 'checkpoint'
|
|
397
|
+
|
|
398
|
+
session.app.destroy
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def test_mark_gravity_reads_and_writes
|
|
403
|
+
assert_tk_app("mark_gravity should read the default and accept an explicit direction") do
|
|
404
|
+
require 'teek/ui'
|
|
405
|
+
|
|
406
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
407
|
+
session.run_async
|
|
408
|
+
text = session[:notes].text_content
|
|
409
|
+
text.insert(:end, 'hello world')
|
|
410
|
+
text.add_marker(:checkpoint, at: '1.3')
|
|
411
|
+
|
|
412
|
+
assert_equal 'right', text.mark_gravity(:checkpoint)
|
|
413
|
+
|
|
414
|
+
text.mark_gravity(:checkpoint, :left)
|
|
415
|
+
assert_equal 'left', text.mark_gravity(:checkpoint)
|
|
416
|
+
|
|
417
|
+
session.app.destroy
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# -- Search ----------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
def test_search_finds_a_match
|
|
424
|
+
assert_tk_app("search should return the matching index, or nil if not found") do
|
|
425
|
+
require 'teek/ui'
|
|
426
|
+
|
|
427
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
428
|
+
session.run_async
|
|
429
|
+
text = session[:notes].text_content
|
|
430
|
+
text.insert(:end, 'the quick brown fox')
|
|
431
|
+
|
|
432
|
+
assert_equal '1.4', text.search('quick', from: '1.0')
|
|
433
|
+
assert_nil text.search('nonexistent', from: '1.0')
|
|
434
|
+
|
|
435
|
+
session.app.destroy
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def test_search_backwards_and_regexp_and_nocase
|
|
440
|
+
assert_tk_app("search's backwards/regexp/nocase switches should all forward correctly") do
|
|
441
|
+
require 'teek/ui'
|
|
442
|
+
|
|
443
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
444
|
+
session.run_async
|
|
445
|
+
text = session[:notes].text_content
|
|
446
|
+
text.insert(:end, 'FOO bar foo baz')
|
|
447
|
+
|
|
448
|
+
assert_equal '1.8', text.search('foo', from: :end, to: '1.0', backwards: true)
|
|
449
|
+
assert_equal '1.0', text.search('F[A-Z]{2}', from: '1.0', regexp: true)
|
|
450
|
+
assert_equal '1.0', text.search('foo', from: '1.0', nocase: true)
|
|
451
|
+
|
|
452
|
+
session.app.destroy
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# -- View / cursor / state --------------------------------------------------
|
|
457
|
+
|
|
458
|
+
def test_scroll_to_and_see_alias
|
|
459
|
+
assert_tk_app("scroll_to (and its see alias) should not raise against a real index") do
|
|
460
|
+
require 'teek/ui'
|
|
461
|
+
|
|
462
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes, height: 3) }
|
|
463
|
+
session.run_async
|
|
464
|
+
text = session[:notes].text_content
|
|
465
|
+
20.times { |i| text.insert(:end, "line #{i}\n") }
|
|
466
|
+
|
|
467
|
+
text.scroll_to('10.0')
|
|
468
|
+
text.see('1.0')
|
|
469
|
+
|
|
470
|
+
session.app.destroy
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def test_index_resolves_to_canonical_form
|
|
475
|
+
assert_tk_app("index should resolve any index expression to canonical line.char form") do
|
|
476
|
+
require 'teek/ui'
|
|
477
|
+
|
|
478
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
479
|
+
session.run_async
|
|
480
|
+
text = session[:notes].text_content
|
|
481
|
+
text.insert(:end, "line one\nline two\n")
|
|
482
|
+
|
|
483
|
+
assert_equal '2.0', text.index('2.0')
|
|
484
|
+
assert_equal '1.4', text.index('1.0 +4 chars')
|
|
485
|
+
|
|
486
|
+
session.app.destroy
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def test_cursor_getter_and_setter
|
|
491
|
+
assert_tk_app("cursor should read and move the text insertion point") do
|
|
492
|
+
require 'teek/ui'
|
|
493
|
+
|
|
494
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
495
|
+
session.run_async
|
|
496
|
+
text = session[:notes].text_content
|
|
497
|
+
text.insert(:end, 'hello world')
|
|
498
|
+
|
|
499
|
+
text.cursor = '1.5'
|
|
500
|
+
|
|
501
|
+
assert_equal '1.5', text.cursor
|
|
502
|
+
assert_equal text.index(:cursor), text.cursor
|
|
503
|
+
|
|
504
|
+
session.app.destroy
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# -- Embedded images ---------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
def test_insert_image_embeds_a_ui_image
|
|
511
|
+
assert_tk_app("insert_image should embed a ui.image inline in the text flow") do
|
|
512
|
+
require 'teek/ui'
|
|
513
|
+
require 'tmpdir'
|
|
514
|
+
|
|
515
|
+
Dir.mktmpdir do |dir|
|
|
516
|
+
path = File.join(dir, 'test.png')
|
|
517
|
+
seed = Teek::Photo.new(app, width: 4, height: 4)
|
|
518
|
+
seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
|
|
519
|
+
app.tcl_eval("#{seed.name} write {#{path}} -format png")
|
|
520
|
+
seed.delete
|
|
521
|
+
|
|
522
|
+
icon = nil
|
|
523
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui|
|
|
524
|
+
icon = ui.image(path)
|
|
525
|
+
ui.text_area(:notes)
|
|
526
|
+
}
|
|
527
|
+
session.run_async
|
|
528
|
+
text = session[:notes].text_content
|
|
529
|
+
text.insert(:end, 'before ')
|
|
530
|
+
|
|
531
|
+
text.insert_image(:end, image: icon)
|
|
532
|
+
text.insert(:end, ' after')
|
|
533
|
+
|
|
534
|
+
dump = session.app.command(session[:notes].path, :dump, '-image', '1.0', 'end')
|
|
535
|
+
assert_match(/#{Regexp.escape(icon.name)}/, dump)
|
|
536
|
+
|
|
537
|
+
session.app.destroy
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def test_insert_image_respects_read_only
|
|
543
|
+
assert_tk_app("insert_image should transparently lift and restore read-only state, same as the other mutators") do
|
|
544
|
+
require 'teek/ui'
|
|
545
|
+
require 'tmpdir'
|
|
546
|
+
|
|
547
|
+
Dir.mktmpdir do |dir|
|
|
548
|
+
path = File.join(dir, 'test.png')
|
|
549
|
+
seed = Teek::Photo.new(app, width: 4, height: 4)
|
|
550
|
+
seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
|
|
551
|
+
app.tcl_eval("#{seed.name} write {#{path}} -format png")
|
|
552
|
+
seed.delete
|
|
553
|
+
|
|
554
|
+
icon = nil
|
|
555
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui|
|
|
556
|
+
icon = ui.image(path)
|
|
557
|
+
ui.text_area(:notes, state: :disabled)
|
|
558
|
+
}
|
|
559
|
+
session.run_async
|
|
560
|
+
text = session[:notes].text_content
|
|
561
|
+
|
|
562
|
+
text.insert_image(:end, image: icon)
|
|
563
|
+
|
|
564
|
+
assert text.read_only, "should still be read-only after insert_image"
|
|
565
|
+
|
|
566
|
+
session.app.destroy
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# -- Driving use cases ---------------------------------------------------
|
|
572
|
+
|
|
573
|
+
def test_use_case_syntax_highlighted_log_view
|
|
574
|
+
assert_tk_app("driving use case: a syntax-highlighted code/log view via format + apply_format") do
|
|
575
|
+
require 'teek/ui'
|
|
576
|
+
|
|
577
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:code) }
|
|
578
|
+
session.run_async
|
|
579
|
+
text = session[:code].text_content
|
|
580
|
+
|
|
581
|
+
text.insert(:end, "def hello\n puts 'hi'\nend\n")
|
|
582
|
+
text.format(:keyword, foreground: 'purple', font: ['Courier', 10, :bold])
|
|
583
|
+
text.apply_format(:keyword, '1.0', '1.3')
|
|
584
|
+
text.apply_format(:keyword, '3.0', '3.3')
|
|
585
|
+
|
|
586
|
+
assert_equal ['1.0', '1.3', '3.0', '3.3'], text.format_ranges(:keyword)
|
|
587
|
+
|
|
588
|
+
session.app.destroy
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def test_use_case_search_and_replace
|
|
593
|
+
assert_tk_app("driving use case: search-and-replace via search + replace") do
|
|
594
|
+
require 'teek/ui'
|
|
595
|
+
|
|
596
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:notes) }
|
|
597
|
+
session.run_async
|
|
598
|
+
text = session[:notes].text_content
|
|
599
|
+
text.insert(:end, 'the cat sat on the mat')
|
|
600
|
+
|
|
601
|
+
loop do
|
|
602
|
+
found = text.search('the', from: '1.0')
|
|
603
|
+
break unless found
|
|
604
|
+
|
|
605
|
+
text.replace(found, "#{found} +3 chars", 'THE')
|
|
606
|
+
# advance search past the just-replaced word next iteration
|
|
607
|
+
break if text.search('the', from: "#{found} +3 chars").nil?
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
assert_equal 'THE cat sat on THE mat', text.value
|
|
611
|
+
|
|
612
|
+
session.app.destroy
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def test_use_case_appending_read_only_log_pane_with_auto_scroll
|
|
617
|
+
assert_tk_app("driving use case: an appending, auto-scrolling, read-only log pane") do
|
|
618
|
+
require 'teek/ui'
|
|
619
|
+
|
|
620
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui| ui.text_area(:log, height: 3, state: :disabled) }
|
|
621
|
+
session.run_async
|
|
622
|
+
text = session[:log].text_content
|
|
623
|
+
|
|
624
|
+
5.times { |i| text.insert(:end, "log line #{i}\n") }
|
|
625
|
+
text.scroll_to(:end)
|
|
626
|
+
|
|
627
|
+
assert text.read_only, "the log pane should still be read-only after appending"
|
|
628
|
+
assert_match(/log line 4/, text.value)
|
|
629
|
+
|
|
630
|
+
session.app.destroy
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def test_use_case_inline_images
|
|
635
|
+
assert_tk_app("driving use case: inline images via insert_image") do
|
|
636
|
+
require 'teek/ui'
|
|
637
|
+
require 'tmpdir'
|
|
638
|
+
|
|
639
|
+
Dir.mktmpdir do |dir|
|
|
640
|
+
path = File.join(dir, 'test.png')
|
|
641
|
+
seed = Teek::Photo.new(app, width: 4, height: 4)
|
|
642
|
+
seed.put_block(([0, 0, 0, 255].pack('CCCC')) * 16, 4, 4)
|
|
643
|
+
app.tcl_eval("#{seed.name} write {#{path}} -format png")
|
|
644
|
+
seed.delete
|
|
645
|
+
|
|
646
|
+
icon = nil
|
|
647
|
+
session = Teek::UI.app(title: 'Text Content Test') { |ui|
|
|
648
|
+
icon = ui.image(path)
|
|
649
|
+
ui.text_area(:notes)
|
|
650
|
+
}
|
|
651
|
+
session.run_async
|
|
652
|
+
text = session[:notes].text_content
|
|
653
|
+
|
|
654
|
+
text.insert(:end, 'Logo: ')
|
|
655
|
+
text.insert_image(:end, image: icon)
|
|
656
|
+
|
|
657
|
+
dump = session.app.command(session[:notes].path, :dump, '-image', '1.0', 'end')
|
|
658
|
+
assert_match(/#{Regexp.escape(icon.name)}/, dump)
|
|
659
|
+
|
|
660
|
+
session.app.destroy
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
end
|