teek 0.1.5 → 0.2.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 +4 -4
- data/README.md +36 -7
- data/Rakefile +76 -0
- data/ext/teek/tcltkbridge.c +69 -4
- data/ext/teek/tkdrop_x11.c +2 -2
- data/lib/teek/callback_registry.rb +80 -0
- data/lib/teek/canvas_bind_interceptor.rb +68 -0
- data/lib/teek/command_interceptors.rb +47 -0
- data/lib/teek/debugger.rb +7 -10
- data/lib/teek/demo_support.rb +2 -2
- data/lib/teek/dialogs.rb +137 -0
- data/lib/teek/menu_interceptor.rb +61 -0
- data/lib/teek/photo.rb +55 -0
- data/lib/teek/tag_bind_interceptor.rb +66 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +25 -2
- data/lib/teek/winfo.rb +104 -0
- data/lib/teek/wm.rb +82 -0
- data/lib/teek.rb +317 -57
- data/teek.gemspec +1 -1
- metadata +10 -2
data/lib/teek.rb
CHANGED
|
@@ -5,7 +5,15 @@ require_relative 'teek/version'
|
|
|
5
5
|
require_relative 'teek/platform'
|
|
6
6
|
require_relative 'teek/ractor_support'
|
|
7
7
|
require_relative 'teek/widget'
|
|
8
|
+
require_relative 'teek/callback_registry'
|
|
9
|
+
require_relative 'teek/command_interceptors'
|
|
10
|
+
require_relative 'teek/menu_interceptor'
|
|
11
|
+
require_relative 'teek/tag_bind_interceptor'
|
|
12
|
+
require_relative 'teek/canvas_bind_interceptor'
|
|
8
13
|
require_relative 'teek/photo'
|
|
14
|
+
require_relative 'teek/dialogs'
|
|
15
|
+
require_relative 'teek/winfo'
|
|
16
|
+
require_relative 'teek/wm'
|
|
9
17
|
|
|
10
18
|
# Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
|
|
11
19
|
# with Ruby callbacks, event bindings, and background work support.
|
|
@@ -30,6 +38,40 @@ require_relative 'teek/photo'
|
|
|
30
38
|
# @see Teek::BackgroundWork
|
|
31
39
|
module Teek
|
|
32
40
|
|
|
41
|
+
# Raised on a Tcl error (TCL_ERROR) from #tcl_eval, #tcl_invoke, or
|
|
42
|
+
# anything built on them. +message+ is the same short one-line Tcl
|
|
43
|
+
# result this exception has always carried; {#tcl_backtrace} and
|
|
44
|
+
# {#tcl_error_code} additionally expose Tcl's own +errorInfo+/+errorCode+
|
|
45
|
+
# for the failing call - captured immediately after the failing
|
|
46
|
+
# Tcl_Eval/Tcl_EvalObjv via Tcl_GetReturnOptions, before anything else
|
|
47
|
+
# can run and disturb them. Both are +nil+ for the handful of Ruby-level
|
|
48
|
+
# guard errors (e.g. "interpreter has been deleted") that never actually
|
|
49
|
+
# reached Tcl.
|
|
50
|
+
class TclError < RuntimeError
|
|
51
|
+
# @return [String, nil] Tcl's +errorInfo+ for the failing call - a
|
|
52
|
+
# multi-line trace with a "while executing"/"invoked from within"
|
|
53
|
+
# frame for each level of Tcl proc call the error unwound through
|
|
54
|
+
attr_reader :tcl_backtrace
|
|
55
|
+
|
|
56
|
+
# @return [String, nil] Tcl's +errorCode+ for the failing call (a Tcl
|
|
57
|
+
# list, typically "NONE" unless the failing command set one explicitly)
|
|
58
|
+
attr_reader :tcl_error_code
|
|
59
|
+
|
|
60
|
+
# @api private
|
|
61
|
+
def initialize(message, tcl_backtrace = nil, tcl_error_code = nil)
|
|
62
|
+
super(message)
|
|
63
|
+
@tcl_backtrace = tcl_backtrace
|
|
64
|
+
@tcl_error_code = tcl_error_code
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Raised by App#command when more than one registered CommandInterceptor
|
|
69
|
+
# claims the same call. The message names each matching interceptor's
|
|
70
|
+
# label so it's clear which ones collided - built-in shape-matching bug,
|
|
71
|
+
# a custom interceptor overlapping a built-in one, or two custom
|
|
72
|
+
# interceptors overlapping each other.
|
|
73
|
+
class AmbiguousCommandError < RuntimeError; end
|
|
74
|
+
|
|
33
75
|
def self.bool_to_tcl(val)
|
|
34
76
|
val ? "1" : "0"
|
|
35
77
|
end
|
|
@@ -48,19 +90,25 @@ module Teek
|
|
|
48
90
|
].freeze
|
|
49
91
|
|
|
50
92
|
class App
|
|
51
|
-
attr_reader :interp, :widgets, :debugger
|
|
93
|
+
attr_reader :interp, :widgets, :debugger, :callback_registry, :winfo, :wm
|
|
52
94
|
attr_writer :_pending_exception # @api private
|
|
53
95
|
|
|
54
96
|
def initialize(title: nil, track_widgets: true, debug: false, &block)
|
|
55
97
|
@interp = Teek::Interp.new
|
|
56
98
|
@interp.tcl_eval('package require Tk')
|
|
99
|
+
@winfo = Teek::Winfo.new(self)
|
|
100
|
+
@wm = Teek::Wm.new(self)
|
|
57
101
|
hide
|
|
58
102
|
@widgets = {}
|
|
59
103
|
@widget_counters = Hash.new(0)
|
|
104
|
+
@callback_registry = Teek::CallbackRegistry.new(self)
|
|
105
|
+
@widget_types_by_path = {}
|
|
60
106
|
@_pending_exception = nil
|
|
61
107
|
debug ||= !!ENV['TEEK_DEBUG']
|
|
62
108
|
track_widgets = true if debug
|
|
109
|
+
@track_widgets = track_widgets
|
|
63
110
|
setup_widget_tracking if track_widgets
|
|
111
|
+
setup_destroy_cleanup
|
|
64
112
|
if debug
|
|
65
113
|
require_relative 'teek/debugger'
|
|
66
114
|
@debugger = Teek::Debugger.new(self)
|
|
@@ -73,6 +121,14 @@ module Teek
|
|
|
73
121
|
# Prefer {#command} for building commands from Ruby values; use this
|
|
74
122
|
# when you need Tcl-level features like variable substitution or
|
|
75
123
|
# inline expressions that {#command} can't express.
|
|
124
|
+
# @note Any callback embedded in +script+ (e.g. a hand-built
|
|
125
|
+
# +ruby_callback <id>+) is on you to register and release - none of
|
|
126
|
+
# {#command}'s tracking applies here. Creating a widget this way
|
|
127
|
+
# (instead of via {#command}/{#create_widget}/{#menu}) also means its
|
|
128
|
+
# type is never recorded, so a registered {CommandInterceptors} entry
|
|
129
|
+
# won't engage for it even on later, ordinary {#command} calls -
|
|
130
|
+
# create widgets through those methods and reach for +tcl_eval+ for
|
|
131
|
+
# everything else.
|
|
76
132
|
# @param script [String] Tcl code to evaluate
|
|
77
133
|
# @return [String] the Tcl result
|
|
78
134
|
def tcl_eval(script)
|
|
@@ -92,10 +148,25 @@ module Teek
|
|
|
92
148
|
# throw :teek_break - stop event propagation (like Tcl "break")
|
|
93
149
|
# throw :teek_continue - Tcl TCL_CONTINUE
|
|
94
150
|
# throw :teek_return - Tcl TCL_RETURN
|
|
151
|
+
#
|
|
152
|
+
# +:teek_break+/+:teek_continue+ only mean something when Tcl actually
|
|
153
|
+
# dispatches the result through a context that knows how to handle
|
|
154
|
+
# TCL_BREAK/TCL_CONTINUE - Tk's bind mechanism does; a plain script
|
|
155
|
+
# invocation (a menu entry's or widget's `-command`) does not, and
|
|
156
|
+
# returning either code there is a Tcl error ("invoked break/continue
|
|
157
|
+
# outside of a loop"), not a no-op. +relay_break_continue: false+ is
|
|
158
|
+
# for exactly those non-bind callers: +throw+ is still caught (so it
|
|
159
|
+
# can't crash as an uncaught throw), but is treated as the callback
|
|
160
|
+
# simply finishing, instead of being relayed to Tcl. +:teek_return+ is
|
|
161
|
+
# always relayed either way - TCL_RETURN is safe in any context.
|
|
95
162
|
# @param callable [#call] a Proc or lambda to invoke from Tcl
|
|
163
|
+
# @param relay_break_continue [Boolean] whether a caught :teek_break/
|
|
164
|
+
# :teek_continue is relayed to Tcl as TCL_BREAK/TCL_CONTINUE (true,
|
|
165
|
+
# for bind-dispatched callbacks) or silently absorbed (false, for
|
|
166
|
+
# callbacks invoked as a plain script - menu/widget -command options)
|
|
96
167
|
# @return [Integer] callback ID, usable as +ruby_callback <id>+ in Tcl
|
|
97
168
|
# @see #unregister_callback
|
|
98
|
-
def register_callback(callable)
|
|
169
|
+
def register_callback(callable, relay_break_continue: true)
|
|
99
170
|
wrapped = proc { |*args|
|
|
100
171
|
caught = nil
|
|
101
172
|
catch(:teek_break) do
|
|
@@ -109,7 +180,9 @@ module Teek
|
|
|
109
180
|
caught ||= :continue
|
|
110
181
|
end
|
|
111
182
|
caught ||= :break
|
|
112
|
-
caught == :_none
|
|
183
|
+
next nil if caught == :_none
|
|
184
|
+
next caught if caught == :return
|
|
185
|
+
relay_break_continue ? caught : nil
|
|
113
186
|
}
|
|
114
187
|
@interp.register_callback(wrapped)
|
|
115
188
|
end
|
|
@@ -121,6 +194,20 @@ module Teek
|
|
|
121
194
|
@interp.unregister_callback(id)
|
|
122
195
|
end
|
|
123
196
|
|
|
197
|
+
# Evaluate +script+ once per App instance under +name+, skipping it on
|
|
198
|
+
# later calls. Meant for widget-behavior modules that need to define a
|
|
199
|
+
# Tcl-side helper proc (e.g. a scan routine) without re-sending and
|
|
200
|
+
# re-parsing that definition on every call.
|
|
201
|
+
# @param name [Symbol] unique name for this helper
|
|
202
|
+
# @yieldreturn [String] the Tcl script to evaluate the first time
|
|
203
|
+
# @return [void]
|
|
204
|
+
def ensure_tcl_helper(name)
|
|
205
|
+
@installed_tcl_helpers ||= {}
|
|
206
|
+
return if @installed_tcl_helpers[name]
|
|
207
|
+
@interp.tcl_eval(yield)
|
|
208
|
+
@installed_tcl_helpers[name] = true
|
|
209
|
+
end
|
|
210
|
+
|
|
124
211
|
# Schedule a one-shot timer. Calls the block after +ms+ milliseconds.
|
|
125
212
|
# @param ms [Integer] delay in milliseconds
|
|
126
213
|
# @param on_error [:raise, Proc, nil] error handling strategy:
|
|
@@ -230,10 +317,18 @@ module Teek
|
|
|
230
317
|
Teek.bool_to_tcl(val)
|
|
231
318
|
end
|
|
232
319
|
|
|
233
|
-
# Build and evaluate a Tcl command from Ruby values.
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
320
|
+
# Build and evaluate a Tcl command from Ruby values. Positional args
|
|
321
|
+
# are converted: Symbols pass bare, Procs become callbacks, everything
|
|
322
|
+
# else is brace-quoted. Keyword args become +-key value+ option pairs.
|
|
323
|
+
#
|
|
324
|
+
# Any Proc-valued arg or kwarg is tracked and released on overwrite,
|
|
325
|
+
# explicit removal, or the owning widget's destruction - there is no
|
|
326
|
+
# unsafe way to attach a callback through this method, regardless of
|
|
327
|
+
# whether the call happens to match a registered per-widget-type
|
|
328
|
+
# interceptor (see {CommandInterceptors}) or falls through to the
|
|
329
|
+
# generic default. Widget type is inferred automatically from calls
|
|
330
|
+
# shaped like widget creation (a {WIDGET_COMMANDS} name as +cmd+, the
|
|
331
|
+
# new path as the first positional arg) - not tied to +track_widgets+.
|
|
237
332
|
# @example
|
|
238
333
|
# app.command(:pack, '.btn', side: :left, padx: 10)
|
|
239
334
|
# # evaluates: pack .btn -side left -padx {10}
|
|
@@ -241,33 +336,126 @@ module Teek
|
|
|
241
336
|
# @param args positional arguments
|
|
242
337
|
# @param kwargs keyword arguments mapped to +-key value+ pairs
|
|
243
338
|
# @return [String] the Tcl result
|
|
339
|
+
# @raise [AmbiguousCommandError] if more than one registered
|
|
340
|
+
# interceptor claims the same call - see {CommandInterceptors}
|
|
244
341
|
def command(cmd, *args, **kwargs)
|
|
245
|
-
|
|
342
|
+
record_widget_type(cmd, args)
|
|
343
|
+
|
|
344
|
+
type = @widget_types_by_path[cmd.to_s]
|
|
345
|
+
entries = type ? CommandInterceptors.for_type(type) : []
|
|
346
|
+
matches = entries.map { |entry| [entry.label, entry.block.call(self, cmd, args, kwargs)] }
|
|
347
|
+
.reject { |_label, result| result.nil? }
|
|
348
|
+
|
|
349
|
+
case matches.size
|
|
350
|
+
when 0 then raw_command(cmd, *args, **track_widget_option_callbacks(cmd, args, kwargs))
|
|
351
|
+
when 1 then matches.first.last
|
|
352
|
+
else
|
|
353
|
+
labels = matches.map(&:first).join(', ')
|
|
354
|
+
raise AmbiguousCommandError,
|
|
355
|
+
"#{matches.size} command interceptors (#{labels}) matched #{cmd.inspect} #{args.inspect} " \
|
|
356
|
+
"for widget type #{type.inspect}"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The dumb Tcl builder underneath {#command} - no interceptor lookup,
|
|
361
|
+
# no per-widget-type awareness. Used internally by interceptors (to
|
|
362
|
+
# actually perform their Tcl work without re-entering dispatch) and by
|
|
363
|
+
# {#command}'s own generic fallback. Any Proc here still gets
|
|
364
|
+
# registered as a real, working callback (positional: bind-shaped,
|
|
365
|
+
# relay_break_continue: true; kwarg, via {#tcl_arg_value}: option-shaped,
|
|
366
|
+
# relay_break_continue: false) - it just isn't tracked for release.
|
|
367
|
+
# Prefer {#command}; call this directly only from within an
|
|
368
|
+
# interceptor.
|
|
369
|
+
#
|
|
370
|
+
# Built as a plain argv array passed to {Interp#tcl_invoke}
|
|
371
|
+
# (Tcl_EvalObjv) rather than a joined string handed to +tcl_eval+, so
|
|
372
|
+
# no value needs escaping - unbalanced braces, +$+, +[+, newlines,
|
|
373
|
+
# whatever, all pass through verbatim. There is nothing here for a
|
|
374
|
+
# value to "break out" of.
|
|
375
|
+
# @return [String] the Tcl result
|
|
376
|
+
def raw_command(cmd, *args, **kwargs)
|
|
377
|
+
argv = [cmd.to_s]
|
|
246
378
|
i = 0
|
|
247
379
|
while i < args.length
|
|
248
380
|
arg = args[i]
|
|
249
381
|
if arg.is_a?(Proc)
|
|
250
|
-
id =
|
|
382
|
+
id = register_callback(arg)
|
|
251
383
|
subs = []
|
|
252
384
|
while i + 1 < args.length && args[i + 1].is_a?(String) && args[i + 1].start_with?('%')
|
|
253
385
|
subs << args[i + 1]
|
|
254
386
|
i += 1
|
|
255
387
|
end
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
388
|
+
argv << if subs.empty?
|
|
389
|
+
"ruby_callback #{id}"
|
|
390
|
+
else
|
|
391
|
+
"ruby_callback #{id} #{subs.join(' ')}"
|
|
392
|
+
end
|
|
261
393
|
else
|
|
262
|
-
|
|
394
|
+
argv << tcl_arg_value(arg)
|
|
263
395
|
end
|
|
264
396
|
i += 1
|
|
265
397
|
end
|
|
266
398
|
kwargs.each do |key, value|
|
|
267
|
-
|
|
268
|
-
|
|
399
|
+
argv << "-#{key}"
|
|
400
|
+
argv << tcl_arg_value(value)
|
|
401
|
+
end
|
|
402
|
+
@interp.tcl_invoke(*argv)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# {#command}'s fallback for any call that no registered interceptor
|
|
406
|
+
# claimed: registers any Proc-valued kwarg (e.g. command:,
|
|
407
|
+
# validatecommand:) as a callback tracked under +cmd+, releasing it if
|
|
408
|
+
# reconfigured or when the widget is destroyed. A widget's own options
|
|
409
|
+
# are never silently renumbered or invalidated out from under us the
|
|
410
|
+
# way menu entries are, so this uses a cheap in-memory
|
|
411
|
+
# {CallbackRegistry#reconcile} rather than a live-scan one.
|
|
412
|
+
#
|
|
413
|
+
# Tracked under the widget's own path, by +[*context, key]+, where
|
|
414
|
+
# +context+ is +args+ normalized to strings - except a bare +configure+
|
|
415
|
+
# (or widget creation - the container is the new widget's path, not the
|
|
416
|
+
# +cmd+ used to create it) normalizes to an empty context, since all
|
|
417
|
+
# three address the same underlying option namespace and must replace
|
|
418
|
+
# each other. Any other subcommand (e.g. a treeview's +heading col+)
|
|
419
|
+
# keeps its own args as part of the key, so two different targets
|
|
420
|
+
# sharing an option name (two columns both using +command:+) don't
|
|
421
|
+
# collide.
|
|
422
|
+
# @return [Hash] +kwargs+ with any Proc values swapped for the Tcl
|
|
423
|
+
# script {#raw_command} embeds
|
|
424
|
+
def track_widget_option_callbacks(cmd, args, kwargs)
|
|
425
|
+
proc_kwargs = kwargs.select { |_, value| value.is_a?(Proc) }
|
|
426
|
+
return kwargs if proc_kwargs.empty?
|
|
427
|
+
|
|
428
|
+
if WIDGET_COMMANDS.include?(cmd.to_s) && args[0].is_a?(String)
|
|
429
|
+
widget_path = args[0]
|
|
430
|
+
context = []
|
|
431
|
+
else
|
|
432
|
+
widget_path = cmd.to_s
|
|
433
|
+
context = (args.empty? || args[0].to_s == 'configure') ? [] : args.map(&:to_s)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
ids = {}
|
|
437
|
+
replacements = {}
|
|
438
|
+
proc_kwargs.each do |key, value|
|
|
439
|
+
id = register_callback(value, relay_break_continue: false)
|
|
440
|
+
ids[context + [key.to_s]] = id
|
|
441
|
+
replacements[key] = "ruby_callback #{id}"
|
|
269
442
|
end
|
|
270
|
-
@
|
|
443
|
+
@callback_registry.reconcile([:widget_option, widget_path]) { |before| before.merge(ids) }
|
|
444
|
+
kwargs.merge(replacements)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Records that +args[0]+ is a widget of type +cmd+, if this call looks
|
|
448
|
+
# like widget creation (+cmd+ is a known {WIDGET_COMMANDS} entry). Not
|
|
449
|
+
# tied to +track_widgets+ - this is what lets {#command} look up a
|
|
450
|
+
# registered interceptor for a bare path string on any later call,
|
|
451
|
+
# regardless of how the widget was created.
|
|
452
|
+
# @return [void]
|
|
453
|
+
def record_widget_type(cmd, args)
|
|
454
|
+
return unless WIDGET_COMMANDS.include?(cmd.to_s)
|
|
455
|
+
path = args[0]
|
|
456
|
+
return unless path.is_a?(String)
|
|
457
|
+
|
|
458
|
+
@widget_types_by_path[path] = cmd.to_s
|
|
271
459
|
end
|
|
272
460
|
|
|
273
461
|
# Create a Tk widget and return a {Widget} wrapper.
|
|
@@ -278,6 +466,10 @@ module Teek
|
|
|
278
466
|
# @param type [String, Symbol] Tk widget command (e.g. 'ttk::button', :canvas)
|
|
279
467
|
# @param path [String, nil] explicit Tk path, or nil for auto-naming
|
|
280
468
|
# @param parent [Widget, String, nil] parent widget for path nesting
|
|
469
|
+
# @param idempotent [Boolean] skip the creation command if a widget
|
|
470
|
+
# already exists at +path+ - for widgets meant to be fetched by a
|
|
471
|
+
# stable, caller-chosen path and reused across many calls (see {#menu})
|
|
472
|
+
# rather than freshly created each time
|
|
281
473
|
# @param kwargs keyword arguments passed to the Tk widget command
|
|
282
474
|
# @return [Widget] the created widget
|
|
283
475
|
#
|
|
@@ -293,13 +485,33 @@ module Teek
|
|
|
293
485
|
# btn = app.create_widget('ttk::button', parent: frm, text: 'Click')
|
|
294
486
|
# # btn.path => ".ttkfrm1.ttkbtn1"
|
|
295
487
|
#
|
|
296
|
-
def create_widget(type, path = nil, parent: nil, **kwargs)
|
|
488
|
+
def create_widget(type, path = nil, parent: nil, idempotent: false, **kwargs)
|
|
297
489
|
type_s = type.to_s
|
|
298
490
|
path ||= next_widget_path(type_s, parent)
|
|
299
|
-
|
|
491
|
+
if idempotent && @winfo.exists?(path)
|
|
492
|
+
# Still record the type even though creation itself is skipped - a
|
|
493
|
+
# path that already existed (e.g. built with a raw tcl_eval) is
|
|
494
|
+
# otherwise never seen by #command, so no interceptor could ever
|
|
495
|
+
# engage for it.
|
|
496
|
+
record_widget_type(type_s, [path])
|
|
497
|
+
else
|
|
498
|
+
command(type_s, path, **kwargs)
|
|
499
|
+
end
|
|
300
500
|
Widget.new(self, path)
|
|
301
501
|
end
|
|
302
502
|
|
|
503
|
+
# Wrap a Tk menu at the given path, creating it (tearoff disabled) if
|
|
504
|
+
# it doesn't exist yet. Safe to call repeatedly with the same path -
|
|
505
|
+
# it's a flyweight, not a handle you need to hold onto: call this again
|
|
506
|
+
# any time you're about to rebuild the menu (e.g. on every right-click).
|
|
507
|
+
# @param path [String] Tk menu path (e.g. ".card.ctx")
|
|
508
|
+
# @param kwargs extra options for the underlying `menu` command, used
|
|
509
|
+
# only the first time this path is created
|
|
510
|
+
# @return [Widget]
|
|
511
|
+
def menu(path, **kwargs)
|
|
512
|
+
create_widget(:menu, path, idempotent: true, tearoff: 0, **kwargs)
|
|
513
|
+
end
|
|
514
|
+
|
|
303
515
|
# Add a directory to Tcl's package search path.
|
|
304
516
|
# @param path [String] directory containing Tcl packages
|
|
305
517
|
# @return [void]
|
|
@@ -351,21 +563,25 @@ module Teek
|
|
|
351
563
|
end
|
|
352
564
|
|
|
353
565
|
# Set a Tcl variable. Useful for widget +textvariable+ and +variable+ options.
|
|
354
|
-
#
|
|
566
|
+
# Goes through Tcl_SetVar directly (no re-parsing), so the value never
|
|
567
|
+
# needs escaping - braces, backslashes, +$+, +[+, whatever, all safe.
|
|
568
|
+
# @param name [String] variable name (array-element and namespaced forms work)
|
|
355
569
|
# @param value [String] value to set
|
|
356
570
|
# @return [String] the value
|
|
357
571
|
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/set.htm set
|
|
358
572
|
def set_variable(name, value)
|
|
359
|
-
|
|
573
|
+
@interp.tcl_set_var(name.to_s, value.to_s)
|
|
360
574
|
end
|
|
361
575
|
|
|
362
576
|
# Get a Tcl variable's value.
|
|
363
|
-
# @param name [String] variable name
|
|
577
|
+
# @param name [String] variable name (array-element and namespaced forms work)
|
|
364
578
|
# @return [String] the value
|
|
365
579
|
# @raise [Teek::TclError] if the variable doesn't exist
|
|
366
580
|
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/set.htm set
|
|
367
581
|
def get_variable(name)
|
|
368
|
-
|
|
582
|
+
value = @interp.tcl_get_var(name.to_s)
|
|
583
|
+
return value unless value.nil?
|
|
584
|
+
raise Teek::TclError, "can't read \"#{name}\": no such variable"
|
|
369
585
|
end
|
|
370
586
|
|
|
371
587
|
# Destroy a widget and all its children.
|
|
@@ -464,17 +680,19 @@ module Teek
|
|
|
464
680
|
# Show a window. Defaults to the root window (".").
|
|
465
681
|
# @param window [String] Tk window path
|
|
466
682
|
# @return [void]
|
|
683
|
+
# @see Wm#deiconify
|
|
467
684
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M38 wm deiconify
|
|
468
685
|
def show(window = '.')
|
|
469
|
-
@
|
|
686
|
+
@wm.deiconify(window: window)
|
|
470
687
|
end
|
|
471
688
|
|
|
472
689
|
# Hide a window without destroying it. Defaults to the root window (".").
|
|
473
690
|
# @param window [String] Tk window path
|
|
474
691
|
# @return [void]
|
|
692
|
+
# @see Wm#withdraw
|
|
475
693
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M65 wm withdraw
|
|
476
694
|
def hide(window = '.')
|
|
477
|
-
@
|
|
695
|
+
@wm.withdraw(window: window)
|
|
478
696
|
end
|
|
479
697
|
|
|
480
698
|
# Enable the Tk debug console. The console starts hidden and can be
|
|
@@ -519,34 +737,38 @@ module Teek
|
|
|
519
737
|
# @param title [String] new title
|
|
520
738
|
# @param window [String] Tk window path
|
|
521
739
|
# @return [String] the title
|
|
740
|
+
# @see Wm#set_title
|
|
522
741
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
|
|
523
742
|
def set_window_title(title, window: '.')
|
|
524
|
-
|
|
743
|
+
@wm.set_title(title, window: window)
|
|
525
744
|
end
|
|
526
745
|
|
|
527
746
|
# Get a window's current title.
|
|
528
747
|
# @param window [String] Tk window path
|
|
529
748
|
# @return [String] current title
|
|
749
|
+
# @see Wm#title
|
|
530
750
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
|
|
531
751
|
def window_title(window: '.')
|
|
532
|
-
|
|
752
|
+
@wm.title(window: window)
|
|
533
753
|
end
|
|
534
754
|
|
|
535
755
|
# Set a window's geometry (e.g. "400x300", "400x300+100+50").
|
|
536
756
|
# @param geometry [String] geometry string
|
|
537
757
|
# @param window [String] Tk window path
|
|
538
758
|
# @return [String] the geometry
|
|
759
|
+
# @see Wm#set_geometry
|
|
539
760
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
|
|
540
761
|
def set_window_geometry(geometry, window: '.')
|
|
541
|
-
|
|
762
|
+
@wm.set_geometry(geometry, window: window)
|
|
542
763
|
end
|
|
543
764
|
|
|
544
765
|
# Get a window's current geometry.
|
|
545
766
|
# @param window [String] Tk window path
|
|
546
767
|
# @return [String] geometry string (e.g. "400x300+0+0")
|
|
768
|
+
# @see Wm#geometry
|
|
547
769
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
|
|
548
770
|
def window_geometry(window: '.')
|
|
549
|
-
|
|
771
|
+
@wm.geometry(window: window)
|
|
550
772
|
end
|
|
551
773
|
|
|
552
774
|
# Set whether a window is resizable.
|
|
@@ -554,18 +776,19 @@ module Teek
|
|
|
554
776
|
# @param height [Boolean] allow vertical resize
|
|
555
777
|
# @param window [String] Tk window path
|
|
556
778
|
# @return [void]
|
|
779
|
+
# @see Wm#set_resizable
|
|
557
780
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
|
|
558
781
|
def set_window_resizable(width, height, window: '.')
|
|
559
|
-
|
|
782
|
+
@wm.set_resizable(width, height, window: window)
|
|
560
783
|
end
|
|
561
784
|
|
|
562
785
|
# Get whether a window is resizable.
|
|
563
786
|
# @param window [String] Tk window path
|
|
564
787
|
# @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
|
|
788
|
+
# @see Wm#resizable
|
|
565
789
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
|
|
566
790
|
def window_resizable(window: '.')
|
|
567
|
-
|
|
568
|
-
[parts[0] == '1', parts[1] == '1']
|
|
791
|
+
@wm.resizable(window: window)
|
|
569
792
|
end
|
|
570
793
|
|
|
571
794
|
# Bind a Tk event on a widget, with optional substitutions forwarded
|
|
@@ -616,6 +839,7 @@ module Teek
|
|
|
616
839
|
def bind(widget, event, *subs, &block)
|
|
617
840
|
event_str = event.start_with?('<') ? event : "<#{event}>"
|
|
618
841
|
cb = register_callback(proc { |*args| block.call(*args) })
|
|
842
|
+
@callback_registry.reconcile([:bind, widget]) { |before| before.merge(event_str => cb) }
|
|
619
843
|
tcl_subs = subs.map { |s| s.is_a?(Symbol) ? BIND_SUBS.fetch(s) : s.to_s }
|
|
620
844
|
sub_str = tcl_subs.empty? ? '' : ' ' + tcl_subs.join(' ')
|
|
621
845
|
@interp.tcl_eval("bind #{widget} #{event_str} {ruby_callback #{cb}#{sub_str}}")
|
|
@@ -629,9 +853,36 @@ module Teek
|
|
|
629
853
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/bind.htm bind
|
|
630
854
|
def unbind(widget, event)
|
|
631
855
|
event_str = event.start_with?('<') ? event : "<#{event}>"
|
|
856
|
+
@callback_registry.reconcile([:bind, widget]) { |before| before.reject { |k, _| k == event_str } }
|
|
632
857
|
@interp.tcl_eval("bind #{widget} #{event_str} {}")
|
|
633
858
|
end
|
|
634
859
|
|
|
860
|
+
# Register a handler for the window manager's close button
|
|
861
|
+
# (WM_DELETE_WINDOW - the titlebar close box, Cmd-W, Alt-F4, etc.,
|
|
862
|
+
# depending on platform).
|
|
863
|
+
#
|
|
864
|
+
# Tk's own default behavior (destroy the window) only applies when
|
|
865
|
+
# nothing else has claimed this protocol - setting a handler here
|
|
866
|
+
# replaces it, so the block is entirely responsible for deciding
|
|
867
|
+
# whether the window actually closes. Call {#destroy} yourself if you
|
|
868
|
+
# want it to; do nothing (or show a confirmation first) if you don't.
|
|
869
|
+
#
|
|
870
|
+
# @example Confirm before quitting
|
|
871
|
+
# app.on_close { app.destroy('.') if app.message_box(message: 'Quit?', type: :yesno) == :yes }
|
|
872
|
+
# @example A toplevel that just hides instead of closing
|
|
873
|
+
# app.on_close(window: settings_window) { app.tcl_eval("wm withdraw #{settings_window}") }
|
|
874
|
+
#
|
|
875
|
+
# @param window [String] Tk window path (default: the root window)
|
|
876
|
+
# @yield called when the window's close button is pressed
|
|
877
|
+
# @return [void]
|
|
878
|
+
# @see #bind
|
|
879
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/wm.htm#M46 wm protocol
|
|
880
|
+
def on_close(window: '.', &block)
|
|
881
|
+
cb = register_callback(block, relay_break_continue: false)
|
|
882
|
+
@callback_registry.reconcile([:wm_protocol, window]) { |before| before.merge('WM_DELETE_WINDOW' => cb) }
|
|
883
|
+
@interp.tcl_eval("wm protocol #{window} WM_DELETE_WINDOW {ruby_callback #{cb}}")
|
|
884
|
+
end
|
|
885
|
+
|
|
635
886
|
# Register a widget as a file drop target.
|
|
636
887
|
# After registration, dropping files onto the widget generates a single
|
|
637
888
|
# +<<DropFile>>+ virtual event with all file paths as a Tcl list in the
|
|
@@ -761,11 +1012,6 @@ module Teek
|
|
|
761
1012
|
@widgets[path] = { class: cls, parent: File.dirname(path).gsub(/\A$/, '.') }
|
|
762
1013
|
@debugger&.on_widget_created(path, cls)
|
|
763
1014
|
})
|
|
764
|
-
@destroy_cb_id = @interp.register_callback(proc { |path|
|
|
765
|
-
next if path.start_with?('.teek_debug')
|
|
766
|
-
@widgets.delete(path)
|
|
767
|
-
@debugger&.on_widget_destroyed(path)
|
|
768
|
-
})
|
|
769
1015
|
|
|
770
1016
|
# Tcl proc called on widget creation (trace leave)
|
|
771
1017
|
@interp.tcl_eval("proc ::teek_track_create {cmd_string code result op} {
|
|
@@ -776,37 +1022,51 @@ module Teek
|
|
|
776
1022
|
}
|
|
777
1023
|
}")
|
|
778
1024
|
|
|
779
|
-
# Tcl proc called on widget destruction (bind)
|
|
780
|
-
@interp.tcl_eval("bind all <Destroy> {ruby_callback #{@destroy_cb_id} %W}")
|
|
781
|
-
|
|
782
1025
|
# Add trace on each widget command
|
|
783
1026
|
Teek::WIDGET_COMMANDS.each do |cmd|
|
|
784
1027
|
@interp.tcl_eval("catch {trace add execution #{cmd} leave ::teek_track_create}")
|
|
785
1028
|
end
|
|
786
1029
|
end
|
|
787
1030
|
|
|
788
|
-
|
|
1031
|
+
# Installed unconditionally (unlike widget-creation tracking, which is
|
|
1032
|
+
# opt-out via track_widgets: false) so that bind-callback cleanup always
|
|
1033
|
+
# runs. A single `bind all <Destroy>` script is used because Tcl's bind
|
|
1034
|
+
# command replaces rather than appends per tag+event, so widget-tracking
|
|
1035
|
+
# cleanup is folded into the same callback rather than installed separately.
|
|
1036
|
+
def setup_destroy_cleanup
|
|
1037
|
+
@destroy_cb_id = @interp.register_callback(proc { |path|
|
|
1038
|
+
@callback_registry.forget_all_for_path(path)
|
|
1039
|
+
next if path.start_with?('.teek_debug')
|
|
1040
|
+
if @track_widgets
|
|
1041
|
+
@widgets.delete(path)
|
|
1042
|
+
@debugger&.on_widget_destroyed(path)
|
|
1043
|
+
end
|
|
1044
|
+
})
|
|
1045
|
+
@interp.tcl_eval("bind all <Destroy> {ruby_callback #{@destroy_cb_id} %W}")
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
# Resolves a Ruby value to the plain string {#raw_command} passes as
|
|
1049
|
+
# one +tcl_invoke+ argv element - no Tcl quoting of any kind, since
|
|
1050
|
+
# +tcl_invoke+ (Tcl_EvalObjv) never re-parses its arguments.
|
|
1051
|
+
def tcl_arg_value(value)
|
|
789
1052
|
case value
|
|
790
1053
|
when Proc
|
|
791
|
-
|
|
792
|
-
|
|
1054
|
+
# A Proc reaching tcl_arg_value is always a kwarg/option value
|
|
1055
|
+
# (e.g. -command), never a bind script - see #register_callback's
|
|
1056
|
+
# note on why break/continue can't be relayed there.
|
|
1057
|
+
id = register_callback(value, relay_break_continue: false)
|
|
1058
|
+
"ruby_callback #{id}"
|
|
793
1059
|
when Symbol
|
|
794
1060
|
value.to_s
|
|
795
1061
|
when Array
|
|
796
|
-
|
|
1062
|
+
# Each element becomes its own well-formed Tcl list element via
|
|
1063
|
+
# make_list (which quotes only where needed), so this produces a
|
|
1064
|
+
# single argv value that Tk parses back out as a proper list -
|
|
1065
|
+
# nested arrays fall out for free, since make_list happily
|
|
1066
|
+
# accepts an already-list-shaped string as one of its elements.
|
|
1067
|
+
make_list(*value.map { |v| tcl_arg_value(v) })
|
|
797
1068
|
else
|
|
798
|
-
|
|
799
|
-
end
|
|
800
|
-
end
|
|
801
|
-
|
|
802
|
-
# Brace-quote a string for Tcl, falling back to double-quote quoting
|
|
803
|
-
# when the string ends with a backslash (Tcl treats \} as an escaped
|
|
804
|
-
# brace, preventing the closing brace from terminating the group).
|
|
805
|
-
def tcl_quote_string(s)
|
|
806
|
-
if s.end_with?('\\')
|
|
807
|
-
'"' + s.gsub(/[\\\[\]$"]/) { |c| "\\#{c}" } + '"'
|
|
808
|
-
else
|
|
809
|
-
"{#{s}}"
|
|
1069
|
+
value.to_s
|
|
810
1070
|
end
|
|
811
1071
|
end
|
|
812
1072
|
end
|
data/teek.gemspec
CHANGED
|
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
20
20
|
spec.require_paths = ["lib"]
|
|
21
21
|
spec.extensions = ["ext/teek/extconf.rb"]
|
|
22
|
-
spec.required_ruby_version = ">= 3.
|
|
22
|
+
spec.required_ruby_version = ">= 3.3"
|
|
23
23
|
|
|
24
24
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
25
25
|
spec.add_development_dependency "rake-compiler", "~> 1.0"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: teek
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Cook
|
|
@@ -137,14 +137,22 @@ files:
|
|
|
137
137
|
- lib/teek/background_none.rb
|
|
138
138
|
- lib/teek/background_ractor4x.rb
|
|
139
139
|
- lib/teek/background_thread.rb
|
|
140
|
+
- lib/teek/callback_registry.rb
|
|
141
|
+
- lib/teek/canvas_bind_interceptor.rb
|
|
142
|
+
- lib/teek/command_interceptors.rb
|
|
140
143
|
- lib/teek/debugger.rb
|
|
141
144
|
- lib/teek/demo_support.rb
|
|
145
|
+
- lib/teek/dialogs.rb
|
|
146
|
+
- lib/teek/menu_interceptor.rb
|
|
142
147
|
- lib/teek/method_coverage_service.rb
|
|
143
148
|
- lib/teek/photo.rb
|
|
144
149
|
- lib/teek/platform.rb
|
|
145
150
|
- lib/teek/ractor_support.rb
|
|
151
|
+
- lib/teek/tag_bind_interceptor.rb
|
|
146
152
|
- lib/teek/version.rb
|
|
147
153
|
- lib/teek/widget.rb
|
|
154
|
+
- lib/teek/winfo.rb
|
|
155
|
+
- lib/teek/wm.rb
|
|
148
156
|
- teek.gemspec
|
|
149
157
|
homepage: https://github.com/jamescook/teek
|
|
150
158
|
licenses:
|
|
@@ -158,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
158
166
|
requirements:
|
|
159
167
|
- - ">="
|
|
160
168
|
- !ruby/object:Gem::Version
|
|
161
|
-
version: '3.
|
|
169
|
+
version: '3.3'
|
|
162
170
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
171
|
requirements:
|
|
164
172
|
- - ">="
|