teek 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/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +139 -0
- data/Rakefile +316 -0
- data/ext/teek/extconf.rb +79 -0
- data/ext/teek/stubs.h +33 -0
- data/ext/teek/tcl9compat.h +211 -0
- data/ext/teek/tcltkbridge.c +1597 -0
- data/ext/teek/tcltkbridge.h +42 -0
- data/ext/teek/tkfont.c +218 -0
- data/ext/teek/tkphoto.c +477 -0
- data/ext/teek/tkwin.c +144 -0
- data/lib/teek/background_none.rb +158 -0
- data/lib/teek/background_ractor4x.rb +410 -0
- data/lib/teek/background_thread.rb +272 -0
- data/lib/teek/debugger.rb +742 -0
- data/lib/teek/demo_support.rb +150 -0
- data/lib/teek/ractor_support.rb +246 -0
- data/lib/teek/version.rb +5 -0
- data/lib/teek.rb +540 -0
- data/sample/calculator.rb +260 -0
- data/sample/debug_demo.rb +45 -0
- data/sample/goldberg.rb +1803 -0
- data/sample/goldberg_helpers.rb +170 -0
- data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
- data/sample/minesweeper/minesweeper.rb +452 -0
- data/sample/threading_demo.rb +499 -0
- data/teek.gemspec +32 -0
- metadata +179 -0
data/lib/teek.rb
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tcltklib'
|
|
4
|
+
require_relative 'teek/version'
|
|
5
|
+
require_relative 'teek/ractor_support'
|
|
6
|
+
|
|
7
|
+
# Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
|
|
8
|
+
# with Ruby callbacks, event bindings, and background work support.
|
|
9
|
+
#
|
|
10
|
+
# The main entry point is {Teek::App}, which initializes Tcl/Tk and provides
|
|
11
|
+
# methods for evaluating Tcl code, creating widgets, and running the event loop.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# app = Teek::App.new
|
|
15
|
+
# app.command('ttk::button', '.btn', text: 'Click', command: proc { puts "hi" })
|
|
16
|
+
# app.command(:pack, '.btn')
|
|
17
|
+
# app.show
|
|
18
|
+
# app.mainloop
|
|
19
|
+
#
|
|
20
|
+
# @example Background work (keeps UI responsive)
|
|
21
|
+
# app.background_work(urls, mode: :thread) do |task, data|
|
|
22
|
+
# data.each { |url| task.yield(fetch(url)) }
|
|
23
|
+
# end.on_progress { |result| update_ui(result) }
|
|
24
|
+
# .on_done { puts "Finished" }
|
|
25
|
+
#
|
|
26
|
+
# @see Teek::App
|
|
27
|
+
# @see Teek::BackgroundWork
|
|
28
|
+
module Teek
|
|
29
|
+
|
|
30
|
+
def self.bool_to_tcl(val)
|
|
31
|
+
val ? "1" : "0"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
WIDGET_COMMANDS = %w[
|
|
35
|
+
button label frame entry text canvas listbox
|
|
36
|
+
scrollbar scale spinbox menu menubutton message
|
|
37
|
+
panedwindow labelframe checkbutton radiobutton
|
|
38
|
+
toplevel
|
|
39
|
+
ttk::button ttk::label ttk::frame ttk::entry
|
|
40
|
+
ttk::combobox ttk::checkbutton ttk::radiobutton
|
|
41
|
+
ttk::scale ttk::scrollbar ttk::spinbox ttk::separator
|
|
42
|
+
ttk::sizegrip ttk::progressbar ttk::notebook
|
|
43
|
+
ttk::panedwindow ttk::labelframe ttk::menubutton
|
|
44
|
+
ttk::treeview
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
class App
|
|
48
|
+
attr_reader :interp, :widgets, :debugger
|
|
49
|
+
|
|
50
|
+
def initialize(track_widgets: true, debug: false, &block)
|
|
51
|
+
@interp = Teek::Interp.new
|
|
52
|
+
@interp.tcl_eval('package require Tk')
|
|
53
|
+
hide
|
|
54
|
+
@widgets = {}
|
|
55
|
+
debug ||= !!ENV['TEEK_DEBUG']
|
|
56
|
+
track_widgets = true if debug
|
|
57
|
+
setup_widget_tracking if track_widgets
|
|
58
|
+
if debug
|
|
59
|
+
require_relative 'teek/debugger'
|
|
60
|
+
@debugger = Teek::Debugger.new(self)
|
|
61
|
+
end
|
|
62
|
+
instance_eval(&block) if block
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Evaluate a raw Tcl script string and return the result.
|
|
66
|
+
# Prefer {#command} for building commands from Ruby values; use this
|
|
67
|
+
# when you need Tcl-level features like variable substitution or
|
|
68
|
+
# inline expressions that {#command} can't express.
|
|
69
|
+
# @param script [String] Tcl code to evaluate
|
|
70
|
+
# @return [String] the Tcl result
|
|
71
|
+
def tcl_eval(script)
|
|
72
|
+
@interp.tcl_eval(script)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Invoke a Tcl command with pre-split arguments (no Tcl parsing).
|
|
76
|
+
# Safer than {#tcl_eval} when arguments may contain special characters.
|
|
77
|
+
# @param args [Array<String>] command name followed by arguments
|
|
78
|
+
# @return [String] the Tcl result
|
|
79
|
+
def tcl_invoke(*args)
|
|
80
|
+
@interp.tcl_invoke(*args)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Register a Ruby callable as a Tcl callback.
|
|
84
|
+
# The callable can use +throw+ for Tcl control flow:
|
|
85
|
+
# throw :teek_break - stop event propagation (like Tcl "break")
|
|
86
|
+
# throw :teek_continue - Tcl TCL_CONTINUE
|
|
87
|
+
# throw :teek_return - Tcl TCL_RETURN
|
|
88
|
+
# @param callable [#call] a Proc or lambda to invoke from Tcl
|
|
89
|
+
# @return [Integer] callback ID, usable as +ruby_callback <id>+ in Tcl
|
|
90
|
+
# @see #unregister_callback
|
|
91
|
+
def register_callback(callable)
|
|
92
|
+
wrapped = proc { |*args|
|
|
93
|
+
caught = nil
|
|
94
|
+
catch(:teek_break) do
|
|
95
|
+
catch(:teek_continue) do
|
|
96
|
+
catch(:teek_return) do
|
|
97
|
+
callable.call(*args)
|
|
98
|
+
caught = :_none
|
|
99
|
+
end
|
|
100
|
+
caught ||= :return
|
|
101
|
+
end
|
|
102
|
+
caught ||= :continue
|
|
103
|
+
end
|
|
104
|
+
caught ||= :break
|
|
105
|
+
caught == :_none ? nil : caught
|
|
106
|
+
}
|
|
107
|
+
@interp.register_callback(wrapped)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Remove a previously registered callback by its ID.
|
|
111
|
+
# @param id [Integer] callback ID returned by {#register_callback}
|
|
112
|
+
# @return [void]
|
|
113
|
+
def unregister_callback(id)
|
|
114
|
+
@interp.unregister_callback(id)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Schedule a one-shot timer. Calls the block after +ms+ milliseconds.
|
|
118
|
+
# @param ms [Integer] delay in milliseconds
|
|
119
|
+
# @yield block to call when the timer fires
|
|
120
|
+
# @return [String] timer ID, pass to {#after_cancel} to cancel
|
|
121
|
+
def after(ms, &block)
|
|
122
|
+
cb_id = nil
|
|
123
|
+
cb_id = @interp.register_callback(proc { |*|
|
|
124
|
+
block.call
|
|
125
|
+
@interp.unregister_callback(cb_id)
|
|
126
|
+
})
|
|
127
|
+
after_id = @interp.tcl_eval("after #{ms.to_i} {ruby_callback #{cb_id}}")
|
|
128
|
+
after_id.instance_variable_set(:@cb_id, cb_id)
|
|
129
|
+
after_id
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Schedule a block to run once when the event loop is idle.
|
|
133
|
+
# @yield block to call when the event loop is idle
|
|
134
|
+
# @return [String] timer ID, pass to {#after_cancel} to cancel
|
|
135
|
+
def after_idle(&block)
|
|
136
|
+
cb_id = nil
|
|
137
|
+
cb_id = @interp.register_callback(proc { |*|
|
|
138
|
+
block.call
|
|
139
|
+
@interp.unregister_callback(cb_id)
|
|
140
|
+
})
|
|
141
|
+
after_id = @interp.tcl_eval("after idle {ruby_callback #{cb_id}}")
|
|
142
|
+
after_id.instance_variable_set(:@cb_id, cb_id)
|
|
143
|
+
after_id
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Cancel a pending {#after} or {#after_idle} timer.
|
|
147
|
+
# @param after_id [String] timer ID returned by {#after} or {#after_idle}
|
|
148
|
+
# @return [void]
|
|
149
|
+
def after_cancel(after_id)
|
|
150
|
+
@interp.tcl_eval("after cancel #{after_id}")
|
|
151
|
+
if (cb_id = after_id.instance_variable_get(:@cb_id))
|
|
152
|
+
@interp.unregister_callback(cb_id)
|
|
153
|
+
after_id.instance_variable_set(:@cb_id, nil)
|
|
154
|
+
end
|
|
155
|
+
after_id
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Split a Tcl list string into a Ruby array of strings.
|
|
159
|
+
# @param str [String] a Tcl-formatted list
|
|
160
|
+
# @return [Array<String>]
|
|
161
|
+
def split_list(str)
|
|
162
|
+
Teek.split_list(str)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Build a properly-escaped Tcl list from Ruby strings.
|
|
166
|
+
# @param args [Array<String>] elements to join
|
|
167
|
+
# @return [String] a Tcl-formatted list
|
|
168
|
+
def make_list(*args)
|
|
169
|
+
Teek.make_list(*args)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Convert a Tcl boolean string ("0", "1", "yes", "no", etc.) to Ruby boolean.
|
|
173
|
+
# @param str [String] a Tcl boolean value
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
def tcl_to_bool(str)
|
|
176
|
+
Teek.tcl_to_bool(str)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Convert a Ruby boolean to a Tcl boolean string ("1" or "0").
|
|
180
|
+
# @param val [Boolean]
|
|
181
|
+
# @return [String] "1" or "0"
|
|
182
|
+
def bool_to_tcl(val)
|
|
183
|
+
Teek.bool_to_tcl(val)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Build and evaluate a Tcl command from Ruby values.
|
|
187
|
+
# Positional args are converted: Symbols pass bare, Procs become
|
|
188
|
+
# callbacks, everything else is brace-quoted. Keyword args become
|
|
189
|
+
# +-key value+ option pairs.
|
|
190
|
+
# @example
|
|
191
|
+
# app.command(:pack, '.btn', side: :left, padx: 10)
|
|
192
|
+
# # evaluates: pack .btn -side left -padx {10}
|
|
193
|
+
# @param cmd [Symbol, String] the Tcl command name
|
|
194
|
+
# @param args positional arguments
|
|
195
|
+
# @param kwargs keyword arguments mapped to +-key value+ pairs
|
|
196
|
+
# @return [String] the Tcl result
|
|
197
|
+
def command(cmd, *args, **kwargs)
|
|
198
|
+
parts = [cmd.to_s]
|
|
199
|
+
args.each do |arg|
|
|
200
|
+
parts << tcl_value(arg)
|
|
201
|
+
end
|
|
202
|
+
kwargs.each do |key, value|
|
|
203
|
+
parts << "-#{key}"
|
|
204
|
+
parts << tcl_value(value)
|
|
205
|
+
end
|
|
206
|
+
@interp.tcl_eval(parts.join(' '))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Add a directory to Tcl's package search path.
|
|
210
|
+
# @param path [String] directory containing Tcl packages
|
|
211
|
+
# @return [void]
|
|
212
|
+
def add_package_path(path)
|
|
213
|
+
tcl_eval("lappend ::auto_path {#{path}}")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Load a Tcl package into this interpreter.
|
|
217
|
+
# @param name [String] package name (e.g. "BWidget")
|
|
218
|
+
# @param version [String, nil] minimum version constraint
|
|
219
|
+
# @return [String] the version that was loaded
|
|
220
|
+
# @raise [Teek::TclError] if the package is not found
|
|
221
|
+
def require_package(name, version = nil)
|
|
222
|
+
cmd = version ? "package require #{name} #{version}" : "package require #{name}"
|
|
223
|
+
tcl_eval(cmd)
|
|
224
|
+
rescue Teek::TclError => e
|
|
225
|
+
raise Teek::TclError, "Package '#{name}' not found. Ensure it is installed and on Tcl's auto_path. (#{e.message})"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# List all packages known to this interpreter.
|
|
229
|
+
# Scans +auto_path+ for package indexes before querying.
|
|
230
|
+
# @return [Array<String>]
|
|
231
|
+
def package_names
|
|
232
|
+
scan_packages
|
|
233
|
+
split_list(tcl_eval('package names'))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Check if a package is already loaded in this interpreter.
|
|
237
|
+
# @param name [String] package name
|
|
238
|
+
# @return [Boolean]
|
|
239
|
+
def package_present?(name)
|
|
240
|
+
tcl_eval("package present #{name}")
|
|
241
|
+
true
|
|
242
|
+
rescue Teek::TclError
|
|
243
|
+
false
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# List available versions of a package.
|
|
247
|
+
# Scans +auto_path+ for package indexes before querying.
|
|
248
|
+
# @param name [String] package name
|
|
249
|
+
# @return [Array<String>]
|
|
250
|
+
def package_versions(name)
|
|
251
|
+
scan_packages
|
|
252
|
+
split_list(tcl_eval("package versions #{name}"))
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Set a Tcl variable. Useful for widget +textvariable+ and +variable+ options.
|
|
256
|
+
# @param name [String] variable name
|
|
257
|
+
# @param value [String] value to set
|
|
258
|
+
# @return [String] the value
|
|
259
|
+
def set_variable(name, value)
|
|
260
|
+
tcl_eval("set #{name} {#{value}}")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Get a Tcl variable's value.
|
|
264
|
+
# @param name [String] variable name
|
|
265
|
+
# @return [String] the value
|
|
266
|
+
# @raise [Teek::TclError] if the variable doesn't exist
|
|
267
|
+
def get_variable(name)
|
|
268
|
+
tcl_eval("set #{name}")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Destroy a widget and all its children.
|
|
272
|
+
# @param widget [String] Tk widget path (e.g. ".frame1")
|
|
273
|
+
# @return [void]
|
|
274
|
+
def destroy(widget)
|
|
275
|
+
tcl_eval("destroy #{widget}")
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Show a busy cursor on a window while executing a block.
|
|
279
|
+
# The cursor is restored even if the block raises.
|
|
280
|
+
# @param window [String] Tk window path
|
|
281
|
+
# @yield the work to perform while busy
|
|
282
|
+
# @return the block's return value
|
|
283
|
+
def busy(window: '.')
|
|
284
|
+
tcl_eval("tk busy hold #{window}")
|
|
285
|
+
tcl_eval('update idletasks')
|
|
286
|
+
yield
|
|
287
|
+
ensure
|
|
288
|
+
tcl_eval("tk busy forget #{window}")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Enter the Tk event loop. Blocks until the application exits.
|
|
292
|
+
# @return [void]
|
|
293
|
+
def mainloop
|
|
294
|
+
if defined?(IRB) || defined?(Pry) || $0 == 'irb' || $0 == 'pry'
|
|
295
|
+
warn "Teek: mainloop blocks the current thread and will make your REPL unresponsive.\n" \
|
|
296
|
+
" Instead, use app.update in a loop or call app.update manually between commands:\n" \
|
|
297
|
+
" app.show\n" \
|
|
298
|
+
" app.update # process pending events\n" \
|
|
299
|
+
" # ... interact with your app ...\n" \
|
|
300
|
+
" app.update # process again after changes"
|
|
301
|
+
end
|
|
302
|
+
@interp.mainloop
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Process all pending events and idle callbacks, then return.
|
|
306
|
+
# @return [void]
|
|
307
|
+
def update
|
|
308
|
+
@interp.tcl_eval('update')
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Process only pending idle callbacks (e.g. geometry redraws), then return.
|
|
312
|
+
# @return [void]
|
|
313
|
+
def update_idletasks
|
|
314
|
+
@interp.tcl_eval('update idletasks')
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Show a window. Defaults to the root window (".").
|
|
318
|
+
# @param window [String] Tk window path
|
|
319
|
+
# @return [void]
|
|
320
|
+
def show(window = '.')
|
|
321
|
+
@interp.tcl_eval("wm deiconify #{window}")
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Hide a window without destroying it. Defaults to the root window (".").
|
|
325
|
+
# @param window [String] Tk window path
|
|
326
|
+
# @return [void]
|
|
327
|
+
def hide(window = '.')
|
|
328
|
+
@interp.tcl_eval("wm withdraw #{window}")
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Set a window's title.
|
|
332
|
+
# @param title [String] new title
|
|
333
|
+
# @param window [String] Tk window path
|
|
334
|
+
# @return [String] the title
|
|
335
|
+
def set_window_title(title, window: '.')
|
|
336
|
+
tcl_eval("wm title #{window} {#{title}}")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Get a window's current title.
|
|
340
|
+
# @param window [String] Tk window path
|
|
341
|
+
# @return [String] current title
|
|
342
|
+
def window_title(window: '.')
|
|
343
|
+
tcl_eval("wm title #{window}")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Set a window's geometry (e.g. "400x300", "400x300+100+50").
|
|
347
|
+
# @param geometry [String] geometry string
|
|
348
|
+
# @param window [String] Tk window path
|
|
349
|
+
# @return [String] the geometry
|
|
350
|
+
def set_window_geometry(geometry, window: '.')
|
|
351
|
+
tcl_eval("wm geometry #{window} #{geometry}")
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Get a window's current geometry.
|
|
355
|
+
# @param window [String] Tk window path
|
|
356
|
+
# @return [String] geometry string (e.g. "400x300+0+0")
|
|
357
|
+
def window_geometry(window: '.')
|
|
358
|
+
tcl_eval("wm geometry #{window}")
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Set whether a window is resizable.
|
|
362
|
+
# @param width [Boolean] allow horizontal resize
|
|
363
|
+
# @param height [Boolean] allow vertical resize
|
|
364
|
+
# @param window [String] Tk window path
|
|
365
|
+
# @return [void]
|
|
366
|
+
def set_window_resizable(width, height, window: '.')
|
|
367
|
+
tcl_eval("wm resizable #{window} #{width ? 1 : 0} #{height ? 1 : 0}")
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Get whether a window is resizable.
|
|
371
|
+
# @param window [String] Tk window path
|
|
372
|
+
# @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
|
|
373
|
+
def window_resizable(window: '.')
|
|
374
|
+
parts = tcl_eval("wm resizable #{window}").split
|
|
375
|
+
[parts[0] == '1', parts[1] == '1']
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Bind a Tk event on a widget, with optional substitutions forwarded
|
|
379
|
+
# as block arguments. Substitutions can be symbols (mapped via
|
|
380
|
+
# {BIND_SUBS}) or raw Tcl +%+ codes passed through as-is.
|
|
381
|
+
#
|
|
382
|
+
# @example Mouse click with window coordinates
|
|
383
|
+
# app.bind('.c', 'Button-1', :x, :y) { |x, y| puts "#{x},#{y}" }
|
|
384
|
+
# @example Key press
|
|
385
|
+
# app.bind('.', 'KeyPress', :keysym) { |k| puts k }
|
|
386
|
+
# @example No substitutions
|
|
387
|
+
# app.bind('.btn', 'Enter') { highlight }
|
|
388
|
+
# @example Raw Tcl expression (for codes not in BIND_SUBS)
|
|
389
|
+
# app.bind('.c', 'Button-1', '%T') { |type| ... }
|
|
390
|
+
# @example Canvas coordinate conversion
|
|
391
|
+
# app.bind(canvas, 'Button-1', :x, :y) do |x, y|
|
|
392
|
+
# cx = app.command(canvas, :canvasx, x).to_f
|
|
393
|
+
# cy = app.command(canvas, :canvasy, y).to_f
|
|
394
|
+
# end
|
|
395
|
+
#
|
|
396
|
+
# @note Each substitution crosses from Tcl to Ruby once. Any {#command}
|
|
397
|
+
# calls inside the block are additional round-trips. This is negligible
|
|
398
|
+
# for click/key events but could matter for hot-path handlers like
|
|
399
|
+
# +<Motion>+ that fire hundreds of times per second. For those, consider
|
|
400
|
+
# {#tcl_eval} with inline Tcl expressions to do all work in one evaluation.
|
|
401
|
+
#
|
|
402
|
+
# @param widget [String] Tk widget path or class tag (e.g. ".btn", "Entry")
|
|
403
|
+
# @param event [String] Tk event name, with or without angle brackets
|
|
404
|
+
# @param subs [Array<Symbol, String>] substitution codes (see {BIND_SUBS})
|
|
405
|
+
# @yield [*values] called when the event fires, with substitution values
|
|
406
|
+
# @return [void]
|
|
407
|
+
# @see #unbind
|
|
408
|
+
#
|
|
409
|
+
BIND_SUBS = {
|
|
410
|
+
x: '%x', y: '%y', # window coordinates
|
|
411
|
+
root_x: '%X', root_y: '%Y', # screen coordinates
|
|
412
|
+
widget: '%W', # widget path
|
|
413
|
+
keysym: '%K', keycode: '%k', # key events
|
|
414
|
+
char: '%A', # character (key events)
|
|
415
|
+
width: '%w', height: '%h', # Configure events
|
|
416
|
+
button: '%b', # mouse button number
|
|
417
|
+
mouse_wheel: '%D', # mousewheel delta
|
|
418
|
+
type: '%T', # event type
|
|
419
|
+
}.freeze
|
|
420
|
+
|
|
421
|
+
def bind(widget, event, *subs, &block)
|
|
422
|
+
event_str = event.start_with?('<') ? event : "<#{event}>"
|
|
423
|
+
cb = register_callback(proc { |*args| block.call(*args) })
|
|
424
|
+
tcl_subs = subs.map { |s| s.is_a?(Symbol) ? BIND_SUBS.fetch(s) : s.to_s }
|
|
425
|
+
sub_str = tcl_subs.empty? ? '' : ' ' + tcl_subs.join(' ')
|
|
426
|
+
@interp.tcl_eval("bind #{widget} #{event_str} {ruby_callback #{cb}#{sub_str}}")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Remove an event binding previously set with {#bind}.
|
|
430
|
+
# @param widget [String] Tk widget path or class tag
|
|
431
|
+
# @param event [String] Tk event name, with or without angle brackets
|
|
432
|
+
# @return [void]
|
|
433
|
+
# @see #bind
|
|
434
|
+
def unbind(widget, event)
|
|
435
|
+
event_str = event.start_with?('<') ? event : "<#{event}>"
|
|
436
|
+
@interp.tcl_eval("bind #{widget} #{event_str} {}")
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Get the macOS window appearance. No-op (returns +nil+) on non-macOS.
|
|
440
|
+
# @example
|
|
441
|
+
# app.appearance # => "aqua", "darkaqua", or "auto"
|
|
442
|
+
# app.appearance = :light # force light mode
|
|
443
|
+
# app.appearance = :dark # force dark mode
|
|
444
|
+
# app.appearance = :auto # follow system setting
|
|
445
|
+
# @return [String, nil] "aqua", "darkaqua", "auto", or nil on non-macOS
|
|
446
|
+
# @see #dark?
|
|
447
|
+
def appearance
|
|
448
|
+
return nil unless aqua?
|
|
449
|
+
if tk_major >= 9
|
|
450
|
+
@interp.tcl_eval('wm attributes . -appearance').delete('"')
|
|
451
|
+
else
|
|
452
|
+
@interp.tcl_eval('tk::unsupported::MacWindowStyle appearance .')
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Set the macOS window appearance. No-op on non-macOS.
|
|
457
|
+
# @param mode [Symbol, String] +:light+, +:dark+, +:auto+, or a raw Tk value
|
|
458
|
+
# @return [void]
|
|
459
|
+
def appearance=(mode)
|
|
460
|
+
return unless aqua?
|
|
461
|
+
value = case mode.to_sym
|
|
462
|
+
when :light then 'aqua'
|
|
463
|
+
when :dark then 'darkaqua'
|
|
464
|
+
when :auto then 'auto'
|
|
465
|
+
else mode.to_s
|
|
466
|
+
end
|
|
467
|
+
if tk_major >= 9
|
|
468
|
+
@interp.tcl_eval("wm attributes . -appearance #{value}")
|
|
469
|
+
else
|
|
470
|
+
@interp.tcl_eval("tk::unsupported::MacWindowStyle appearance . #{value}")
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Returns true if the window is currently displayed in dark mode.
|
|
475
|
+
# Always returns false on non-macOS.
|
|
476
|
+
# @return [Boolean]
|
|
477
|
+
def dark?
|
|
478
|
+
return false unless aqua?
|
|
479
|
+
@interp.tcl_eval('tk::unsupported::MacWindowStyle isdark .').delete('"') == '1'
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
private
|
|
483
|
+
|
|
484
|
+
# Force Tcl to scan auto_path for pkgIndex.tcl files so that
|
|
485
|
+
# package_names and package_versions reflect all discoverable packages.
|
|
486
|
+
def scan_packages
|
|
487
|
+
tcl_eval('catch {package require __teek_scan__}')
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def aqua?
|
|
491
|
+
@aqua ||= @interp.tcl_eval('tk windowingsystem') == 'aqua'
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def tk_major
|
|
495
|
+
@tk_major ||= @interp.tcl_eval('info patchlevel').split('.').first.to_i
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def setup_widget_tracking
|
|
499
|
+
@create_cb_id = @interp.register_callback(proc { |path, cls|
|
|
500
|
+
next if path.start_with?('.teek_debug')
|
|
501
|
+
@widgets[path] = { class: cls, parent: File.dirname(path).gsub(/\A$/, '.') }
|
|
502
|
+
@debugger&.on_widget_created(path, cls)
|
|
503
|
+
})
|
|
504
|
+
@destroy_cb_id = @interp.register_callback(proc { |path|
|
|
505
|
+
next if path.start_with?('.teek_debug')
|
|
506
|
+
@widgets.delete(path)
|
|
507
|
+
@debugger&.on_widget_destroyed(path)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
# Tcl proc called on widget creation (trace leave)
|
|
511
|
+
@interp.tcl_eval("proc ::teek_track_create {cmd_string code result op} {
|
|
512
|
+
set path [lindex $cmd_string 1]
|
|
513
|
+
if {$code == 0 && [winfo exists $path]} {
|
|
514
|
+
set cls [winfo class $path]
|
|
515
|
+
ruby_callback #{@create_cb_id} $path $cls
|
|
516
|
+
}
|
|
517
|
+
}")
|
|
518
|
+
|
|
519
|
+
# Tcl proc called on widget destruction (bind)
|
|
520
|
+
@interp.tcl_eval("bind all <Destroy> {ruby_callback #{@destroy_cb_id} %W}")
|
|
521
|
+
|
|
522
|
+
# Add trace on each widget command
|
|
523
|
+
Teek::WIDGET_COMMANDS.each do |cmd|
|
|
524
|
+
@interp.tcl_eval("catch {trace add execution #{cmd} leave ::teek_track_create}")
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def tcl_value(value)
|
|
529
|
+
case value
|
|
530
|
+
when Proc
|
|
531
|
+
id = @interp.register_callback(value)
|
|
532
|
+
"{ruby_callback #{id}}"
|
|
533
|
+
when Symbol
|
|
534
|
+
value.to_s
|
|
535
|
+
else
|
|
536
|
+
"{#{value}}"
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|