Class: Teek::UI::Handle

Inherits:
Object
  • Object
show all
Defined in:
lib/teek/ui/handle.rb

Overview

The single handle type for a node, valid across both phases (Resolved decision #3 in the architecture doc - no separate build-time NodeRef). During build you compose/name/record-events on it; live methods (#path, #configure) raise NotRealizedError until the node's realized slot is filled in by the realizer, then the same Handle object drives the real widget through it.

Constant Summary collapse

RIGHT_CLICK_EVENTS =
MouseEvents::RIGHT_CLICK_EVENTS
MouseEvents::MENU_HANDLE_TYPES

Instance Method Summary collapse

Constructor Details

#initialize(node) ⇒ Handle

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.

Returns a new instance of Handle.



27
28
29
30
# File 'lib/teek/ui/handle.rb', line 27

def initialize(node)
  @node = node
  @addressing = (WidgetTypes.for_type(node.type)&.addressing || WidgetAddressing).new(node)
end

Instance Method Details

#appTeek::App

Returns the underlying app this widget was realized into.

Returns:

  • (Teek::App)

    the underlying app this widget was realized into

Raises:



54
55
56
# File 'lib/teek/ui/handle.rb', line 54

def app
  realized.app
end

#arc(*coords, **opts) ⇒ CanvasItem

An arc/pie-slice/chord along the oval inscribed in the bounding box [x1, y1, x2, y2]. Only valid on a ui.canvas handle.

Parameters:

  • coords (Array<Numeric>)
  • opts (Hash)

    item options, e.g. +start:+/+extent:+/+style:+/+tags:+

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



398
399
400
# File 'lib/teek/ui/handle.rb', line 398

def arc(*coords, **opts)
  create_canvas_item(:arc, coords, opts)
end

#bitmap(*coords, **opts) ⇒ CanvasItem

A stipple bitmap anchored at [x, y]. Only valid on a ui.canvas handle.

Parameters:

  • coords (Array<Numeric>)

    a single [x, y] point

  • opts (Hash)

    item options, e.g. +bitmap:+/+foreground:+/+tags:+

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



408
409
410
# File 'lib/teek/ui/handle.rb', line 408

def bitmap(*coords, **opts)
  create_canvas_item(:bitmap, coords, opts)
end

#configure(**opts) ⇒ Object

Mutate the live widget's (or, for a type with none of its own, e.g. a menu entry - the entry's own) options - delegated entirely to this node type's WidgetType#addressing strategy, so Handle itself carries no per-type knowledge of how to reach it.

Parameters:

  • opts (Hash)

    e.g. text: "Go"

Raises:



64
65
66
# File 'lib/teek/ui/handle.rb', line 64

def configure(**opts)
  @addressing.configure(**opts)
end

#destroy!(defer: nil) ⇒ nil

