Class: Teek::App
- Inherits:
-
Object
- Object
- Teek::App
- Defined in:
- lib/teek.rb,
lib/teek/dialogs.rb
Constant Summary collapse
- BIND_SUBS =
Note:
Each substitution crosses from Tcl to Ruby once. Any #command calls inside the block are additional round-trips. This is negligible for click/key events but could matter for hot-path handlers like
<Motion>that fire hundreds of times per second. For those, consider #tcl_eval with inline Tcl expressions to do all work in one evaluation.Bind a Tk event on a widget, with optional substitutions forwarded as block arguments. Substitutions can be symbols (mapped via BIND_SUBS) or raw Tcl
%codes passed through as-is. { x: '%x', y: '%y', # window coordinates root_x: '%X', root_y: '%Y', # screen coordinates widget: '%W', # widget path keysym: '%K', keycode: '%k', # key events char: '%A', # character (key events) width: '%w', height: '%h', # Configure events button: '%b', # mouse button number mouse_wheel: '%D', # mousewheel delta type: '%T', # event type data: '%d', # virtual event data (Tk 8.6+) }.freeze
Instance Attribute Summary collapse
- #_pending_exception ⇒ Object writeonly private
-
#callback_registry ⇒ Object
readonly
Returns the value of attribute callback_registry.
-
#debugger ⇒ Object
readonly
Returns the value of attribute debugger.
-
#interp ⇒ Object
readonly
Returns the value of attribute interp.
-
#widgets ⇒ Object
readonly
Returns the value of attribute widgets.
-
#winfo ⇒ Object
readonly
Returns the value of attribute winfo.
-
#wm ⇒ Object
readonly
Returns the value of attribute wm.
Instance Method Summary collapse
-
#add_debug_console(keybinding = '<F12>') ⇒ Boolean
Enable the Tk debug console.
-
#add_package_path(path) ⇒ void
Add a directory to Tcl's package search path.
-
#after(ms, on_error: :raise) { ... } ⇒ String
Schedule a one-shot timer.
-
#after_cancel(after_id) ⇒ void
Cancel a pending #after or #after_idle timer.
-
#after_idle { ... } ⇒ String
Schedule a block to run once when the event loop is idle.
-
#appearance ⇒ String?
Get the macOS window appearance.
-
#appearance=(mode) ⇒ void
Set the macOS window appearance.
- #bind(widget, event, *subs, &block) ⇒ Object
-
#bool_to_tcl(val) ⇒ String
Convert a Ruby boolean to a Tcl boolean string ("1" or "0").
-
#busy(window: '.') { ... } ⇒ Object
Show a busy cursor on a window while executing a block.
-
#choose_color(initial: nil, title: nil, parent: nil) ⇒ String?
Show the native color picker dialog.
-
#choose_open_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, multiple: false, parent: nil) ⇒ String, ...
Show the native "choose file to open" dialog.
-
#choose_save_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil) ⇒ String?
Show the native "choose file to save" dialog.
-
#command(cmd, *args, **kwargs) ⇒ String
Build and evaluate a Tcl command from Ruby values.
-
#create_widget(type, path = nil, parent: nil, idempotent: false, **kwargs) ⇒ Widget
Create a Tk widget and return a Widget wrapper.
-
#dark? ⇒ Boolean
Returns true if the window is currently displayed in dark mode.
-
#destroy(widget = '.') ⇒ void
Destroy a widget and all its children.
-
#ensure_tcl_helper(name) ⇒ void
Evaluate
scriptonce per App instance undername, skipping it on later calls. -
#every(ms, on_error: :raise) { ... } ⇒ RepeatingTimer
Schedule a repeating timer.
-
#font_metrics(font) ⇒ Hash{Symbol => Integer}
Get font metrics (ascent, descent, linespace) for a given font.
-
#get_variable(name) ⇒ String
Get a Tcl variable's value.
-
#hide(window = '.') ⇒ void
Hide a window without destroying it.
-
#initialize(title: nil, track_widgets: true, debug: false, &block) ⇒ App
constructor
A new instance of App.
-
#mainloop ⇒ void
Enter the Tk event loop.
-
#make_list(*args) ⇒ String
Build a properly-escaped Tcl list from Ruby strings.
-
#measure_chars(font, text, max_pixels, **opts) ⇒ Hash{Symbol => Integer}
Measure how many bytes of text fit within a pixel width limit.
-
#menu(path, **kwargs) ⇒ Widget
Wrap a Tk menu at the given path, creating it (tearoff disabled) if it doesn't exist yet.
-
#message_box(message:, title: nil, detail: nil, icon: :info, type: :ok, default: nil, parent: nil) ⇒ Symbol
Show a message box with one or more buttons.
-
#on_close(window: '.') { ... } ⇒ void
Register a handler for the window manager's close button (WM_DELETE_WINDOW - the titlebar close box, Cmd-W, Alt-F4, etc., depending on platform).
-
#package_names ⇒ Array<String>
List all packages known to this interpreter.
-
#package_present?(name) ⇒ Boolean
Check if a package is already loaded in this interpreter.
-
#package_versions(name) ⇒ Array<String>
List available versions of a package.
-
#popup_menu(menu, x:, y:, entry: nil) ⇒ void
Pop up a menu at the given screen coordinates.
-
#raw_command(cmd, *args, **kwargs) ⇒ String
The dumb Tcl builder underneath #command - no interceptor lookup, no per-widget-type awareness.
-
#record_widget_type(cmd, args) ⇒ void
Records that
args[0]is a widget of typecmd, if this call looks like widget creation (+cmd+ is a known WIDGET_COMMANDS entry). -
#register_callback(callable, relay_break_continue: true) ⇒ Integer
Register a Ruby callable as a Tcl callback.
-
#register_drop_target(widget) ⇒ void
Register a widget as a file drop target.
-
#require_package(name, version = nil) ⇒ String
Load a Tcl package into this interpreter.
-
#set_variable(name, value) ⇒ String
Set a Tcl variable.
-
#set_window_geometry(geometry, window: '.') ⇒ String
Set a window's geometry (e.g. "400x300", "400x300+100+50").
-
#set_window_resizable(width, height, window: '.') ⇒ void
Set whether a window is resizable.
-
#set_window_title(title, window: '.') ⇒ String
Set a window's title.
-
#show(window = '.') ⇒ void
Show a window.
-
#split_list(str) ⇒ Array<String>
Split a Tcl list string into a Ruby array of strings.
-
#tcl_eval(script) ⇒ String
Evaluate a raw Tcl script string and return the result.
-
#tcl_invoke(*args) ⇒ String
Invoke a Tcl command with pre-split arguments (no Tcl parsing).
-
#tcl_to_bool(str) ⇒ Boolean
Convert a Tcl boolean string ("0", "1", "yes", "no", etc.) to Ruby boolean.
-
#text_width(font, text) ⇒ Integer
Measure the pixel width of a text string in a given font.
-
#track_widget_option_callbacks(cmd, args, kwargs) ⇒ Hash
#command's fallback for any call that no registered interceptor claimed: registers any Proc-valued kwarg (e.g. command:, validatecommand:) as a callback tracked under
cmd, releasing it if reconfigured or when the widget is destroyed. -
#unbind(widget, event) ⇒ void
Remove an event binding previously set with #bind.
-
#unregister_callback(id) ⇒ void
Remove a previously registered callback by its ID.
-
#update ⇒ void
Process all pending events and idle callbacks, then return.
-
#update_idletasks ⇒ void
Process only pending idle callbacks (e.g. geometry redraws), then return.
-
#window_geometry(window: '.') ⇒ String
Get a window's current geometry.
-
#window_resizable(window: '.') ⇒ Array(Boolean, Boolean)
Get whether a window is resizable.
-
#window_title(window: '.') ⇒ String
Get a window's current title.
Constructor Details
#initialize(title: nil, track_widgets: true, debug: false, &block) ⇒ App
Returns a new instance of App.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/teek.rb', line 96 def initialize(title: nil, track_widgets: true, debug: false, &block) @interp = Teek::Interp.new @interp.tcl_eval('package require Tk') @winfo = Teek::Winfo.new(self) @wm = Teek::Wm.new(self) hide @widgets = {} @widget_counters = Hash.new(0) @callback_registry = Teek::CallbackRegistry.new(self) @widget_types_by_path = {} @_pending_exception = nil debug ||= !!ENV['TEEK_DEBUG'] = true if debug @track_widgets = if setup_destroy_cleanup if debug require_relative 'teek/debugger' @debugger = Teek::Debugger.new(self) end set_window_title(title) if title instance_eval(&block) if block end |
Instance Attribute Details
#_pending_exception=(value) ⇒ Object (writeonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
94 95 96 |
# File 'lib/teek.rb', line 94 def _pending_exception=(value) @_pending_exception = value end |
#callback_registry ⇒ Object (readonly)
Returns the value of attribute callback_registry.
93 94 95 |
# File 'lib/teek.rb', line 93 def callback_registry @callback_registry end |
#debugger ⇒ Object (readonly)
Returns the value of attribute debugger.
93 94 95 |
# File 'lib/teek.rb', line 93 def debugger @debugger end |
#interp ⇒ Object (readonly)
Returns the value of attribute interp.
93 94 95 |
# File 'lib/teek.rb', line 93 def interp @interp end |
#widgets ⇒ Object (readonly)
Returns the value of attribute widgets.
93 94 95 |
# File 'lib/teek.rb', line 93 def @widgets end |
#winfo ⇒ Object (readonly)
Returns the value of attribute winfo.
93 94 95 |
# File 'lib/teek.rb', line 93 def winfo @winfo end |
#wm ⇒ Object (readonly)
Returns the value of attribute wm.
93 94 95 |
# File 'lib/teek.rb', line 93 def wm @wm end |
Instance Method Details
#add_debug_console(keybinding = '<F12>') ⇒ Boolean
Enable the Tk debug console. The console starts hidden and can be toggled with the given keyboard shortcut (default: F12).
The Tk console is a built-in interactive Tcl shell — useful for inspecting variables, running Tcl commands, and debugging widget layouts at runtime. It is available on macOS and Windows only; on Linux this method is a no-op (Linux has the real terminal).
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 |
# File 'lib/teek.rb', line 715 def add_debug_console(keybinding = '<F12>') @interp.create_console @_console_visible = false toggle = proc do |*| if @_console_visible tcl_eval('console hide') @_console_visible = false else tcl_eval('console show') @_console_visible = true end end command(:bind, '.', keybinding, toggle) true rescue TclError => e warn "Teek: debug console not available on this platform (#{e.})" false end |
#add_package_path(path) ⇒ void
This method returns an undefined value.
Add a directory to Tcl's package search path.
518 519 520 |
# File 'lib/teek.rb', line 518 def add_package_path(path) tcl_eval("lappend ::auto_path {#{path}}") end |
#after(ms, on_error: :raise) { ... } ⇒ String
Schedule a one-shot timer. Calls the block after ms milliseconds.
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/teek.rb', line 220 def after(ms, on_error: :raise, &block) cb_id = nil cb_id = @interp.register_callback(proc { |*| begin block.call rescue => e raise if on_error == :raise on_error.call(e) if on_error.is_a?(Proc) ensure @interp.unregister_callback(cb_id) end }) after_id = @interp.tcl_eval("after #{ms.to_i} {ruby_callback #{cb_id}}") after_id.instance_variable_set(:@cb_id, cb_id) after_id end |
#after_cancel(after_id) ⇒ void
This method returns an undefined value.
Cancel a pending #after or #after_idle timer.
283 284 285 286 287 288 289 290 |
# File 'lib/teek.rb', line 283 def after_cancel(after_id) @interp.tcl_eval("after cancel #{after_id}") if (cb_id = after_id.instance_variable_get(:@cb_id)) @interp.unregister_callback(cb_id) after_id.instance_variable_set(:@cb_id, nil) end after_id end |
#after_idle { ... } ⇒ String
Schedule a block to run once when the event loop is idle.
241 242 243 244 245 246 247 248 249 250 |
# File 'lib/teek.rb', line 241 def after_idle(&block) cb_id = nil cb_id = @interp.register_callback(proc { |*| block.call @interp.unregister_callback(cb_id) }) after_id = @interp.tcl_eval("after idle {ruby_callback #{cb_id}}") after_id.instance_variable_set(:@cb_id, cb_id) after_id end |
#appearance ⇒ String?
Get the macOS window appearance. No-op (returns nil) on non-macOS.
910 911 912 913 914 915 916 917 |
# File 'lib/teek.rb', line 910 def appearance return nil unless aqua? if tk_major >= 9 @interp.tcl_eval('wm attributes . -appearance').delete('"') else @interp.tcl_eval('tk::unsupported::MacWindowStyle appearance .') end end |
#appearance=(mode) ⇒ void
This method returns an undefined value.
Set the macOS window appearance. No-op on non-macOS.
922 923 924 925 926 927 928 929 930 931 932 933 934 935 |
# File 'lib/teek.rb', line 922 def appearance=(mode) return unless aqua? value = case mode.to_sym when :light then 'aqua' when :dark then 'darkaqua' when :auto then 'auto' else mode.to_s end if tk_major >= 9 @interp.tcl_eval("wm attributes . -appearance #{value}") else @interp.tcl_eval("tk::unsupported::MacWindowStyle appearance . #{value}") end end |
#bind(widget, event, *subs, &block) ⇒ Object
839 840 841 842 843 844 845 846 |
# File 'lib/teek.rb', line 839 def bind(, event, *subs, &block) event_str = event.start_with?('<') ? event : "<#{event}>" cb = register_callback(proc { |*args| block.call(*args) }) @callback_registry.reconcile([:bind, ]) { |before| before.merge(event_str => cb) } tcl_subs = subs.map { |s| s.is_a?(Symbol) ? BIND_SUBS.fetch(s) : s.to_s } sub_str = tcl_subs.empty? ? '' : ' ' + tcl_subs.join(' ') @interp.tcl_eval("bind #{} #{event_str} {ruby_callback #{cb}#{sub_str}}") end |
#bool_to_tcl(val) ⇒ String
Convert a Ruby boolean to a Tcl boolean string ("1" or "0").
316 317 318 |
# File 'lib/teek.rb', line 316 def bool_to_tcl(val) Teek.bool_to_tcl(val) end |
#busy(window: '.') { ... } ⇒ Object
Show a busy cursor on a window while executing a block. The cursor is restored even if the block raises.
639 640 641 642 643 644 645 |
# File 'lib/teek.rb', line 639 def busy(window: '.') tcl_eval("tk busy hold #{window}") tcl_eval('update idletasks') yield ensure tcl_eval("tk busy forget #{window}") end |
#choose_color(initial: nil, title: nil, parent: nil) ⇒ String?
Show the native color picker dialog.
99 100 101 102 103 104 105 106 107 |
# File 'lib/teek/dialogs.rb', line 99 def choose_color(initial: nil, title: nil, parent: nil) args = ['tk_chooseColor'] args.push('-initialcolor', initial) if initial args.push('-title', title) if title args.push('-parent', parent) if parent result = tcl_invoke(*args) result.empty? ? nil : result end |
#choose_open_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, multiple: false, parent: nil) ⇒ String, ...
Show the native "choose file to open" dialog.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/teek/dialogs.rb', line 19 def choose_open_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, multiple: false, parent: nil) args = ['tk_getOpenFile'] args.push('-filetypes', build_filetypes(filetypes)) if filetypes args.push('-initialdir', initialdir) if initialdir args.push('-initialfile', initialfile) if initialfile args.push('-title', title) if title args.push('-parent', parent) if parent args.push('-multiple', bool_to_tcl(true)) if multiple result = tcl_invoke(*args) return nil if result.empty? multiple ? split_list(result) : result end |
#choose_save_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil) ⇒ String?
Show the native "choose file to save" dialog.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/teek/dialogs.rb', line 49 def choose_save_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil) args = ['tk_getSaveFile'] args.push('-filetypes', build_filetypes(filetypes)) if filetypes args.push('-initialdir', initialdir) if initialdir args.push('-initialfile', initialfile) if initialfile args.push('-title', title) if title args.push('-defaultextension', defaultextension) if defaultextension args.push('-confirmoverwrite', bool_to_tcl(false)) unless confirmoverwrite args.push('-parent', parent) if parent result = tcl_invoke(*args) result.empty? ? nil : result end |
#command(cmd, *args, **kwargs) ⇒ String
Build and evaluate a Tcl command from Ruby values. Positional args
are converted: Symbols pass bare, Procs become callbacks, everything
else is brace-quoted. Keyword args become -key value option pairs.
Any Proc-valued arg or kwarg is tracked and released on overwrite,
explicit removal, or the owning widget's destruction - there is no
unsafe way to attach a callback through this method, regardless of
whether the call happens to match a registered per-widget-type
interceptor (see CommandInterceptors) or falls through to the
generic default. Widget type is inferred automatically from calls
shaped like widget creation (a WIDGET_COMMANDS name as cmd, the
new path as the first positional arg) - not tied to track_widgets.
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/teek.rb', line 341 def command(cmd, *args, **kwargs) (cmd, args) type = @widget_types_by_path[cmd.to_s] entries = type ? CommandInterceptors.for_type(type) : [] matches = entries.map { |entry| [entry.label, entry.block.call(self, cmd, args, kwargs)] } .reject { |_label, result| result.nil? } case matches.size when 0 then raw_command(cmd, *args, **(cmd, args, kwargs)) when 1 then matches.first.last else labels = matches.map(&:first).join(', ') raise AmbiguousCommandError, "#{matches.size} command interceptors (#{labels}) matched #{cmd.inspect} #{args.inspect} " \ "for widget type #{type.inspect}" end end |
#create_widget(type, path = nil, parent: nil, idempotent: false, **kwargs) ⇒ Widget
Create a Tk widget and return a Widget wrapper.
Auto-generates a unique path if none is given. The path is derived from the widget type and a monotonic counter.
488 489 490 491 492 493 494 495 496 497 498 499 500 501 |
# File 'lib/teek.rb', line 488 def (type, path = nil, parent: nil, idempotent: false, **kwargs) type_s = type.to_s path ||= (type_s, parent) if idempotent && @winfo.exists?(path) # Still record the type even though creation itself is skipped - a # path that already existed (e.g. built with a raw tcl_eval) is # otherwise never seen by #command, so no interceptor could ever # engage for it. (type_s, [path]) else command(type_s, path, **kwargs) end Widget.new(self, path) end |
#dark? ⇒ Boolean
Returns true if the window is currently displayed in dark mode. Always returns false on non-macOS.
940 941 942 943 |
# File 'lib/teek.rb', line 940 def dark? return false unless aqua? @interp.tcl_eval('tk::unsupported::MacWindowStyle isdark .').delete('"') == '1' end |
#destroy(widget = '.') ⇒ void
This method returns an undefined value.
Destroy a widget and all its children.
591 592 593 594 |
# File 'lib/teek.rb', line 591 def destroy( = '.') raise ArgumentError, 'widget path cannot be nil' if .nil? tcl_eval("destroy #{}") end |
#ensure_tcl_helper(name) ⇒ void
This method returns an undefined value.
Evaluate script once per App instance under name, skipping it on
later calls. Meant for widget-behavior modules that need to define a
Tcl-side helper proc (e.g. a scan routine) without re-sending and
re-parsing that definition on every call.
204 205 206 207 208 209 |
# File 'lib/teek.rb', line 204 def ensure_tcl_helper(name) @installed_tcl_helpers ||= {} return if @installed_tcl_helpers[name] @interp.tcl_eval(yield) @installed_tcl_helpers[name] = true end |
#every(ms, on_error: :raise) { ... } ⇒ RepeatingTimer
Schedule a repeating timer. Calls the block every ms milliseconds
until cancelled. The block runs on the main thread in the event loop,
so it must be fast (don't block the UI).
275 276 277 |
# File 'lib/teek.rb', line 275 def every(ms, on_error: :raise, &block) RepeatingTimer.new(self, ms, on_error: on_error, &block) end |
#font_metrics(font) ⇒ Hash{Symbol => Integer}
Get font metrics (ascent, descent, linespace) for a given font. Uses Tk's C font API directly.
613 614 615 |
# File 'lib/teek.rb', line 613 def font_metrics(font) @interp.font_metrics(font) end |
#get_variable(name) ⇒ String
Get a Tcl variable's value.
581 582 583 584 585 |
# File 'lib/teek.rb', line 581 def get_variable(name) value = @interp.tcl_get_var(name.to_s) return value unless value.nil? raise Teek::TclError, "can't read \"#{name}\": no such variable" end |
#hide(window = '.') ⇒ void
This method returns an undefined value.
Hide a window without destroying it. Defaults to the root window (".").
694 695 696 |
# File 'lib/teek.rb', line 694 def hide(window = '.') @wm.withdraw(window: window) end |
#mainloop ⇒ void
This method returns an undefined value.
Enter the Tk event loop. Blocks until the application exits.
650 651 652 653 654 655 656 657 658 659 660 |
# File 'lib/teek.rb', line 650 def mainloop if defined?(IRB) || defined?(Pry) || $0 == 'irb' || $0 == 'pry' warn "Teek: mainloop blocks the current thread and will make your REPL unresponsive.\n" \ " Instead, use app.update in a loop or call app.update manually between commands:\n" \ " app.show\n" \ " app.update # process pending events\n" \ " # ... interact with your app ...\n" \ " app.update # process again after changes" end @interp.mainloop end |
#make_list(*args) ⇒ String
Build a properly-escaped Tcl list from Ruby strings.
302 303 304 |
# File 'lib/teek.rb', line 302 def make_list(*args) Teek.make_list(*args) end |
#measure_chars(font, text, max_pixels, **opts) ⇒ Hash{Symbol => Integer}
Measure how many bytes of text fit within a pixel width limit. Useful for text truncation, ellipsis, and line wrapping.
629 630 631 |
# File 'lib/teek.rb', line 629 def measure_chars(font, text, max_pixels, **opts) @interp.measure_chars(font, text, max_pixels, opts) end |
#menu(path, **kwargs) ⇒ Widget
Wrap a Tk menu at the given path, creating it (tearoff disabled) if it doesn't exist yet. Safe to call repeatedly with the same path - it's a flyweight, not a handle you need to hold onto: call this again any time you're about to rebuild the menu (e.g. on every right-click).
511 512 513 |
# File 'lib/teek.rb', line 511 def (path, **kwargs) (:menu, path, idempotent: true, tearoff: 0, **kwargs) end |
#message_box(message:, title: nil, detail: nil, icon: :info, type: :ok, default: nil, parent: nil) ⇒ Symbol
Show a message box with one or more buttons.
79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/teek/dialogs.rb', line 79 def (message:, title: nil, detail: nil, icon: :info, type: :ok, default: nil, parent: nil) args = ['tk_messageBox', '-message', .to_s] args.push('-title', title) if title args.push('-detail', detail) if detail args.push('-icon', icon.to_s) if icon args.push('-type', type.to_s) if type args.push('-default', default.to_s) if default args.push('-parent', parent) if parent tcl_invoke(*args).to_sym end |
#on_close(window: '.') { ... } ⇒ void
This method returns an undefined value.
Register a handler for the window manager's close button (WM_DELETE_WINDOW - the titlebar close box, Cmd-W, Alt-F4, etc., depending on platform).
Tk's own default behavior (destroy the window) only applies when nothing else has claimed this protocol - setting a handler here replaces it, so the block is entirely responsible for deciding whether the window actually closes. Call #destroy yourself if you want it to; do nothing (or show a confirmation first) if you don't.
880 881 882 883 884 |
# File 'lib/teek.rb', line 880 def on_close(window: '.', &block) cb = register_callback(block, relay_break_continue: false) @callback_registry.reconcile([:wm_protocol, window]) { |before| before.merge('WM_DELETE_WINDOW' => cb) } @interp.tcl_eval("wm protocol #{window} WM_DELETE_WINDOW {ruby_callback #{cb}}") end |
#package_names ⇒ Array<String>
List all packages known to this interpreter.
Scans auto_path for package indexes before querying.
539 540 541 542 |
# File 'lib/teek.rb', line 539 def package_names scan_packages split_list(tcl_eval('package names')) end |
#package_present?(name) ⇒ Boolean
Check if a package is already loaded in this interpreter.
548 549 550 551 552 553 |
# File 'lib/teek.rb', line 548 def package_present?(name) tcl_eval("package present #{name}") true rescue Teek::TclError false end |
#package_versions(name) ⇒ Array<String>
List available versions of a package.
Scans auto_path for package indexes before querying.
560 561 562 563 |
# File 'lib/teek.rb', line 560 def package_versions(name) scan_packages split_list(tcl_eval("package versions #{name}")) end |
#popup_menu(menu, x:, y:, entry: nil) ⇒ void
This method returns an undefined value.
Pop up a menu at the given screen coordinates.
118 119 120 121 122 123 |
# File 'lib/teek/dialogs.rb', line 118 def (, x:, y:, entry: nil) args = ['tk_popup', .to_s, x.to_s, y.to_s] args << entry.to_s if entry tcl_invoke(*args) nil end |
#raw_command(cmd, *args, **kwargs) ⇒ String
The dumb Tcl builder underneath #command - no interceptor lookup, no per-widget-type awareness. Used internally by interceptors (to actually perform their Tcl work without re-entering dispatch) and by #command's own generic fallback. Any Proc here still gets registered as a real, working callback (positional: bind-shaped, relay_break_continue: true; kwarg, via #tcl_arg_value: option-shaped, relay_break_continue: false) - it just isn't tracked for release. Prefer #command; call this directly only from within an interceptor.
Built as a plain argv array passed to Interp#tcl_invoke
(Tcl_EvalObjv) rather than a joined string handed to tcl_eval, so
no value needs escaping - unbalanced braces, $, [, newlines,
whatever, all pass through verbatim. There is nothing here for a
value to "break out" of.
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'lib/teek.rb', line 376 def raw_command(cmd, *args, **kwargs) argv = [cmd.to_s] i = 0 while i < args.length arg = args[i] if arg.is_a?(Proc) id = register_callback(arg) subs = [] while i + 1 < args.length && args[i + 1].is_a?(String) && args[i + 1].start_with?('%') subs << args[i + 1] i += 1 end argv << if subs.empty? "ruby_callback #{id}" else "ruby_callback #{id} #{subs.join(' ')}" end else argv << tcl_arg_value(arg) end i += 1 end kwargs.each do |key, value| argv << "-#{key}" argv << tcl_arg_value(value) end @interp.tcl_invoke(*argv) end |
#record_widget_type(cmd, args) ⇒ void
This method returns an undefined value.
Records that args[0] is a widget of type cmd, if this call looks
like widget creation (+cmd+ is a known WIDGET_COMMANDS entry). Not
tied to track_widgets - this is what lets #command look up a
registered interceptor for a bare path string on any later call,
regardless of how the widget was created.
453 454 455 456 457 458 459 |
# File 'lib/teek.rb', line 453 def (cmd, args) return unless WIDGET_COMMANDS.include?(cmd.to_s) path = args[0] return unless path.is_a?(String) @widget_types_by_path[path] = cmd.to_s end |
#register_callback(callable, relay_break_continue: true) ⇒ Integer
Register a Ruby callable as a Tcl callback.
The callable can use throw for Tcl control flow:
throw :teek_break - stop event propagation (like Tcl "break")
throw :teek_continue - Tcl TCL_CONTINUE
throw :teek_return - Tcl TCL_RETURN
+:teek_break+/+:teek_continue+ only mean something when Tcl actually
dispatches the result through a context that knows how to handle
TCL_BREAK/TCL_CONTINUE - Tk's bind mechanism does; a plain script
invocation (a menu entry's or widget's -command) does not, and
returning either code there is a Tcl error ("invoked break/continue
outside of a loop"), not a no-op. relay_break_continue: false is
for exactly those non-bind callers: throw is still caught (so it
can't crash as an uncaught throw), but is treated as the callback
simply finishing, instead of being relayed to Tcl. :teek_return is
always relayed either way - TCL_RETURN is safe in any context.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/teek.rb', line 169 def register_callback(callable, relay_break_continue: true) wrapped = proc { |*args| caught = nil catch(:teek_break) do catch(:teek_continue) do catch(:teek_return) do callable.call(*args) caught = :_none end caught ||= :return end caught ||= :continue end caught ||= :break next nil if caught == :_none next caught if caught == :return relay_break_continue ? caught : nil } @interp.register_callback(wrapped) end |
#register_drop_target(widget) ⇒ void
This method returns an undefined value.
Register a widget as a file drop target.
After registration, dropping files onto the widget generates a single
<<DropFile>> virtual event with all file paths as a Tcl list in the
event data. Use #split_list to convert to a Ruby array.
898 899 900 |
# File 'lib/teek.rb', line 898 def register_drop_target() @interp.register_drop_target(.to_s) end |
#require_package(name, version = nil) ⇒ String
Load a Tcl package into this interpreter.
528 529 530 531 532 533 |
# File 'lib/teek.rb', line 528 def require_package(name, version = nil) cmd = version ? "package require #{name} #{version}" : "package require #{name}" tcl_eval(cmd) rescue Teek::TclError => e raise Teek::TclError, "Package '#{name}' not found. Ensure it is installed and on Tcl's auto_path. (#{e.})" end |
#set_variable(name, value) ⇒ String
Set a Tcl variable. Useful for widget textvariable and variable options.
Goes through Tcl_SetVar directly (no re-parsing), so the value never
needs escaping - braces, backslashes, $, [, whatever, all safe.
572 573 574 |
# File 'lib/teek.rb', line 572 def set_variable(name, value) @interp.tcl_set_var(name.to_s, value.to_s) end |
#set_window_geometry(geometry, window: '.') ⇒ String
Set a window's geometry (e.g. "400x300", "400x300+100+50").
761 762 763 |
# File 'lib/teek.rb', line 761 def set_window_geometry(geometry, window: '.') @wm.set_geometry(geometry, window: window) end |
#set_window_resizable(width, height, window: '.') ⇒ void
This method returns an undefined value.
Set whether a window is resizable.
781 782 783 |
# File 'lib/teek.rb', line 781 def set_window_resizable(width, height, window: '.') @wm.set_resizable(width, height, window: window) end |
#set_window_title(title, window: '.') ⇒ String
Set a window's title.
742 743 744 |
# File 'lib/teek.rb', line 742 def set_window_title(title, window: '.') @wm.set_title(title, window: window) end |
#show(window = '.') ⇒ void
This method returns an undefined value.
Show a window. Defaults to the root window (".").
685 686 687 |
# File 'lib/teek.rb', line 685 def show(window = '.') @wm.deiconify(window: window) end |
#split_list(str) ⇒ Array<String>
Split a Tcl list string into a Ruby array of strings.
295 296 297 |
# File 'lib/teek.rb', line 295 def split_list(str) Teek.split_list(str) end |
#tcl_eval(script) ⇒ String
Any callback embedded in script (e.g. a hand-built
ruby_callback <id>) is on you to register and release - none of
#command's tracking applies here. Creating a widget this way
(instead of via #command/#create_widget/#menu) also means its
type is never recorded, so a registered CommandInterceptors entry
won't engage for it even on later, ordinary #command calls -
create widgets through those methods and reach for tcl_eval for
everything else.
Evaluate a raw Tcl script string and return the result. Prefer #command for building commands from Ruby values; use this when you need Tcl-level features like variable substitution or inline expressions that #command can't express.
134 135 136 |
# File 'lib/teek.rb', line 134 def tcl_eval(script) @interp.tcl_eval(script) end |
#tcl_invoke(*args) ⇒ String
Invoke a Tcl command with pre-split arguments (no Tcl parsing). Safer than #tcl_eval when arguments may contain special characters.
142 143 144 |
# File 'lib/teek.rb', line 142 def tcl_invoke(*args) @interp.tcl_invoke(*args) end |
#tcl_to_bool(str) ⇒ Boolean
Convert a Tcl boolean string ("0", "1", "yes", "no", etc.) to Ruby boolean.
309 310 311 |
# File 'lib/teek.rb', line 309 def tcl_to_bool(str) Teek.tcl_to_bool(str) end |
#text_width(font, text) ⇒ Integer
Measure the pixel width of a text string in a given font.
Uses Tk's C font API directly — faster than the Tcl font measure command.
603 604 605 |
# File 'lib/teek.rb', line 603 def text_width(font, text) @interp.text_width(font, text) end |
#track_widget_option_callbacks(cmd, args, kwargs) ⇒ Hash
#command's fallback for any call that no registered interceptor
claimed: registers any Proc-valued kwarg (e.g. command:,
validatecommand:) as a callback tracked under cmd, releasing it if
reconfigured or when the widget is destroyed. A widget's own options
are never silently renumbered or invalidated out from under us the
way menu entries are, so this uses a cheap in-memory
CallbackRegistry#reconcile rather than a live-scan one.
Tracked under the widget's own path, by [*context, key], where
context is args normalized to strings - except a bare configure
(or widget creation - the container is the new widget's path, not the
cmd used to create it) normalizes to an empty context, since all
three address the same underlying option namespace and must replace
each other. Any other subcommand (e.g. a treeview's heading col)
keeps its own args as part of the key, so two different targets
sharing an option name (two columns both using command:) don't
collide.
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
# File 'lib/teek.rb', line 424 def (cmd, args, kwargs) proc_kwargs = kwargs.select { |_, value| value.is_a?(Proc) } return kwargs if proc_kwargs.empty? if WIDGET_COMMANDS.include?(cmd.to_s) && args[0].is_a?(String) = args[0] context = [] else = cmd.to_s context = (args.empty? || args[0].to_s == 'configure') ? [] : args.map(&:to_s) end ids = {} replacements = {} proc_kwargs.each do |key, value| id = register_callback(value, relay_break_continue: false) ids[context + [key.to_s]] = id replacements[key] = "ruby_callback #{id}" end @callback_registry.reconcile([:widget_option, ]) { |before| before.merge(ids) } kwargs.merge(replacements) end |
#unbind(widget, event) ⇒ void
This method returns an undefined value.
Remove an event binding previously set with #bind.
854 855 856 857 858 |
# File 'lib/teek.rb', line 854 def unbind(, event) event_str = event.start_with?('<') ? event : "<#{event}>" @callback_registry.reconcile([:bind, ]) { |before| before.reject { |k, _| k == event_str } } @interp.tcl_eval("bind #{} #{event_str} {}") end |
#unregister_callback(id) ⇒ void
This method returns an undefined value.
Remove a previously registered callback by its ID.
193 194 195 |
# File 'lib/teek.rb', line 193 def unregister_callback(id) @interp.unregister_callback(id) end |
#update ⇒ void
This method returns an undefined value.
Process all pending events and idle callbacks, then return.
665 666 667 668 669 670 671 |
# File 'lib/teek.rb', line 665 def update @interp.tcl_eval('update') if (e = @_pending_exception) @_pending_exception = nil raise e end end |
#update_idletasks ⇒ void
This method returns an undefined value.
Process only pending idle callbacks (e.g. geometry redraws), then return.
676 677 678 |
# File 'lib/teek.rb', line 676 def update_idletasks @interp.tcl_eval('update idletasks') end |
#window_geometry(window: '.') ⇒ String
Get a window's current geometry.
770 771 772 |
# File 'lib/teek.rb', line 770 def window_geometry(window: '.') @wm.geometry(window: window) end |
#window_resizable(window: '.') ⇒ Array(Boolean, Boolean)
Get whether a window is resizable.
790 791 792 |
# File 'lib/teek.rb', line 790 def window_resizable(window: '.') @wm.resizable(window: window) end |
#window_title(window: '.') ⇒ String
Get a window's current title.
751 752 753 |
# File 'lib/teek.rb', line 751 def window_title(window: '.') @wm.title(window: window) end |