Class: Teek::UI::Session
- Inherits:
-
Object
- Object
- Teek::UI::Session
- Includes:
- WidgetDSL
- Defined in:
- lib/teek/ui/session.rb
Overview
The object yielded to (and returned by) app - owns the
build-phase Document and the realize/run lifecycle, and (via
WidgetDSL) the ui.<widget> build surface itself.
Building is Tk-free: app never constructs a App, so the block runs (and #document is buildable/inspectable) with no interpreter at all. Nothing talks to Tk until #realize (called by #run and #run_async, or directly) actually creates one and walks the tree into it via Realizer.
Defined Under Namespace
Classes: Timer
Constant Summary collapse
- DEFAULT_TOAST_DURATION_MS =
Milliseconds a #toast stays visible when no
duration:is given. 1500
Constants included from WidgetDSL
WidgetDSL::MENU_BAR_HOSTS, WidgetDSL::ORIENTATIONS
Instance Attribute Summary collapse
-
#document ⇒ Document
readonly
The build-phase tree - constructible and traversable with no interpreter, before or after realize.
-
#images ⇒ Array<Image>
readonly
Images declared in this build - retained here for the session's whole lifetime, so a widget's
image:never outlives the Photo it points at (see Image). -
#vars ⇒ Array<Var>
readonly
Reactive variables declared in this build.
Attributes included from WidgetDSL
Instance Method Summary collapse
-
#add(parent_name) {|ui| ... } ⇒ nil
Build and immediately realize a subtree into the already-running app, as a child of an already-realized widget named
parent_name- for dynamic UIs (adding cards/rows/menu entries at runtime), not just the initial build. -
#after(ms, on_error: :raise, &block) ⇒ Object?
See #every.
-
#app ⇒ Teek::App
The underlying app - the DSL's escape hatch.
-
#busy(window: '.') { ... } ⇒ Object
Show a busy cursor over
windowfor the duration of the block - App#busy already restores it even if the block raises, nothing extra to do here for that. -
#choose_color(initial: nil, title: nil, parent: nil) ⇒ Object
Show the native color picker dialog.
-
#choose_dir(initialdir: nil, mustexist: false, title: nil, parent: nil) ⇒ Object
Show the native "choose directory" dialog.
-
#clipboard ⇒ Teek::Clipboard
+.set(text)+/+.get+/+.clear+ - text widgets don't need this at all for their own copy/cut/paste (Tk wires that to the platform's expected keys already); this is for reading/writing the clipboard directly from app code.
-
#debug_info ⇒ Hash{Symbol => Integer}
A live snapshot of currently-registered callbacks, grouped by what registered them - "is my app leaking callbacks, and where." A tag absent from the result means nothing of that kind is currently registered (not a zero entry).
- #emit(event, *args, **kwargs) ⇒ void
-
#every(ms, on_error: :raise, &block) ⇒ Object?
Same queue-then-wire shape as an
on_*event binding: called inside the build block, it queues and registers once the tree realizes; called after, it registers immediately - same method, correct behavior either way, so a tick loop can be declared right alongside the UI it drives instead of being forced out to a separate post-run_asyncstep. -
#find_by_path(path) ⇒ Handle?
Reverse lookup: given a real Tk path (from an error message, a
winfoquery, or poking around in a REPL), find which widget it belongs to - the counterpart to the name-basedui[:name]. -
#initialize(title: nil, scroll: nil, app_opts: {}) ⇒ Session
constructor
private
A new instance of Session.
-
#message(message:, title: nil, detail: nil, icon: :info, type: :ok, default: nil, parent: nil) ⇒ Object
Show a message box with one or more buttons.
- #off(event, block) ⇒ void
-
#on(event, &block) ⇒ Proc
The block, to pass to a later #off.
-
#open_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, multiple: false, parent: nil) ⇒ Object
Show the native "choose file to open" dialog.
-
#realize(strict: false) ⇒ Teek::App
Validate the build tree, then create the underlying App and realize the tree into it, if that hasn't happened yet.
-
#run(strict: false, debug: false) ⇒ void
Realize, show the window, and enter the Tk event loop.
-
#run_async(strict: false, debug: false) ⇒ self
Realize and show the window without entering the event loop, for interactive/REPL use.
-
#save_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil) ⇒ Object
Show the native "choose file to save" dialog.
-
#toast(message, duration: DEFAULT_TOAST_DURATION_MS) ⇒ void
Briefly flash a message near the bottom of the window - "Saved" after a save action, "Settings" when a modal gains focus, that kind of transient feedback, not a persistent status.
Methods included from WidgetDSL
#[], #box, #cell, #component, #context_menu, #current_path, #dialog, #image, #menu_bar, #overlay, #pane, #raw, #screens, #split, #stretch, #tab, #var
Constructor Details
#initialize(title: nil, scroll: nil, app_opts: {}) ⇒ Session
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 Session.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/teek/ui/session.rb', line 42 def initialize(title: nil, scroll: nil, app_opts: {}) @title = title @scroll = scroll @app_opts = app_opts @document = Document.new @stack = [@document.root] @scope_stack = [Scope::TOP_LEVEL] @vars = [] @images = [] @app = nil @in_add = false @bus = EventBus.new @timers = [] @toast_path = nil @toast_timer_id = nil end |
Instance Attribute Details
#document ⇒ Document (readonly)
Returns the build-phase tree - constructible and traversable with no interpreter, before or after realize.
31 32 33 |
# File 'lib/teek/ui/session.rb', line 31 def document @document end |
#images ⇒ Array<Image> (readonly)
Returns images declared in this build - retained
here for the session's whole lifetime, so a widget's image:
never outlives the Photo it points at (see Image).
39 40 41 |
# File 'lib/teek/ui/session.rb', line 39 def images @images end |
#vars ⇒ Array<Var> (readonly)
Returns reactive variables declared in this build.
34 35 36 |
# File 'lib/teek/ui/session.rb', line 34 def vars @vars end |
Instance Method Details
#add(parent_name) {|ui| ... } ⇒ nil
Build and immediately realize a subtree into the already-running
app, as a child of an already-realized widget named parent_name -
for dynamic UIs (adding cards/rows/menu entries at runtime), not
just the initial build. The block uses the exact same widget DSL as
everywhere else (a.button(...), a.column(...) { }, ...); new
widgets show up immediately, routed through the same
App#command/leak-cleanup path the initial realize uses, so
destroying an added widget reclaims its callbacks the normal way.
Unlike the initial #realize, this does not run Validator - it's already-known-good territory (the session realized once already); validating one small addition on every call would be wasted work.
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/teek/ui/session.rb', line 329 def add(parent_name) raise_unless_realized! parent_node = @document.find(parent_name) or raise ArgumentError, "no widget named :#{parent_name} in this build" raise NotRealizedError, "##{parent_name} is not realized yet" unless parent_node.realized before = parent_node.children.length vars_before = @vars.length images_before = @images.length push_stack(parent_node) @in_add = true begin yield self if block_given? ensure @in_add = false pop_stack end # A var/image declared inside this block needs to be real before # the new widget subtree realizes, exactly like the initial # #realize orders them - a widget referencing one via # bind:/image: assumes it's already backed by the time IT gets # created (see Var#realize/Image#realize). @vars[vars_before..].each { |v| v.realize(@app) } @images[images_before..].each { |img| img.realize(@app) } realizer = Realizer.new(@app, @document, default_scroll: @scroll) # A lazy: true child built in this block (see WidgetDSL#append_container) # stays unrealized here too, exactly like one built during the # initial realize - it's realized later, on demand (see Handle#realize!). parent_node.children[before..].each { |child| realizer.realize_subtree(child, parent_node) unless child.lazy? } nil end |
#after(ms, on_error: :raise, &block) ⇒ Object?
Returns see #every.
213 214 215 216 217 218 219 220 |
# File 'lib/teek/ui/session.rb', line 213 def after(ms, on_error: :raise, &block) if @app @app.after(ms, on_error: on_error, &block) else @timers << Timer.new(kind: :after, ms: ms, on_error: on_error, block: block) nil end end |
#app ⇒ Teek::App
Returns the underlying app - the DSL's escape hatch.
Anything the DSL doesn't wrap yet is one call away: ui.app.command(...).
62 63 64 65 |
# File 'lib/teek/ui/session.rb', line 62 def app raise_unless_realized! @app end |
#busy(window: '.') { ... } ⇒ Object
Show a busy cursor over window for the duration of the block -
App#busy already restores it even if the block raises,
nothing extra to do here for that.
273 274 275 276 |
# File 'lib/teek/ui/session.rb', line 273 def busy(window: '.', &block) raise_unless_realized! @app.busy(window: window, &block) end |
#choose_color(initial: nil, title: nil, parent: nil) ⇒ Object
Show the native color picker dialog.
253 254 255 256 |
# File 'lib/teek/ui/session.rb', line 253 def choose_color(initial: nil, title: nil, parent: nil) raise_unless_realized! @app.choose_color(initial: initial, title: title, parent: parent) end |
#choose_dir(initialdir: nil, mustexist: false, title: nil, parent: nil) ⇒ Object
Show the native "choose directory" dialog.
261 262 263 264 |
# File 'lib/teek/ui/session.rb', line 261 def choose_dir(initialdir: nil, mustexist: false, title: nil, parent: nil) raise_unless_realized! @app.choose_dir(initialdir: initialdir, mustexist: mustexist, title: title, parent: parent) end |
#clipboard ⇒ Teek::Clipboard
Returns +.set(text)+/+.get+/+.clear+ - text widgets don't need this at all for their own copy/cut/paste (Tk wires that to the platform's expected keys already); this is for reading/writing the clipboard directly from app code.
283 284 285 286 |
# File 'lib/teek/ui/session.rb', line 283 def clipboard raise_unless_realized! @app.clipboard end |
#debug_info ⇒ Hash{Symbol => Integer}
A live snapshot of currently-registered callbacks, grouped by
what registered them - "is my app leaking callbacks, and where."
A tag absent from the result means nothing of that kind is
currently registered (not a zero entry). Safe to call any time
after realize; see run/run_async's debug: for printing this
automatically instead of calling it yourself.
77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/teek/ui/session.rb', line 77 def debug_info raise_unless_realized! counts = @app.callback_registry.counts_by_tag { event_bindings: counts[:bind], menu_entries: counts[:menu], canvas_item_binds: counts[:canvas_bind], tag_binds: counts[:tag_bind], widget_option_callbacks: counts[:widget_option], window_close_handlers: counts[:wm_protocol], }.reject { |_, count| count.zero? } end |
#emit(event, *args, **kwargs) ⇒ void
This method returns an undefined value.
181 182 183 |
# File 'lib/teek/ui/session.rb', line 181 def emit(event, *args, **kwargs) @bus.emit(event, *args, **kwargs) end |
#every(ms, on_error: :raise, &block) ⇒ Object?
Same queue-then-wire shape as an on_* event binding: called
inside the build block, it queues and registers once the tree
realizes; called after, it registers immediately - same method,
correct behavior either way, so a tick loop can be declared right
alongside the UI it drives instead of being forced out to a
separate post-run_async step.
201 202 203 204 205 206 207 208 |
# File 'lib/teek/ui/session.rb', line 201 def every(ms, on_error: :raise, &block) if @app @app.every(ms, on_error: on_error, &block) else @timers << Timer.new(kind: :every, ms: ms, on_error: on_error, block: block) nil end end |
#find_by_path(path) ⇒ Handle?
Reverse lookup: given a real Tk path (from an error message, a
winfo query, or poking around in a REPL), find which widget it
belongs to - the counterpart to the name-based ui[:name]. See
Document#find_by_path for exactly what counts as a match.
97 98 99 100 101 |
# File 'lib/teek/ui/session.rb', line 97 def find_by_path(path) raise_unless_realized! node = @document.find_by_path(path) node && Handle.new(node) end |
#message(message:, title: nil, detail: nil, icon: :info, type: :ok, default: nil, parent: nil) ⇒ Object
Show a message box with one or more buttons.
244 245 246 247 248 |
# File 'lib/teek/ui/session.rb', line 244 def (message:, title: nil, detail: nil, icon: :info, type: :ok, default: nil, parent: nil) raise_unless_realized! @app.(message: , title: title, detail: detail, icon: icon, type: type, default: default, parent: parent) end |
#off(event, block) ⇒ void
This method returns an undefined value.
187 188 189 |
# File 'lib/teek/ui/session.rb', line 187 def off(event, block) @bus.off(event, block) end |
#on(event, &block) ⇒ Proc
Returns the block, to pass to a later #off.
175 176 177 |
# File 'lib/teek/ui/session.rb', line 175 def on(event, &block) @bus.on(event, &block) end |
#open_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, multiple: false, parent: nil) ⇒ Object
Show the native "choose file to open" dialog.
225 226 227 228 229 |
# File 'lib/teek/ui/session.rb', line 225 def open_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, multiple: false, parent: nil) raise_unless_realized! @app.choose_open_file(filetypes: filetypes, initialdir: initialdir, initialfile: initialfile, title: title, multiple: multiple, parent: parent) end |
#realize(strict: false) ⇒ Teek::App
Validate the build tree, then create the underlying App and realize the tree into it, if that hasn't happened yet. Idempotent - calling it again after the first time just returns the same app.
Atomic in two senses: a validation failure means no interpreter is ever constructed at all, and even once realizing starts, the app's root window stays withdrawn until the whole tree is realized, so a mid-realize error never leaves a half-built window visible either way. On failure the session is left exactly as if #realize had never been called - it isn't left half-realized (or half-validated).
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/teek/ui/session.rb', line 116 def realize(strict: false) return @app if @app Validator.validate!(@document, strict: strict) app = Teek::App.new(title: @title, **@app_opts) begin # vars/images realize first, so a widget bound/pointed to one # displays correctly (a value, a loaded image) from the moment # it's created instead of starting blank/broken. @vars.each { |v| v.realize(app) } @images.each { |img| img.realize(app) } Realizer.new(app, @document, default_scroll: @scroll).realize flush_timers!(app) rescue app.destroy raise end @app = app end |
#run(strict: false, debug: false) ⇒ void
This method returns an undefined value.
Realize, show the window, and enter the Tk event loop. Blocks until the app exits.
145 146 147 148 149 150 151 |
# File 'lib/teek/ui/session.rb', line 145 def run(strict: false, debug: false) realize(strict: strict) @app.show print_debug_info if debug @app.mainloop print_debug_info if debug end |
#run_async(strict: false, debug: false) ⇒ self
this does not (yet) service the event loop automatically between
REPL prompts - call ui.app.update yourself to process pending
events while exploring interactively, the same manual-pump workaround
App#mainloop's own REPL warning documents. A REPL session
helper that services the loop for you on its own is future work,
not built yet.
Realize and show the window without entering the event loop, for interactive/REPL use. Returns immediately.
166 167 168 169 170 171 |
# File 'lib/teek/ui/session.rb', line 166 def run_async(strict: false, debug: false) realize(strict: strict) @app.show print_debug_info if debug self end |
#save_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil) ⇒ Object
Show the native "choose file to save" dialog.
234 235 236 237 238 239 |
# File 'lib/teek/ui/session.rb', line 234 def save_file(filetypes: nil, initialdir: nil, initialfile: nil, title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil) raise_unless_realized! @app.choose_save_file(filetypes: filetypes, initialdir: initialdir, initialfile: initialfile, title: title, defaultextension: defaultextension, confirmoverwrite: confirmoverwrite, parent: parent) end |
#toast(message, duration: DEFAULT_TOAST_DURATION_MS) ⇒ void
This method returns an undefined value.
Briefly flash a message near the bottom of the window - "Saved" after a save action, "Settings" when a modal gains focus, that kind of transient feedback, not a persistent status. Reuses one widget across every call rather than building a new one each time: calling this again while a toast is already showing replaces it (new text, restarted timer) instead of stacking a second one, and the earlier toast's own pending auto-dismiss is cancelled so it can never fire late and hide the replacement.
303 304 305 306 307 308 309 310 |
# File 'lib/teek/ui/session.rb', line 303 def toast(, duration: DEFAULT_TOAST_DURATION_MS) raise_unless_realized! @app.command(@toast_path, :configure, text: ) @app.command(:place, @toast_path, in: '.', relx: 0.5, rely: 1.0, anchor: 's', y: -12) @app.after_cancel(@toast_timer_id) if @toast_timer_id @toast_timer_id = @app.after(duration) { @app.command(:place, :forget, @toast_path) } end |