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.
-
#clipboard ⇒ Object
readonly
Returns the value of attribute clipboard.
-
#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_dir(initialdir: nil, mustexist: false, title: nil, parent: nil) ⇒ String?
Show the native "choose directory" 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.
-
#grab_release(window: '.') ⇒ void
Release a grab previously set with #grab_set.
-
#grab_set(window: '.', global: false) ⇒ void
Set the input grab on window.
-
#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.
-
#modal(window: '.', global: false) { ... } ⇒ void
Make window modal.
-
#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(path = '.') ⇒ Window
A single toplevel window, addressed by path - groups
wmsubcommands and composite window-lifecycle behaviors (#on_close, #grab_set/ #grab_release, #modal) into one object instead of threadingwindow:through a pile of unrelated flat methods. -
#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.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/teek.rb', line 99 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) @clipboard = Teek::Clipboard.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.
97 98 99 |
# File 'lib/teek.rb', line 97 def _pending_exception=(value) @_pending_exception = value end |
#callback_registry ⇒ Object (readonly)
Returns the value of attribute callback_registry.
96 97 98 |
# File 'lib/teek.rb', line 96 def callback_registry @callback_registry end |
#clipboard ⇒ Object (readonly)
Returns the value of attribute clipboard.
96 97 98 |
# File 'lib/teek.rb', line 96 def clipboard @clipboard end |
#debugger ⇒ Object (readonly)
Returns the value of attribute debugger.
96 97 98 |
# File 'lib/teek.rb', line 96 def debugger @debugger end |
#interp ⇒ Object (readonly)
Returns the value of attribute interp.
96 97 98 |
# File 'lib/teek.rb', line 96 def interp @interp end |
#widgets ⇒ Object (readonly)
Returns the value of attribute widgets.
96 97 98 |
# File 'lib/teek.rb', line 96 def @widgets end |
#winfo ⇒ Object (readonly)
Returns the value of attribute winfo.
96 97 98 |
# File 'lib/teek.rb', line 96 def winfo @winfo end |
#wm ⇒ Object (readonly)
Returns the value of attribute wm.
96 97 98 |
# File 'lib/teek.rb', line 96 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).
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 |
# File 'lib/teek.rb', line 719 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.
522 523 524 |
# File 'lib/teek.rb', line 522 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.
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/teek.rb', line 224 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.
287 288 289 290 291 292 293 294 |
# File 'lib/teek.rb', line 287 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.
245 246 247 248 249 250 251 252 253 254 |
# File 'lib/teek.rb', line 245 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.
962 963 964 965 966 967 968 969 |
# File 'lib/teek.rb', line 962 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.
974 975 976 977 978 979 980 981 982 983 984 985 986 987 |
# File 'lib/teek.rb', line 974 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
843 844 845 846 847 848 849 850 |
# File 'lib/teek.rb', line 843 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").
320 321 322 |
# File 'lib/teek.rb', line 320 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.
643 644 645 646 647 648 649 |
# File 'lib/teek.rb', line 643 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_dir(initialdir: nil, mustexist: false, title: nil, parent: nil) ⇒ String?
Show the native "choose directory" dialog.
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/teek/dialogs.rb', line 118 def choose_dir(initialdir: nil, mustexist: false, title: nil, parent: nil) args = ['tk_chooseDirectory'] args.push('-initialdir', initialdir) if initialdir args.push('-mustexist', bool_to_tcl(true)) if mustexist 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.
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 |
# File 'lib/teek.rb', line 345 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.
492 493 494 495 496 497 498 499 500 501 502 503 504 505 |
# File 'lib/teek.rb', line 492 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.
992 993 994 995 |
# File 'lib/teek.rb', line 992 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.
595 596 597 598 |
# File 'lib/teek.rb', line 595 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.
208 209 210 211 212 213 |
# File 'lib/teek.rb', line 208 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).
279 280 281 |
# File 'lib/teek.rb', line 279 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.
617 618 619 |
# File 'lib/teek.rb', line 617 def font_metrics(font) @interp.font_metrics(font) end |
#get_variable(name) ⇒ String
Get a Tcl variable's value.
585 586 587 588 589 |
# File 'lib/teek.rb', line 585 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 |
#grab_release(window: '.') ⇒ void
Prefer app.window(window).grab_release for new code - this
flat method is kept for compatibility and just delegates there.
This method returns an undefined value.
Release a grab previously set with #grab_set. See Window#grab_release.
921 922 923 |
# File 'lib/teek.rb', line 921 def grab_release(window: '.') self.window(window).grab_release end |
#grab_set(window: '.', global: false) ⇒ void
Prefer app.window(window).grab_set for new code - this flat
method is kept for compatibility and just delegates there.
This method returns an undefined value.
Set the input grab on window. See Window#grab_set.
912 913 914 |
# File 'lib/teek.rb', line 912 def grab_set(window: '.', global: false) self.window(window).grab_set(global: global) end |
#hide(window = '.') ⇒ void
This method returns an undefined value.
Hide a window without destroying it. Defaults to the root window (".").
698 699 700 |
# File 'lib/teek.rb', line 698 def hide(window = '.') self.window(window).withdraw end |
#mainloop ⇒ void
This method returns an undefined value.
Enter the Tk event loop. Blocks until the application exits.
654 655 656 657 658 659 660 661 662 663 664 |
# File 'lib/teek.rb', line 654 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.
306 307 308 |
# File 'lib/teek.rb', line 306 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.
633 634 635 |
# File 'lib/teek.rb', line 633 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).
515 516 517 |
# File 'lib/teek.rb', line 515 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 |
#modal(window: '.', global: false) { ... } ⇒ void
Prefer app.window(window).modal { } for new code - this flat
method is kept for compatibility and just delegates there.
This method returns an undefined value.
Make window modal. See Window#modal.
934 935 936 |
# File 'lib/teek.rb', line 934 def modal(window: '.', global: false, &block) self.window(window).modal(global: global, &block) end |
#on_close(window: '.') { ... } ⇒ void
Prefer app.window(window).on_close { } for new code - this
flat method is kept for compatibility and just delegates there.
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.
900 901 902 |
# File 'lib/teek.rb', line 900 def on_close(window: '.', &block) self.window(window).on_close(&block) end |
#package_names ⇒ Array<String>
List all packages known to this interpreter.
Scans auto_path for package indexes before querying.
543 544 545 546 |
# File 'lib/teek.rb', line 543 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.
552 553 554 555 556 557 |
# File 'lib/teek.rb', line 552 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.
564 565 566 567 |
# File 'lib/teek.rb', line 564 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.
138 139 140 141 142 143 |
# File 'lib/teek/dialogs.rb', line 138 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.
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'lib/teek.rb', line 380 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.
457 458 459 460 461 462 463 |
# File 'lib/teek.rb', line 457 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.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/teek.rb', line 173 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.
950 951 952 |
# File 'lib/teek.rb', line 950 def register_drop_target() @interp.register_drop_target(.to_s) end |
#require_package(name, version = nil) ⇒ String
Load a Tcl package into this interpreter.
532 533 534 535 536 537 |
# File 'lib/teek.rb', line 532 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.
576 577 578 |
# File 'lib/teek.rb', line 576 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").
765 766 767 |
# File 'lib/teek.rb', line 765 def set_window_geometry(geometry, window: '.') self.window(window).set_geometry(geometry) end |
#set_window_resizable(width, height, window: '.') ⇒ void
This method returns an undefined value.
Set whether a window is resizable.
785 786 787 |
# File 'lib/teek.rb', line 785 def set_window_resizable(width, height, window: '.') self.window(window).set_resizable(width, height) end |
#set_window_title(title, window: '.') ⇒ String
Set a window's title.
746 747 748 |
# File 'lib/teek.rb', line 746 def set_window_title(title, window: '.') self.window(window).set_title(title) end |
#show(window = '.') ⇒ void
This method returns an undefined value.
Show a window. Defaults to the root window (".").
689 690 691 |
# File 'lib/teek.rb', line 689 def show(window = '.') self.window(window).deiconify end |
#split_list(str) ⇒ Array<String>
Split a Tcl list string into a Ruby array of strings.
299 300 301 |
# File 'lib/teek.rb', line 299 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.
138 139 140 |
# File 'lib/teek.rb', line 138 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.
146 147 148 |
# File 'lib/teek.rb', line 146 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.
313 314 315 |
# File 'lib/teek.rb', line 313 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.
607 608 609 |
# File 'lib/teek.rb', line 607 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.
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 |
# File 'lib/teek.rb', line 428 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.
858 859 860 861 862 |
# File 'lib/teek.rb', line 858 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.
197 198 199 |
# File 'lib/teek.rb', line 197 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.
669 670 671 672 673 674 675 |
# File 'lib/teek.rb', line 669 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.
680 681 682 |
# File 'lib/teek.rb', line 680 def update_idletasks @interp.tcl_eval('update idletasks') end |
#window(path = '.') ⇒ Window
A single toplevel window, addressed by path - groups wm subcommands
and composite window-lifecycle behaviors (#on_close, #grab_set/
#grab_release, #modal) into one object instead of threading
window: through a pile of unrelated flat methods. This app's own
+window_title+/+set_window_title+/etc., #wm, #on_close,
#grab_set, #grab_release, and #modal all delegate here
internally - use whichever reads better to you, they're the same
underlying calls.
874 875 876 |
# File 'lib/teek.rb', line 874 def window(path = '.') Window.new(self, path) end |
#window_geometry(window: '.') ⇒ String
Get a window's current geometry.
774 775 776 |
# File 'lib/teek.rb', line 774 def window_geometry(window: '.') self.window(window).geometry end |
#window_resizable(window: '.') ⇒ Array(Boolean, Boolean)
Get whether a window is resizable.
794 795 796 |
# File 'lib/teek.rb', line 794 def window_resizable(window: '.') self.window(window).resizable end |
#window_title(window: '.') ⇒ String
Get a window's current title.
755 756 757 |
# File 'lib/teek.rb', line 755 def window_title(window: '.') self.window(window).title end |