Tears down this node's live widget (and everything under it), releasing its callbacks via teek's existing <Destroy> cleanup, and resets it so a later push of a fresh mount rebuilds it from scratch. The rebuild gets a fresh Tk path, not necessarily this same one - path segments are claimed once and never recycled (see Document#claim_path_segment), so this stays safe even if another instance sharing this same local name is still alive elsewhere under the same parent. Typically called after popping a screen you don't want to keep warm (see Screens) - popping alone only conceals, exactly as before; this is a separate, explicit step - +ui.screens.pop&.destroy!+/+ui.modal.pop&.destroy!+.

Destroying a widget SYNCHRONOUSLY from inside the click handler of one of its own descendants (a dialog's own "Close" button tearing down the dialog it lives in) is a real Tk hazard: ttk::button (and others) queue their own internal bindings for that SAME click, which then run against a widget that's already gone. defer absorbs this automatically - no need to know about it or reach for ui.after yourself.

Parameters:

  • defer (Boolean, nil) (defaults to: nil)

    nil (the default) auto-detects: defers to the next Tk idle point (Teek.in_callback? true - the hazard above) so the current click finishes first, or destroys synchronously otherwise (a script/test with no event loop running has nothing to defer TO, and wants "gone when this call returns" semantics). Pass explicitly to override either way. Calling this again on the same handle while its own deferred destroy hasn't run yet is a safe no-op.

    The one residual to know about: when this DOES defer, it returns before the widget is actually gone - the node still reports realized and its old Tk path still exists until the deferred teardown runs. Don't destroy! then immediately rebuild a fresh mount at that SAME name/path in the same handler expecting the old one to already be gone; either pass defer: false to force it synchronous first, or build the replacement under a genuinely distinct mount (the normal "fresh lazy: true component per open" pattern already does this, since Document#claim_path_segment never reuses a path segment anyway).

Returns:

  • (nil)

Raises:



140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/teek/ui/handle.rb', line 140

def destroy!(defer: nil)
  return nil if @node.pending_destroy?

  should_defer = defer.nil? ? Teek.in_callback? : defer
  if should_defer
    app = realized.app
    @node.pending_destroy = true
    app.after_idle { perform_destroy! }
  else
    perform_destroy!
  end
  nil
end

#disableself

Shorthand for configure(state: :disabled) - greyed out, not interactive/invocable.

Returns:

  • (self)

Raises:



94
95
96
97
# File 'lib/teek/ui/handle.rb', line 94

def disable
  configure(state: :disabled)
  self
end

#ellipse(*coords, **opts) ⇒ CanvasItem Also known as: oval

An ellipse inscribed in the bounding box [x1, y1, x2, y2]. Only valid on a ui.canvas handle.

Parameters:

  • coords (Array<Numeric>)
  • opts (Hash)

    item options, e.g. +fill:+/+outline:+/+tags:+

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



354
355
356
# File 'lib/teek/ui/handle.rb', line 354

def ellipse(*coords, **opts)
  create_canvas_item(:oval, coords, opts)
end

#enableself

Shorthand for configure(state: :normal) - Tk's own default state, meaningful for anything with a -state option (a menu entry, a ttk widget, ...).

Returns:

  • (self)

Raises:



85
86
87
88
# File 'lib/teek/ui/handle.rb', line 85

def enable
  configure(state: :normal)
  self
end

#eventsArray<EventBinding>

Every event binding declared on this node so far, in declaration order - +on_click+/+on_key+/+on_drag+/+on_right_click+ and friends all funnel through here. Meaningful at any phase: before realize these are still queued (nothing wired to Tcl yet), after realize they're the bindings actually in effect - covers both a binding declared in the original build block and one added later (e.g. from inside Session#add), so this stays a true live picture, not just a record of what was queued pre-realize. Each entry's own handler is the real Proc that runs, so .source_location answers "what code does this" directly.

Returns:



423
424
425
# File 'lib/teek/ui/handle.rb', line 423

def events
  @node.events
end

#hideself

Hide the window: releases any grab #show set (a no-op if it wasn't modal - Window#grab_release is always safe to call) and withdraws it. Only valid on a ui.window handle.

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if this handle isn't a window

  • (NotRealizedError)

    before realize



326
327
328
329
330
331
332
333
334
# File 'lib/teek/ui/handle.rb', line 326

def hide
  unless type == :window
    raise ArgumentError, "hide only makes sense on a window (got a :#{type})"
  end

  grab_release
  window.withdraw
  self
end

#line(*coords, **opts) ⇒ CanvasItem

A straight line through the given points - [x1, y1, x2, y2, ...], flat or nested, two or more points. Only valid on a ui.canvas handle.

Parameters:

  • coords (Array<Numeric>)
  • opts (Hash)

    item options, e.g. +fill:+/+width:+/+tags:+

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



343
344
345
# File 'lib/teek/ui/handle.rb', line 343

def line(*coords, **opts)
  create_canvas_item(:line, coords, opts)
end

This method returns an undefined value.

Show the window modally: grabs input and sets focus on it immediately. Release it explicitly with #grab_release (typically from the window's own dismiss/close handling) when the dialog is done - not released automatically just because this method returns, since a modal dialog stays grabbed for its whole visible lifetime, not just its setup. Released immediately if the optional setup block itself raises, or if the window is destroyed while still grabbed - see Window#modal, which this delegates to entirely (no grab/focus/destroy-safety-net logic lives here). Only valid on a ui.window handle.

Parameters:

  • global (Boolean) (defaults to: false)

    see Window#grab_set

Yields:

  • optional - runs with the grab and focus already set

Raises:

  • (ArgumentError)

    if this handle isn't a window

  • (NotRealizedError)

    before realize



276
277
278
279
280
281
282
# File 'lib/teek/ui/handle.rb', line 276

def modal(global: false, &block)
  unless type == :window
    raise ArgumentError, "modal only makes sense on a window (got a :#{type})"
  end

  window.modal(global: global, &block)
end

#nameSymbol?

Returns the node's explicit name.

Returns:

  • (Symbol, nil)

    the node's explicit name



38
39
40
# File 'lib/teek/ui/handle.rb', line 38

def name
  @node.name
end

#on_click { ... } ⇒ self

Fires on a left click.

Yields:

  • called with no arguments

Returns:

  • (self)


157
158
159
160
# File 'lib/teek/ui/handle.rb', line 157

def on_click(&block)
  bind_event('<Button-1>', block)
  self
end

#on_close { ... } ⇒ self

Fires when the window's close button (titlebar close box, Cmd-W, Alt-F4, ...) is pressed. Teek's own default (destroy the window) only applies when nothing else has claimed it - the block decides whether the window actually closes; call .destroy yourself if you want that. Only valid on a ui.window handle.

Yields:

  • called with no arguments

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if this handle isn't a window



225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/teek/ui/handle.rb', line 225

def on_close(&block)
  unless type == :window
    raise ArgumentError, "on_close only makes sense on a window (got a :#{type})"
  end

  if @node.realized
    @node.realized.app.on_close(window: @node.realized.path, &block)
  else
    @node.opts[:on_close] = block
  end
  self
end

#on_drag {|x, y| ... } ⇒ self

Fires while dragging (left button held down and moving). Delivers Integer x/y, converted through the widget's own canvasx/canvasy when bound to a canvas so callers never have to remember to do that themselves.

Yields:

  • (x, y)

    Integer coordinates

Returns:

  • (self)


196
197
198
199
200
201
202
203
# File 'lib/teek/ui/handle.rb', line 196

def on_drag(&block)
  drag_type = type
  wrapped = lambda do |raw_x, raw_y|
    block.call(*convert_drag_coords(drag_type, raw_x, raw_y))
  end
  bind_event('<B1-Motion>', wrapped, subs: %i[x y])
  self
end

#on_key(spec) { ... } ⇒ self

Fires on a key press. spec is either a friendly Symbol (+:enter+, :escape, :up, ...) or a "Modifier-Modifier-Key" String (+"Ctrl-s"+, "Ctrl-Shift-s") - see Keysyms.

Parameters:

  • spec (Symbol, String)

Yields:

  • called with no arguments

Returns:

  • (self)


211
212
213
214
215
# File 'lib/teek/ui/handle.rb', line 211

def on_key(spec, &block)
  modifiers, keysym = Keysyms.resolve(spec)
  Keysyms.patterns_for(modifiers, keysym).each { |event| bind_event(event, block) }
  self
end

#on_right_click(menu = nil) { ... } ⇒ self

Fires on a right click, however the platform spells it (Button-3 on Linux/Windows, Button-2 or Control-Button-1 on macOS). Either handle it yourself with a block, or hand it a :menu/:context_menu handle to pop up at the click's screen position - not both.

Parameters:

  • menu (Handle, nil) (defaults to: nil)

    a :menu or :context_menu handle to tk_popup

Yields:

  • called with no arguments (only when menu isn't given)

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if given neither or both, or menu isn't a menu handle



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/teek/ui/handle.rb', line 170

def on_right_click(menu = nil, &block)
  if menu && block
    raise ArgumentError, "on_right_click takes either a menu handle or a block, not both"
  elsif menu
    unless MENU_HANDLE_TYPES.include?(menu.type)
      raise ArgumentError, "on_right_click(menu) needs a :menu or :context_menu handle (got a :#{menu.type})"
    end

    popup = lambda do |root_x, root_y|
      realized.app.popup_menu(menu.path, x: root_x, y: root_y)
    end
    RIGHT_CLICK_EVENTS.each { |event| bind_event(event, popup, subs: %i[root_x root_y]) }
  elsif block
    RIGHT_CLICK_EVENTS.each { |event| bind_event(event, block) }
  else
    raise ArgumentError, "on_right_click needs either a menu handle or a block"
  end
  self
end

#on_tab_changed {|name_or_index| ... } ⇒ self

Fires when the selected tab changes (Tk's <>). The block receives the newly selected tab's own name (the Symbol given via t.tab(label, name)) if it has one, otherwise its plain zero-based index - preferring a name over a raw Tk index, same as ui[:name] lookup does everywhere else in the DSL. Only valid on a ui.tabs handle.

Yields:

  • (name_or_index)

    Symbol or Integer

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if this handle isn't a tabs container



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/teek/ui/handle.rb', line 247

def on_tab_changed(&block)
  unless type == :tabs
    raise ArgumentError, "on_tab_changed only makes sense on a tabs container (got a :#{type})"
  end

  wrapped = lambda {
    index = realized.app.command(realized.path, :index, :current).to_i
    tab_node = @node.children[index]
    block.call(tab_node&.name || index)
  }
  bind_event('<<NotebookTabChanged>>', wrapped)
  self
end

#optionsHash{Symbol => String}

What Tk currently thinks this widget's (or, for a type with none of its own, e.g. a menu entry - the entry's own) options are right now, straight from a bare configure - for when a prior #configure call seems like it silently didn't take, or just exploring what's actually set on a live widget. Delegated to this node type's WidgetType#addressing strategy, same as #configure.

Returns:

  • (Hash{Symbol => String})

    option name (no leading -) => current value

Raises:



76
77
78
# File 'lib/teek/ui/handle.rb', line 76

def options
  @addressing.option_dump
end

#pathString

Returns this node's live address - the real Tk widget path for an ordinary widget, or (for a type with none of its own, e.g. a menu entry) its WidgetType#addressing strategy's own marked, Tk-path-shaped virtual path - see MenuEntryAddressing#virtual_path.

Returns:

Raises:



48
49
50
# File 'lib/teek/ui/handle.rb', line 48

def path
  @addressing.virtual_path
end

#polygon(*coords, **opts) ⇒ CanvasItem

A closed shape through the given points - [x1, y1, x2, y2, ...], flat or nested, three or more points. Only valid on a ui.canvas handle.

Parameters:

  • coords (Array<Numeric>)
  • opts (Hash)

    item options, e.g. +fill:+/+smooth:+/+tags:+

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



366
367
368
# File 'lib/teek/ui/handle.rb', line 366

def polygon(*coords, **opts)
  create_canvas_item(:polygon, coords, opts)
end

#rectangle(*coords, **opts) ⇒ CanvasItem

A rectangle with corners [x1, y1, x2, y2]. Only valid on a ui.canvas handle.

Parameters:

  • coords (Array<Numeric>)
  • opts (Hash)

    item options, e.g. +fill:+/+outline:+/+tags:+

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



377
378
379
# File 'lib/teek/ui/handle.rb', line 377

def rectangle(*coords, **opts)
  create_canvas_item(:rectangle, coords, opts)
end

#release_focusvoid Also known as: grab_release

This method returns an undefined value.

Release a grab previously set with #modal - "grab" is X11/Tk jargon for capturing all input to one window, which is what a modal dialog is doing while it's up. Only valid on a ui.window handle. See Window#grab_release.

Raises:

  • (ArgumentError)

    if this handle isn't a window

  • (NotRealizedError)

    before realize



291
292
293
294
295
296
297
# File 'lib/teek/ui/handle.rb', line 291

def release_focus
  unless type == :window
    raise ArgumentError, "release_focus only makes sense on a window (got a :#{type})"
  end

  window.grab_release
end

#showself

Reveal the window: positions it just to the right of the parent it's nested under (root, or another window if this one's nested inside it), deiconifies, raises it to the front, and - only if this window was declared modal: true - grabs input and focuses it too (via #modal). Only valid on a ui.window handle.

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if this handle isn't a window

  • (NotRealizedError)

    before realize



308
309
310
311
312
313
314
315
316
317
318
# File 'lib/teek/ui/handle.rb', line 308

def show
  unless type == :window
    raise ArgumentError, "show only makes sense on a window (got a :#{type})"
  end

  position_near_parent
  window.deiconify
  realized.app.command(:raise, realized.path)
  modal if @node.opts[:modal]
  self
end

#tagged(tag) ⇒ CanvasItem

A handle onto whatever items currently carry tag - zero, one, or many (see CanvasItem, which addresses a tag and an id identically). Doesn't create anything; a shape-creation method (e.g. #line) already returns a single-item handle for its own new item, this is for addressing a shared tags: group (or reaching an item by an id you already have) after the fact. Only valid on a ui.canvas handle.

Parameters:

  • tag (String, Symbol, Integer)

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



438
439
440
441
# File 'lib/teek/ui/handle.rb', line 438

def tagged(tag)
  raise_unless_canvas!('tagged')
  CanvasItem.new(realized.app, realized.path, tag)
end

#text(*coords, **opts) ⇒ CanvasItem

Text anchored at [x, y]. Only valid on a ui.canvas handle.

Parameters:

  • coords (Array<Numeric>)

    a single [x, y] point

  • opts (Hash)

    item options, e.g. +text:+/+fill:+/+font:+/+anchor:+/+tags:+

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a canvas

  • (NotRealizedError)

    before realize



387
388
389
# File 'lib/teek/ui/handle.rb', line 387

def text(*coords, **opts)
  create_canvas_item(:text, coords, opts)
end

#text_contentTextContent

The rich text API for this widget's content - insert/get/delete, named formats (Tk's own "tag" concept), markers, search, and embedded images. See TextContent for the full surface. Only valid on a ui.text_area handle.

Returns:

Raises:

  • (ArgumentError)

    if this handle isn't a text_area

  • (NotRealizedError)

    before realize



450
451
452
453
# File 'lib/teek/ui/handle.rb', line 450

def text_content
  raise_unless_text_area!('text_content')
  TextContent.new(realized.app, realized.path)
end

#typeSymbol

Returns the node's type, e.g. :button.

Returns:

  • (Symbol)

    the node's type, e.g. :button



33
34
35
# File 'lib/teek/ui/handle.rb', line 33

def type
  @node.type
end