teek 0.1.0 → 0.1.2
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 +99 -15
- data/Rakefile +201 -2
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +3 -110
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkeventsource.c +195 -0
- data/ext/teek/tkphoto.c +169 -5
- data/ext/teek/tkwin.c +84 -0
- data/lib/teek/background_ractor4x.rb +35 -6
- data/lib/teek/debugger.rb +37 -32
- data/lib/teek/method_coverage_service.rb +265 -0
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/ractor_support.rb +1 -1
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +104 -0
- data/lib/teek.rb +144 -1
- data/sample/calculator.rb +16 -21
- data/sample/debug_demo.rb +20 -22
- data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
- data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
- data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
- data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
- data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
- data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
- data/sample/optcarrot/vendor/optcarrot.rb +14 -0
- data/sample/optcarrot.rb +354 -0
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +255 -0
- data/sample/paint/layer_manager.rb +179 -0
- data/sample/paint/paint_demo.rb +837 -0
- data/sample/paint/sparse_pixel_buffer.rb +202 -0
- data/sample/sdl2_demo.rb +318 -0
- data/sample/threading_demo.rb +127 -132
- metadata +31 -1
data/lib/teek.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'tcltklib'
|
|
4
4
|
require_relative 'teek/version'
|
|
5
5
|
require_relative 'teek/ractor_support'
|
|
6
|
+
require_relative 'teek/widget'
|
|
7
|
+
require_relative 'teek/photo'
|
|
6
8
|
|
|
7
9
|
# Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
|
|
8
10
|
# with Ruby callbacks, event bindings, and background work support.
|
|
@@ -52,6 +54,7 @@ module Teek
|
|
|
52
54
|
@interp.tcl_eval('package require Tk')
|
|
53
55
|
hide
|
|
54
56
|
@widgets = {}
|
|
57
|
+
@widget_counters = Hash.new(0)
|
|
55
58
|
debug ||= !!ENV['TEEK_DEBUG']
|
|
56
59
|
track_widgets = true if debug
|
|
57
60
|
setup_widget_tracking if track_widgets
|
|
@@ -118,6 +121,7 @@ module Teek
|
|
|
118
121
|
# @param ms [Integer] delay in milliseconds
|
|
119
122
|
# @yield block to call when the timer fires
|
|
120
123
|
# @return [String] timer ID, pass to {#after_cancel} to cancel
|
|
124
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M5 after ms
|
|
121
125
|
def after(ms, &block)
|
|
122
126
|
cb_id = nil
|
|
123
127
|
cb_id = @interp.register_callback(proc { |*|
|
|
@@ -132,6 +136,7 @@ module Teek
|
|
|
132
136
|
# Schedule a block to run once when the event loop is idle.
|
|
133
137
|
# @yield block to call when the event loop is idle
|
|
134
138
|
# @return [String] timer ID, pass to {#after_cancel} to cancel
|
|
139
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M9 after idle
|
|
135
140
|
def after_idle(&block)
|
|
136
141
|
cb_id = nil
|
|
137
142
|
cb_id = @interp.register_callback(proc { |*|
|
|
@@ -146,6 +151,7 @@ module Teek
|
|
|
146
151
|
# Cancel a pending {#after} or {#after_idle} timer.
|
|
147
152
|
# @param after_id [String] timer ID returned by {#after} or {#after_idle}
|
|
148
153
|
# @return [void]
|
|
154
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M7 after cancel
|
|
149
155
|
def after_cancel(after_id)
|
|
150
156
|
@interp.tcl_eval("after cancel #{after_id}")
|
|
151
157
|
if (cb_id = after_id.instance_variable_get(:@cb_id))
|
|
@@ -206,6 +212,36 @@ module Teek
|
|
|
206
212
|
@interp.tcl_eval(parts.join(' '))
|
|
207
213
|
end
|
|
208
214
|
|
|
215
|
+
# Create a Tk widget and return a {Widget} wrapper.
|
|
216
|
+
#
|
|
217
|
+
# Auto-generates a unique path if none is given. The path is derived from
|
|
218
|
+
# the widget type and a monotonic counter.
|
|
219
|
+
#
|
|
220
|
+
# @param type [String, Symbol] Tk widget command (e.g. 'ttk::button', :canvas)
|
|
221
|
+
# @param path [String, nil] explicit Tk path, or nil for auto-naming
|
|
222
|
+
# @param parent [Widget, String, nil] parent widget for path nesting
|
|
223
|
+
# @param kwargs keyword arguments passed to the Tk widget command
|
|
224
|
+
# @return [Widget] the created widget
|
|
225
|
+
#
|
|
226
|
+
# @example Auto-named
|
|
227
|
+
# btn = app.create_widget('ttk::button', text: 'Click')
|
|
228
|
+
# # btn.path => ".ttkbtn1"
|
|
229
|
+
#
|
|
230
|
+
# @example Explicit path
|
|
231
|
+
# frm = app.create_widget('ttk::frame', '.myframe')
|
|
232
|
+
#
|
|
233
|
+
# @example Nested under a parent
|
|
234
|
+
# frm = app.create_widget('ttk::frame')
|
|
235
|
+
# btn = app.create_widget('ttk::button', parent: frm, text: 'Click')
|
|
236
|
+
# # btn.path => ".ttkfrm1.ttkbtn1"
|
|
237
|
+
#
|
|
238
|
+
def create_widget(type, path = nil, parent: nil, **kwargs)
|
|
239
|
+
type_s = type.to_s
|
|
240
|
+
path ||= next_widget_path(type_s, parent)
|
|
241
|
+
command(type_s, path, **kwargs)
|
|
242
|
+
Widget.new(self, path)
|
|
243
|
+
end
|
|
244
|
+
|
|
209
245
|
# Add a directory to Tcl's package search path.
|
|
210
246
|
# @param path [String] directory containing Tcl packages
|
|
211
247
|
# @return [void]
|
|
@@ -218,6 +254,7 @@ module Teek
|
|
|
218
254
|
# @param version [String, nil] minimum version constraint
|
|
219
255
|
# @return [String] the version that was loaded
|
|
220
256
|
# @raise [Teek::TclError] if the package is not found
|
|
257
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M10 package require
|
|
221
258
|
def require_package(name, version = nil)
|
|
222
259
|
cmd = version ? "package require #{name} #{version}" : "package require #{name}"
|
|
223
260
|
tcl_eval(cmd)
|
|
@@ -228,6 +265,7 @@ module Teek
|
|
|
228
265
|
# List all packages known to this interpreter.
|
|
229
266
|
# Scans +auto_path+ for package indexes before querying.
|
|
230
267
|
# @return [Array<String>]
|
|
268
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M7 package names
|
|
231
269
|
def package_names
|
|
232
270
|
scan_packages
|
|
233
271
|
split_list(tcl_eval('package names'))
|
|
@@ -236,6 +274,7 @@ module Teek
|
|
|
236
274
|
# Check if a package is already loaded in this interpreter.
|
|
237
275
|
# @param name [String] package name
|
|
238
276
|
# @return [Boolean]
|
|
277
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M8 package present
|
|
239
278
|
def package_present?(name)
|
|
240
279
|
tcl_eval("package present #{name}")
|
|
241
280
|
true
|
|
@@ -247,6 +286,7 @@ module Teek
|
|
|
247
286
|
# Scans +auto_path+ for package indexes before querying.
|
|
248
287
|
# @param name [String] package name
|
|
249
288
|
# @return [Array<String>]
|
|
289
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/package.htm#M14 package versions
|
|
250
290
|
def package_versions(name)
|
|
251
291
|
scan_packages
|
|
252
292
|
split_list(tcl_eval("package versions #{name}"))
|
|
@@ -256,6 +296,7 @@ module Teek
|
|
|
256
296
|
# @param name [String] variable name
|
|
257
297
|
# @param value [String] value to set
|
|
258
298
|
# @return [String] the value
|
|
299
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/set.htm set
|
|
259
300
|
def set_variable(name, value)
|
|
260
301
|
tcl_eval("set #{name} {#{value}}")
|
|
261
302
|
end
|
|
@@ -264,6 +305,7 @@ module Teek
|
|
|
264
305
|
# @param name [String] variable name
|
|
265
306
|
# @return [String] the value
|
|
266
307
|
# @raise [Teek::TclError] if the variable doesn't exist
|
|
308
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/set.htm set
|
|
267
309
|
def get_variable(name)
|
|
268
310
|
tcl_eval("set #{name}")
|
|
269
311
|
end
|
|
@@ -271,15 +313,55 @@ module Teek
|
|
|
271
313
|
# Destroy a widget and all its children.
|
|
272
314
|
# @param widget [String] Tk widget path (e.g. ".frame1")
|
|
273
315
|
# @return [void]
|
|
274
|
-
|
|
316
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/destroy.htm destroy
|
|
317
|
+
def destroy(widget = '.')
|
|
318
|
+
raise ArgumentError, 'widget path cannot be nil' if widget.nil?
|
|
275
319
|
tcl_eval("destroy #{widget}")
|
|
276
320
|
end
|
|
277
321
|
|
|
322
|
+
# Measure the pixel width of a text string in a given font.
|
|
323
|
+
# Uses Tk's C font API directly — faster than the Tcl +font measure+ command.
|
|
324
|
+
# @param font [String] font description (e.g. "Helvetica 12", "TkDefaultFont")
|
|
325
|
+
# @param text [String] text to measure
|
|
326
|
+
# @return [Integer] pixel width
|
|
327
|
+
# @raise [Teek::TclError] if the font is not found
|
|
328
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkLib/MeasureChar.htm Tk_TextWidth
|
|
329
|
+
def text_width(font, text)
|
|
330
|
+
@interp.text_width(font, text)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Get font metrics (ascent, descent, linespace) for a given font.
|
|
334
|
+
# Uses Tk's C font API directly.
|
|
335
|
+
# @param font [String] font description (e.g. "Helvetica 12", "TkDefaultFont")
|
|
336
|
+
# @return [Hash{Symbol => Integer}] +:ascent+, +:descent+, +:linespace+
|
|
337
|
+
# @raise [Teek::TclError] if the font is not found
|
|
338
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkLib/FontId.htm Tk_GetFontMetrics
|
|
339
|
+
def font_metrics(font)
|
|
340
|
+
@interp.font_metrics(font)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Measure how many bytes of text fit within a pixel width limit.
|
|
344
|
+
# Useful for text truncation, ellipsis, and line wrapping.
|
|
345
|
+
# @param font [String] font description (e.g. "Helvetica 12")
|
|
346
|
+
# @param text [String] text to measure
|
|
347
|
+
# @param max_pixels [Integer] maximum pixel width (-1 for unlimited)
|
|
348
|
+
# @param opts [Hash] options
|
|
349
|
+
# @option opts [Boolean] :partial_ok allow partial character at boundary
|
|
350
|
+
# @option opts [Boolean] :whole_words break only at word boundaries
|
|
351
|
+
# @option opts [Boolean] :at_least_one always return at least one character
|
|
352
|
+
# @return [Hash{Symbol => Integer}] +:bytes+ and +:width+
|
|
353
|
+
# @raise [Teek::TclError] if the font is not found
|
|
354
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkLib/MeasureChar.htm Tk_MeasureChars
|
|
355
|
+
def measure_chars(font, text, max_pixels, **opts)
|
|
356
|
+
@interp.measure_chars(font, text, max_pixels, opts)
|
|
357
|
+
end
|
|
358
|
+
|
|
278
359
|
# Show a busy cursor on a window while executing a block.
|
|
279
360
|
# The cursor is restored even if the block raises.
|
|
280
361
|
# @param window [String] Tk window path
|
|
281
362
|
# @yield the work to perform while busy
|
|
282
363
|
# @return the block's return value
|
|
364
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/busy.htm tk busy
|
|
283
365
|
def busy(window: '.')
|
|
284
366
|
tcl_eval("tk busy hold #{window}")
|
|
285
367
|
tcl_eval('update idletasks')
|
|
@@ -290,6 +372,7 @@ module Teek
|
|
|
290
372
|
|
|
291
373
|
# Enter the Tk event loop. Blocks until the application exits.
|
|
292
374
|
# @return [void]
|
|
375
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkLib/MainLoop.htm Tk_MainLoop
|
|
293
376
|
def mainloop
|
|
294
377
|
if defined?(IRB) || defined?(Pry) || $0 == 'irb' || $0 == 'pry'
|
|
295
378
|
warn "Teek: mainloop blocks the current thread and will make your REPL unresponsive.\n" \
|
|
@@ -304,12 +387,14 @@ module Teek
|
|
|
304
387
|
|
|
305
388
|
# Process all pending events and idle callbacks, then return.
|
|
306
389
|
# @return [void]
|
|
390
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/update.htm update
|
|
307
391
|
def update
|
|
308
392
|
@interp.tcl_eval('update')
|
|
309
393
|
end
|
|
310
394
|
|
|
311
395
|
# Process only pending idle callbacks (e.g. geometry redraws), then return.
|
|
312
396
|
# @return [void]
|
|
397
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/update.htm update idletasks
|
|
313
398
|
def update_idletasks
|
|
314
399
|
@interp.tcl_eval('update idletasks')
|
|
315
400
|
end
|
|
@@ -317,6 +402,7 @@ module Teek
|
|
|
317
402
|
# Show a window. Defaults to the root window (".").
|
|
318
403
|
# @param window [String] Tk window path
|
|
319
404
|
# @return [void]
|
|
405
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M38 wm deiconify
|
|
320
406
|
def show(window = '.')
|
|
321
407
|
@interp.tcl_eval("wm deiconify #{window}")
|
|
322
408
|
end
|
|
@@ -324,6 +410,7 @@ module Teek
|
|
|
324
410
|
# Hide a window without destroying it. Defaults to the root window (".").
|
|
325
411
|
# @param window [String] Tk window path
|
|
326
412
|
# @return [void]
|
|
413
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M65 wm withdraw
|
|
327
414
|
def hide(window = '.')
|
|
328
415
|
@interp.tcl_eval("wm withdraw #{window}")
|
|
329
416
|
end
|
|
@@ -332,6 +419,7 @@ module Teek
|
|
|
332
419
|
# @param title [String] new title
|
|
333
420
|
# @param window [String] Tk window path
|
|
334
421
|
# @return [String] the title
|
|
422
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
|
|
335
423
|
def set_window_title(title, window: '.')
|
|
336
424
|
tcl_eval("wm title #{window} {#{title}}")
|
|
337
425
|
end
|
|
@@ -339,6 +427,7 @@ module Teek
|
|
|
339
427
|
# Get a window's current title.
|
|
340
428
|
# @param window [String] Tk window path
|
|
341
429
|
# @return [String] current title
|
|
430
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
|
|
342
431
|
def window_title(window: '.')
|
|
343
432
|
tcl_eval("wm title #{window}")
|
|
344
433
|
end
|
|
@@ -347,6 +436,7 @@ module Teek
|
|
|
347
436
|
# @param geometry [String] geometry string
|
|
348
437
|
# @param window [String] Tk window path
|
|
349
438
|
# @return [String] the geometry
|
|
439
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
|
|
350
440
|
def set_window_geometry(geometry, window: '.')
|
|
351
441
|
tcl_eval("wm geometry #{window} #{geometry}")
|
|
352
442
|
end
|
|
@@ -354,6 +444,7 @@ module Teek
|
|
|
354
444
|
# Get a window's current geometry.
|
|
355
445
|
# @param window [String] Tk window path
|
|
356
446
|
# @return [String] geometry string (e.g. "400x300+0+0")
|
|
447
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
|
|
357
448
|
def window_geometry(window: '.')
|
|
358
449
|
tcl_eval("wm geometry #{window}")
|
|
359
450
|
end
|
|
@@ -363,6 +454,7 @@ module Teek
|
|
|
363
454
|
# @param height [Boolean] allow vertical resize
|
|
364
455
|
# @param window [String] Tk window path
|
|
365
456
|
# @return [void]
|
|
457
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
|
|
366
458
|
def set_window_resizable(width, height, window: '.')
|
|
367
459
|
tcl_eval("wm resizable #{window} #{width ? 1 : 0} #{height ? 1 : 0}")
|
|
368
460
|
end
|
|
@@ -370,6 +462,7 @@ module Teek
|
|
|
370
462
|
# Get whether a window is resizable.
|
|
371
463
|
# @param window [String] Tk window path
|
|
372
464
|
# @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
|
|
465
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
|
|
373
466
|
def window_resizable(window: '.')
|
|
374
467
|
parts = tcl_eval("wm resizable #{window}").split
|
|
375
468
|
[parts[0] == '1', parts[1] == '1']
|
|
@@ -405,6 +498,7 @@ module Teek
|
|
|
405
498
|
# @yield [*values] called when the event fires, with substitution values
|
|
406
499
|
# @return [void]
|
|
407
500
|
# @see #unbind
|
|
501
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/bind.htm bind
|
|
408
502
|
#
|
|
409
503
|
BIND_SUBS = {
|
|
410
504
|
x: '%x', y: '%y', # window coordinates
|
|
@@ -431,6 +525,7 @@ module Teek
|
|
|
431
525
|
# @param event [String] Tk event name, with or without angle brackets
|
|
432
526
|
# @return [void]
|
|
433
527
|
# @see #bind
|
|
528
|
+
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/bind.htm bind
|
|
434
529
|
def unbind(widget, event)
|
|
435
530
|
event_str = event.start_with?('<') ? event : "<#{event}>"
|
|
436
531
|
@interp.tcl_eval("bind #{widget} #{event_str} {}")
|
|
@@ -481,6 +576,54 @@ module Teek
|
|
|
481
576
|
|
|
482
577
|
private
|
|
483
578
|
|
|
579
|
+
# Short prefixes for common Tk widget types.
|
|
580
|
+
# The base name (after the last ::) is looked up here; the namespace
|
|
581
|
+
# prefix (e.g. "ttk") is prepended verbatim. Unmapped types fall
|
|
582
|
+
# back to the full lowercased name with colons stripped.
|
|
583
|
+
WIDGET_PREFIXES = {
|
|
584
|
+
'button' => 'btn',
|
|
585
|
+
'label' => 'lbl',
|
|
586
|
+
'entry' => 'ent',
|
|
587
|
+
'frame' => 'frm',
|
|
588
|
+
'text' => 'txt',
|
|
589
|
+
'canvas' => 'cvs',
|
|
590
|
+
'scrollbar' => 'sb',
|
|
591
|
+
'scale' => 'scl',
|
|
592
|
+
'checkbutton' => 'chk',
|
|
593
|
+
'radiobutton' => 'rad',
|
|
594
|
+
'combobox' => 'cbx',
|
|
595
|
+
'labelframe' => 'lfrm',
|
|
596
|
+
'treeview' => 'tv',
|
|
597
|
+
'notebook' => 'nb',
|
|
598
|
+
'progressbar' => 'pbar',
|
|
599
|
+
'separator' => 'sep',
|
|
600
|
+
'spinbox' => 'spn',
|
|
601
|
+
'panedwindow' => 'pw',
|
|
602
|
+
'toplevel' => 'top',
|
|
603
|
+
'menubutton' => 'mbtn',
|
|
604
|
+
'sizegrip' => 'sg',
|
|
605
|
+
}.freeze
|
|
606
|
+
private_constant :WIDGET_PREFIXES
|
|
607
|
+
|
|
608
|
+
def next_widget_path(type, parent)
|
|
609
|
+
prefix = widget_prefix(type)
|
|
610
|
+
@widget_counters[prefix] += 1
|
|
611
|
+
parent_path = parent ? parent.to_s : ''
|
|
612
|
+
if parent_path.empty? || parent_path == '.'
|
|
613
|
+
".#{prefix}#{@widget_counters[prefix]}"
|
|
614
|
+
else
|
|
615
|
+
"#{parent_path}.#{prefix}#{@widget_counters[prefix]}"
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def widget_prefix(type)
|
|
620
|
+
parts = type.downcase.split('::')
|
|
621
|
+
base = parts.pop
|
|
622
|
+
ns = parts.join
|
|
623
|
+
short = WIDGET_PREFIXES[base] || base
|
|
624
|
+
"#{ns}#{short}"
|
|
625
|
+
end
|
|
626
|
+
|
|
484
627
|
# Force Tcl to scan auto_path for pkgIndex.tcl files so that
|
|
485
628
|
# package_names and package_versions reflect all discoverable packages.
|
|
486
629
|
def scan_packages
|
data/sample/calculator.rb
CHANGED
|
@@ -23,21 +23,18 @@ class Calculator
|
|
|
23
23
|
|
|
24
24
|
def build_ui
|
|
25
25
|
@app.show
|
|
26
|
-
@app.
|
|
27
|
-
@app.
|
|
26
|
+
@app.set_window_title('Calculator')
|
|
27
|
+
@app.set_window_resizable(false, false)
|
|
28
28
|
|
|
29
29
|
# Button style — use a larger font since macOS aqua theme
|
|
30
30
|
# ignores vertical stretch; font size drives button height
|
|
31
31
|
@app.tcl_eval('ttk::style configure Calc.TButton -font {{TkDefaultFont} 18}')
|
|
32
32
|
|
|
33
33
|
# Display
|
|
34
|
-
@app.
|
|
35
|
-
@app.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
state: :readonly,
|
|
39
|
-
font: '{TkDefaultFont} 24')
|
|
40
|
-
@app.command(:grid, '.display', row: 0, column: 0, columnspan: 4,
|
|
34
|
+
@app.set_variable('::display', '0')
|
|
35
|
+
@display = @app.create_widget('ttk::entry', textvariable: '::display',
|
|
36
|
+
justify: :right, state: :readonly, font: '{TkDefaultFont} 24')
|
|
37
|
+
@display.grid(row: 0, column: 0, columnspan: 4,
|
|
41
38
|
sticky: :ew, padx: 4, pady: 4, ipady: 8)
|
|
42
39
|
|
|
43
40
|
build_buttons
|
|
@@ -82,32 +79,30 @@ class Calculator
|
|
|
82
79
|
# Click a button by its label (for demo/testing).
|
|
83
80
|
# In recording mode, shows the pressed visual state briefly before invoking.
|
|
84
81
|
def click(label, recording: false)
|
|
85
|
-
|
|
86
|
-
return unless
|
|
82
|
+
widget = @buttons[label]
|
|
83
|
+
return unless widget
|
|
87
84
|
if recording
|
|
88
|
-
|
|
85
|
+
widget.command('state', 'pressed')
|
|
89
86
|
@app.after(80) {
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
widget.command('state', '!pressed')
|
|
88
|
+
widget.command(:invoke)
|
|
92
89
|
}
|
|
93
90
|
else
|
|
94
|
-
|
|
91
|
+
widget.command(:invoke)
|
|
95
92
|
end
|
|
96
93
|
end
|
|
97
94
|
|
|
98
95
|
def button(text, row, col, style: :num, colspan: 1, &action)
|
|
99
|
-
@btn_id = (@btn_id || 0) + 1
|
|
100
96
|
@buttons ||= {}
|
|
101
|
-
|
|
102
|
-
@buttons[text] = path
|
|
103
|
-
@app.command('ttk::button', path, text: text, style: 'Calc.TButton',
|
|
97
|
+
widget = @app.create_widget('ttk::button', text: text, style: 'Calc.TButton',
|
|
104
98
|
command: proc { |*| action.call })
|
|
105
|
-
@
|
|
99
|
+
@buttons[text] = widget
|
|
100
|
+
widget.grid(row: row, column: col, columnspan: colspan,
|
|
106
101
|
sticky: :nsew, padx: 2, pady: 2)
|
|
107
102
|
end
|
|
108
103
|
|
|
109
104
|
def update_display
|
|
110
|
-
@app.
|
|
105
|
+
@app.set_variable('::display', @display_value)
|
|
111
106
|
end
|
|
112
107
|
|
|
113
108
|
# --- Calculator logic ---
|
data/sample/debug_demo.rb
CHANGED
|
@@ -13,33 +13,31 @@ app.set_window_title('Debug Demo App')
|
|
|
13
13
|
app.set_window_geometry('300x200')
|
|
14
14
|
|
|
15
15
|
# Create some widgets
|
|
16
|
-
app.
|
|
17
|
-
app.
|
|
16
|
+
frame = app.create_widget('ttk::frame')
|
|
17
|
+
app.command(:pack, frame, fill: :both, expand: 1, padx: 10, pady: 10)
|
|
18
18
|
|
|
19
|
-
app.
|
|
20
|
-
app.
|
|
19
|
+
lbl = app.create_widget('ttk::label', parent: frame, text: 'Hello from the app')
|
|
20
|
+
app.command(:pack, lbl, pady: 5)
|
|
21
21
|
|
|
22
|
-
app.
|
|
23
|
-
app.
|
|
22
|
+
ent = app.create_widget('ttk::entry', parent: frame)
|
|
23
|
+
app.command(:pack, ent, pady: 5)
|
|
24
24
|
|
|
25
25
|
# Button that creates more widgets dynamically
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
app.
|
|
26
|
+
dynamic_widgets = []
|
|
27
|
+
add_btn = app.create_widget('ttk::button', parent: frame, text: 'Add Widget',
|
|
28
|
+
command: proc { |*|
|
|
29
|
+
btn = app.create_widget('ttk::button', parent: frame, text: "Button #{dynamic_widgets.size + 1}")
|
|
30
|
+
app.command(:pack, btn, pady: 2)
|
|
31
|
+
dynamic_widgets << btn
|
|
32
|
+
})
|
|
33
|
+
app.command(:pack, add_btn, pady: 5)
|
|
34
34
|
|
|
35
35
|
# Button to destroy last widget
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
app.tcl_eval("ttk::button .f.rm -text {Remove Widget} -command {ruby_callback #{rm_cb}}")
|
|
43
|
-
app.tcl_eval('pack .f.rm -pady 5')
|
|
36
|
+
rm_btn = app.create_widget('ttk::button', parent: frame, text: 'Remove Widget',
|
|
37
|
+
command: proc { |*|
|
|
38
|
+
widget = dynamic_widgets.pop
|
|
39
|
+
widget&.destroy
|
|
40
|
+
})
|
|
41
|
+
app.command(:pack, rm_btn, pady: 5)
|
|
44
42
|
|
|
45
43
|
app.mainloop
|