teek 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +27 -1
- data/Rakefile +40 -1
- data/lib/teek/callback_registry.rb +17 -0
- data/lib/teek/canvas_bind_interceptor.rb +9 -9
- data/lib/teek/clipboard.rb +48 -0
- data/lib/teek/dialogs.rb +20 -0
- data/lib/teek/repeating_timer.rb +114 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +32 -3
- data/lib/teek/window.rb +176 -0
- data/lib/teek/wm.rb +16 -22
- data/lib/teek.rb +73 -132
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f75bdcd33165c47419853eabf3d2393f2b576648647f7ad17efef30df2da023
|
|
4
|
+
data.tar.gz: 636773f5d6a743dd2ef94a3365afd4647878cc0a4ef7842627a0d5325f6f5329
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 892af65f50c58578001bd8a274b96ff2663630489c904218a066d049caa80a49a1bd5de6012d5a4333178e030f72fb55c7001396c39fcaa851868c6f2401b415
|
|
7
|
+
data.tar.gz: 661bb18457269fe174470e26f16fd070404dcb3f9c70740ec5d543b501365dd0733313bd2ff803fb7108285583f86b42da98153c65a3edf77d681ddf15e1b663
|
data/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Teek
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ruby apps for the desktop, built on Tcl/Tk.
|
|
4
|
+
|
|
5
|
+
**Building an app?** Start with [teek-ui](teek-ui/README.md) — a DSL for declaring widgets, layout, and events instead of hand-writing Tk mechanics:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
gem install teek-ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That pulls in `teek` (below) transitively. Think Rails and Rack: you write against teek-ui, and teek is the engine underneath it.
|
|
12
|
+
|
|
13
|
+
This repo/gem is **teek** itself — the low-level Ruby↔Tcl/Tk binding teek-ui compiles down to. It's the right layer to reach for directly if you're embedding Tk in an existing app, building your own abstraction on top, or want direct control over widget mechanics — but most app authors want teek-ui instead.
|
|
4
14
|
|
|
5
15
|
[API Documentation](https://jamescook.github.io/teek/)
|
|
6
16
|
|
|
@@ -294,6 +304,22 @@ Dropped files arrive as a Tcl list in the `:data` substitution. Use `split_list`
|
|
|
294
304
|
|
|
295
305
|
See [`sample/drop_demo.rb`](sample/drop_demo.rb) for a complete example.
|
|
296
306
|
|
|
307
|
+
## Multiple Interpreters
|
|
308
|
+
|
|
309
|
+
Teek can create more than one Tcl/Tk interpreter in a process — each `Teek::App.new` is a fully independent interpreter with its own widget tree, callbacks, and `.` main window — and the low-level `Interp#create_slave(name, safe)` primitive wraps Tcl's master/slave (child) interpreters. **In practice you almost never want either.** Two hard constraints make multi-interpreter setups more trouble than they solve:
|
|
310
|
+
|
|
311
|
+
- **The event loop is singular.** Tcl's notifier and event queue are per *thread*, not per interpreter. One `mainloop` drives every interpreter on that thread and exits only when the last window anywhere closes; multiple `mainloop` calls do not compose. Multi-interpreter buys you no concurrency — the GUI still runs on one thread.
|
|
312
|
+
- **GUI is main-thread-only on macOS.** Tk on Aqua sits on Cocoa, which requires the main thread. You can run several interpreters *on the main thread*, but you cannot own Tk windows or run a Tk event loop on a background thread on macOS. (X11 is more permissive, but code that leans on that won't port.)
|
|
313
|
+
|
|
314
|
+
So teek's stance is a hard lean **against** multiple interpreters. Reach for what you actually need instead:
|
|
315
|
+
|
|
316
|
+
- **More windows** → use `toplevel`, not more interpreters. One interpreter manages any number of top-level windows.
|
|
317
|
+
- **Concurrency / a responsive UI during heavy work** → use Background Work (Ractors) plus `after`/`after_idle`, which marshal results back to the main thread. See [Background Work](#background-work).
|
|
318
|
+
- **Isolating state or widget names** → use Tcl namespaces and a widget-path naming convention within a single interpreter.
|
|
319
|
+
- **Running untrusted Tcl** → this is the one legitimate reason for a separate (safe) interpreter. The `Interp#create_slave` primitive exists, but a safe slave has **no Tk** by default and isn't wired into the `App`/widget layer — treat GUI-in-a-sandbox as advanced, unsupported territory for now.
|
|
320
|
+
|
|
321
|
+
If you do run multiple `Teek::App`s, keep them all on the main thread and let a single one own the `mainloop`.
|
|
322
|
+
|
|
297
323
|
## Known Issues
|
|
298
324
|
|
|
299
325
|
- **File drop on Linux/Wayland** — `register_drop_target` does not yet work under Wayland. The current implementation uses the X11 XDND protocol, which is not compatible with Wayland's native drag-and-drop. Workaround: select an Xorg/X11 session at the login screen (e.g., "GNOME on Xorg"). Native Wayland support via `wl_data_device` is planned.
|
data/Rakefile
CHANGED
|
@@ -293,6 +293,15 @@ namespace :sdl2 do
|
|
|
293
293
|
task test: 'sdl2:compile'
|
|
294
294
|
end
|
|
295
295
|
|
|
296
|
+
namespace :ui do
|
|
297
|
+
Rake::TestTask.new(:test) do |t|
|
|
298
|
+
t.libs << 'teek-ui/test' << 'teek-ui/lib'
|
|
299
|
+
t.test_files = FileList['teek-ui/test/**/test_*.rb'] - FileList['teek-ui/test/test_helper.rb']
|
|
300
|
+
t.ruby_opts << '-r test_helper'
|
|
301
|
+
t.verbose = true
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
296
305
|
task :default => :compile
|
|
297
306
|
|
|
298
307
|
namespace :release do
|
|
@@ -510,6 +519,31 @@ namespace :docker do
|
|
|
510
519
|
sh cmd
|
|
511
520
|
end
|
|
512
521
|
|
|
522
|
+
desc "Run teek-ui tests in Docker"
|
|
523
|
+
task ui: :build do
|
|
524
|
+
tcl_version = tcl_version_from_env
|
|
525
|
+
ruby_version = ruby_version_from_env
|
|
526
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
527
|
+
|
|
528
|
+
require 'fileutils'
|
|
529
|
+
FileUtils.mkdir_p('coverage')
|
|
530
|
+
|
|
531
|
+
warn_if_containers_running(image_name)
|
|
532
|
+
|
|
533
|
+
puts "Running teek-ui tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
534
|
+
cmd = "docker run --rm --init"
|
|
535
|
+
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
536
|
+
cmd += " -e TCL_VERSION=#{tcl_version}"
|
|
537
|
+
if ENV['COVERAGE'] == '1'
|
|
538
|
+
cmd += " -e COVERAGE=1"
|
|
539
|
+
cmd += " -e COVERAGE_NAME=#{ENV['COVERAGE_NAME'] || 'ui'}"
|
|
540
|
+
end
|
|
541
|
+
cmd += " #{image_name}"
|
|
542
|
+
cmd += " xvfb-run -a bundle exec rake ui:test"
|
|
543
|
+
|
|
544
|
+
sh cmd
|
|
545
|
+
end
|
|
546
|
+
|
|
513
547
|
desc "Run all tests (teek + teek-sdl2) with coverage and generate report"
|
|
514
548
|
task all: 'docker:build' do
|
|
515
549
|
tcl_version = tcl_version_from_env
|
|
@@ -520,7 +554,7 @@ namespace :docker do
|
|
|
520
554
|
FileUtils.rm_rf('coverage')
|
|
521
555
|
FileUtils.mkdir_p('coverage/results')
|
|
522
556
|
|
|
523
|
-
# Run all
|
|
557
|
+
# Run all test suites with coverage enabled and distinct COVERAGE_NAMEs
|
|
524
558
|
ENV['COVERAGE'] = '1'
|
|
525
559
|
|
|
526
560
|
ENV['COVERAGE_NAME'] = 'main'
|
|
@@ -531,6 +565,11 @@ namespace :docker do
|
|
|
531
565
|
Rake::Task['docker:build'].reenable
|
|
532
566
|
Rake::Task['docker:test:sdl2'].invoke
|
|
533
567
|
|
|
568
|
+
ENV['COVERAGE_NAME'] = 'ui'
|
|
569
|
+
Rake::Task['docker:test:ui'].reenable
|
|
570
|
+
Rake::Task['docker:build'].reenable
|
|
571
|
+
Rake::Task['docker:test:ui'].invoke
|
|
572
|
+
|
|
534
573
|
# Collate inside Docker (paths match /app/lib/...)
|
|
535
574
|
puts "Collating coverage results..."
|
|
536
575
|
cmd = "docker run --rm --init"
|
|
@@ -70,6 +70,23 @@ module Teek
|
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# Diagnostic aggregate, not itself load-bearing for cleanup - a live
|
|
74
|
+
# snapshot of how many tracked callback ids currently exist, grouped
|
|
75
|
+
# by the tag every {#reconcile} container is already keyed on
|
|
76
|
+
# (+:bind+, +:menu+, +:canvas_bind+, +:tag_bind+, +:widget_option+,
|
|
77
|
+
# +:wm_protocol+, ...). Counts individual ids, not containers - a
|
|
78
|
+
# single container can hold several (e.g. one widget bound to
|
|
79
|
+
# several events). A tag with nothing currently tracked under it is
|
|
80
|
+
# simply absent from the result, not present with a zero count.
|
|
81
|
+
# @return [Hash{Object => Integer}]
|
|
82
|
+
def counts_by_tag
|
|
83
|
+
counts = Hash.new(0)
|
|
84
|
+
@entries.each do |(tag, _path), ids|
|
|
85
|
+
counts[tag] += ids.size unless ids.empty?
|
|
86
|
+
end
|
|
87
|
+
counts
|
|
88
|
+
end
|
|
89
|
+
|
|
73
90
|
private
|
|
74
91
|
|
|
75
92
|
# Convention, not a type check: every container is [feature_tag, path].
|
|
@@ -51,15 +51,15 @@ module Teek
|
|
|
51
51
|
rescue Teek::TclError
|
|
52
52
|
''
|
|
53
53
|
end
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
# canvas-bind-shaped app.command
|
|
58
|
-
# App#bind uses
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
after[[tag_or_id, seq]] = Regexp.last_match(1) if current =~ /\Aruby_callback (\S+)
|
|
54
|
+
# Matches a bare `ruby_callback <id>` or one followed by
|
|
55
|
+
# %-substitution codes (`ruby_callback <id> %x %y`) -
|
|
56
|
+
# raw_command's generic positional-Proc handling allows a caller
|
|
57
|
+
# to pass %-substitutions to a canvas-bind-shaped app.command
|
|
58
|
+
# call (the same mechanism App#bind uses; CanvasItem#on_drag and
|
|
59
|
+
# #on_right_click's menu form both do). `\S+` is greedy and stops
|
|
60
|
+
# at the first space, so it can't overrun into the substitution
|
|
61
|
+
# codes themselves.
|
|
62
|
+
after[[tag_or_id, seq]] = Regexp.last_match(1) if current =~ /\Aruby_callback (\S+)(?:\s|\z)/
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# Thin, typed wrapper around Tk's `clipboard` command family, reached
|
|
5
|
+
# via {App#clipboard}.
|
|
6
|
+
#
|
|
7
|
+
# Doesn't touch text widgets' own copy/cut/paste at all - `ttk::entry`/
|
|
8
|
+
# `text` already bind `<<Copy>>`/`<<Cut>>`/`<<Paste>>` to the expected
|
|
9
|
+
# platform keys (Control-c/x/v, plus their Command-key equivalents on
|
|
10
|
+
# macOS) via Tk's own built-in class bindings, with nothing for teek to
|
|
11
|
+
# wire up. This class is purely for reading/writing the clipboard
|
|
12
|
+
# directly from app code (e.g. a "Copy to Clipboard" button that isn't
|
|
13
|
+
# itself a text widget's own selection).
|
|
14
|
+
#
|
|
15
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/clipboard.htm clipboard
|
|
16
|
+
class Clipboard
|
|
17
|
+
# @api private
|
|
18
|
+
def initialize(app)
|
|
19
|
+
@app = app
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Replace the clipboard's contents outright - Tk's own `clipboard
|
|
23
|
+
# clear` followed by `clipboard append` two-step, done as one call.
|
|
24
|
+
# @param text [String]
|
|
25
|
+
# @return [void]
|
|
26
|
+
def set(text)
|
|
27
|
+
@app.tcl_invoke('clipboard', 'clear')
|
|
28
|
+
@app.tcl_invoke('clipboard', 'append', '--', text.to_s)
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String, nil] the clipboard's current text, or +nil+ if
|
|
33
|
+
# it's empty/has no owner (Tk raises a TclError for this rather
|
|
34
|
+
# than returning an empty string)
|
|
35
|
+
def get
|
|
36
|
+
@app.tcl_invoke('clipboard', 'get')
|
|
37
|
+
rescue Teek::TclError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Clear the clipboard without setting new contents.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def clear
|
|
44
|
+
@app.tcl_invoke('clipboard', 'clear')
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/teek/dialogs.rb
CHANGED
|
@@ -106,6 +106,26 @@ module Teek
|
|
|
106
106
|
result.empty? ? nil : result
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Show the native "choose directory" dialog.
|
|
110
|
+
#
|
|
111
|
+
# @param initialdir [String, nil] directory the dialog starts in
|
|
112
|
+
# @param mustexist [Boolean] restrict the choice to an already-existing
|
|
113
|
+
# directory (Tk's own default is false, allowing a not-yet-created one)
|
|
114
|
+
# @param title [String, nil] dialog window title
|
|
115
|
+
# @param parent [String, nil] parent window (defaults to the root window)
|
|
116
|
+
# @return [String, nil] the chosen directory path, or +nil+ if cancelled
|
|
117
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/chooseDirectory.htm tk_chooseDirectory
|
|
118
|
+
def choose_dir(initialdir: nil, mustexist: false, title: nil, parent: nil)
|
|
119
|
+
args = ['tk_chooseDirectory']
|
|
120
|
+
args.push('-initialdir', initialdir) if initialdir
|
|
121
|
+
args.push('-mustexist', bool_to_tcl(true)) if mustexist
|
|
122
|
+
args.push('-title', title) if title
|
|
123
|
+
args.push('-parent', parent) if parent
|
|
124
|
+
|
|
125
|
+
result = tcl_invoke(*args)
|
|
126
|
+
result.empty? ? nil : result
|
|
127
|
+
end
|
|
128
|
+
|
|
109
129
|
# Pop up a menu at the given screen coordinates.
|
|
110
130
|
#
|
|
111
131
|
# @param menu [Widget, String] the menu to pop up
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# A cancellable repeating timer that fires on the main thread.
|
|
5
|
+
#
|
|
6
|
+
# Created via {App#every}. Reschedules itself after each tick using
|
|
7
|
+
# Tcl's +after+ command. The block runs in the event loop, so it
|
|
8
|
+
# must complete quickly to avoid blocking the UI.
|
|
9
|
+
#
|
|
10
|
+
# Tracks timing drift: if a tick fires significantly late (more than
|
|
11
|
+
# 2x the interval), a warning is printed to stderr. This helps catch
|
|
12
|
+
# blocks that are too slow for the requested interval.
|
|
13
|
+
#
|
|
14
|
+
# @see App#every
|
|
15
|
+
class RepeatingTimer
|
|
16
|
+
# @return [Integer] interval in milliseconds
|
|
17
|
+
attr_reader :interval
|
|
18
|
+
|
|
19
|
+
# @return [Exception, nil] the last error if the timer stopped due to an
|
|
20
|
+
# unhandled exception, nil otherwise
|
|
21
|
+
attr_reader :last_error
|
|
22
|
+
|
|
23
|
+
# @return [Integer] number of ticks that fired late (> 2x interval)
|
|
24
|
+
attr_reader :late_ticks
|
|
25
|
+
|
|
26
|
+
# @api private
|
|
27
|
+
def initialize(app, ms, on_error: nil, &block)
|
|
28
|
+
raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
|
|
29
|
+
|
|
30
|
+
@app = app
|
|
31
|
+
@interval = ms
|
|
32
|
+
@block = block
|
|
33
|
+
@on_error = on_error
|
|
34
|
+
@cancelled = false
|
|
35
|
+
@after_id = nil
|
|
36
|
+
@last_error = nil
|
|
37
|
+
@late_ticks = 0
|
|
38
|
+
@next_expected = nil
|
|
39
|
+
schedule
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Stop the timer. Safe to call multiple times.
|
|
43
|
+
# @return [void]
|
|
44
|
+
def cancel
|
|
45
|
+
return if @cancelled
|
|
46
|
+
@cancelled = true
|
|
47
|
+
@app.after_cancel(@after_id) if @after_id
|
|
48
|
+
@after_id = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Boolean] true if the timer has been cancelled
|
|
52
|
+
def cancelled?
|
|
53
|
+
@cancelled
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Change the interval. Takes effect on the next tick.
|
|
57
|
+
# @param ms [Integer] new interval in milliseconds
|
|
58
|
+
def interval=(ms)
|
|
59
|
+
raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
|
|
60
|
+
@interval = ms
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def schedule
|
|
66
|
+
return if @cancelled
|
|
67
|
+
@next_expected = now_ms + @interval
|
|
68
|
+
@after_id = @app.after(@interval) { tick }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tick
|
|
72
|
+
return if @cancelled
|
|
73
|
+
check_drift
|
|
74
|
+
@block.call
|
|
75
|
+
schedule
|
|
76
|
+
rescue => e
|
|
77
|
+
@last_error = e
|
|
78
|
+
case @on_error
|
|
79
|
+
when :raise
|
|
80
|
+
@cancelled = true
|
|
81
|
+
# Store on App so it raises from the next app.update call.
|
|
82
|
+
# Don't re-raise here — that would go through rb_protect → bgerror.
|
|
83
|
+
@app._pending_exception = e
|
|
84
|
+
when Proc
|
|
85
|
+
begin
|
|
86
|
+
@on_error.call(e)
|
|
87
|
+
rescue => handler_err
|
|
88
|
+
@last_error = handler_err
|
|
89
|
+
@cancelled = true
|
|
90
|
+
@app._pending_exception = handler_err
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
schedule
|
|
94
|
+
when nil
|
|
95
|
+
@cancelled = true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def check_drift
|
|
100
|
+
return unless @next_expected
|
|
101
|
+
actual = now_ms
|
|
102
|
+
drift = actual - @next_expected
|
|
103
|
+
if drift > @interval
|
|
104
|
+
@late_ticks += 1
|
|
105
|
+
warn "Teek::RepeatingTimer: tick #{@late_ticks} fired #{drift.round}ms late " \
|
|
106
|
+
"(interval=#{@interval}ms)"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def now_ms
|
|
111
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/teek/version.rb
CHANGED
data/lib/teek/widget.rb
CHANGED
|
@@ -102,13 +102,42 @@ module Teek
|
|
|
102
102
|
@app.unbind(@path, event)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# This widget as a {Window} - the window-scoped counterpart covering
|
|
106
|
+
# `wm` subcommands and composite behaviors (on_close, grab_set/
|
|
107
|
+
# grab_release, modal). Meant for toplevels.
|
|
108
|
+
# @return [Window]
|
|
109
|
+
def window
|
|
110
|
+
@app.window(@path)
|
|
111
|
+
end
|
|
112
|
+
|
|
105
113
|
# Register a handler for this window's close button (WM_DELETE_WINDOW).
|
|
106
|
-
# Meant for toplevels; see {
|
|
114
|
+
# Meant for toplevels; see {Window#on_close} for the full behavior.
|
|
107
115
|
# @yield called when the window's close button is pressed
|
|
108
116
|
# @return [void]
|
|
109
|
-
# @see
|
|
117
|
+
# @see Window#on_close
|
|
110
118
|
def on_close(&block)
|
|
111
|
-
|
|
119
|
+
window.on_close(&block)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Grab input on this window. See {Window#grab_set}.
|
|
123
|
+
# @param global [Boolean]
|
|
124
|
+
# @return [void]
|
|
125
|
+
def grab_set(global: false)
|
|
126
|
+
window.grab_set(global: global)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Release a grab previously set on this window. See {Window#grab_release}.
|
|
130
|
+
# @return [void]
|
|
131
|
+
def grab_release
|
|
132
|
+
window.grab_release
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Make this window modal. See {Window#modal}.
|
|
136
|
+
# @param global [Boolean]
|
|
137
|
+
# @yield optional - runs with the grab and focus already set
|
|
138
|
+
# @return [void]
|
|
139
|
+
def modal(global: false, &block)
|
|
140
|
+
window.modal(global: global, &block)
|
|
112
141
|
end
|
|
113
142
|
|
|
114
143
|
def inspect
|
data/lib/teek/window.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# A single toplevel window, addressed by its Tk path - the window-scoped
|
|
5
|
+
# counterpart to {Widget} (which covers any widget of any type). Groups
|
|
6
|
+
# every `wm` subcommand alongside composite window-lifecycle behaviors
|
|
7
|
+
# (on_close, grab_set/grab_release, modal) that would otherwise keep
|
|
8
|
+
# growing as more and more +window:+-kwarg methods flattened onto {App}
|
|
9
|
+
# - `app.window(path).thing` reads better than threading `window:`
|
|
10
|
+
# through a dozen unrelated top-level methods, and gives callers (like
|
|
11
|
+
# teek-ui's DSL) one coherent object per window instead of loose parts.
|
|
12
|
+
#
|
|
13
|
+
# `App#wm` ({Wm}) and App's own `window_title`/`set_window_title`/etc.
|
|
14
|
+
# convenience methods, plus `App#on_close`/`#grab_set`/`#grab_release`/
|
|
15
|
+
# `#modal`, all delegate here internally - nothing about those public
|
|
16
|
+
# methods changed, this is where their actual work happens now.
|
|
17
|
+
#
|
|
18
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/wm.htm wm
|
|
19
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/grab.htm grab
|
|
20
|
+
class Window
|
|
21
|
+
attr_reader :path
|
|
22
|
+
|
|
23
|
+
# @api private
|
|
24
|
+
def initialize(app, path)
|
|
25
|
+
@app = app
|
|
26
|
+
@path = path.to_s
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String] the Tk window path
|
|
30
|
+
def to_s
|
|
31
|
+
@path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# -- wm subcommands --
|
|
35
|
+
|
|
36
|
+
# @return [String] the window's current title
|
|
37
|
+
def title
|
|
38
|
+
@app.tcl_invoke('wm', 'title', @path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param value [String] new title
|
|
42
|
+
# @return [String] the title
|
|
43
|
+
def set_title(value)
|
|
44
|
+
@app.tcl_invoke('wm', 'title', @path, value.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String] geometry string (e.g. +"400x300+0+0"+)
|
|
48
|
+
def geometry
|
|
49
|
+
@app.tcl_invoke('wm', 'geometry', @path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param value [String] new geometry (e.g. +"400x300"+, +"400x300+100+50"+)
|
|
53
|
+
# @return [String] the geometry
|
|
54
|
+
def set_geometry(value)
|
|
55
|
+
@app.tcl_invoke('wm', 'geometry', @path, value.to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
|
|
59
|
+
def resizable
|
|
60
|
+
parts = @app.tcl_invoke('wm', 'resizable', @path).split
|
|
61
|
+
[@app.tcl_to_bool(parts[0]), @app.tcl_to_bool(parts[1])]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param width [Boolean] allow horizontal resize
|
|
65
|
+
# @param height [Boolean] allow vertical resize
|
|
66
|
+
# @return [void]
|
|
67
|
+
def set_resizable(width, height)
|
|
68
|
+
@app.tcl_invoke('wm', 'resizable', @path, @app.bool_to_tcl(width), @app.bool_to_tcl(height))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Show the window (map it if withdrawn/iconified).
|
|
72
|
+
# @return [void]
|
|
73
|
+
def deiconify
|
|
74
|
+
@app.tcl_invoke('wm', 'deiconify', @path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Hide the window without destroying it.
|
|
78
|
+
# @return [void]
|
|
79
|
+
def withdraw
|
|
80
|
+
@app.tcl_invoke('wm', 'withdraw', @path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# -- composite behaviors --
|
|
84
|
+
|
|
85
|
+
# Register a handler for the window manager's close button
|
|
86
|
+
# (WM_DELETE_WINDOW - the titlebar close box, Cmd-W, Alt-F4, etc.,
|
|
87
|
+
# depending on platform).
|
|
88
|
+
#
|
|
89
|
+
# Tk's own default behavior (destroy the window) only applies when
|
|
90
|
+
# nothing else has claimed this protocol - setting a handler here
|
|
91
|
+
# replaces it, so the block is entirely responsible for deciding
|
|
92
|
+
# whether the window actually closes. Call {App#destroy} yourself if
|
|
93
|
+
# you want it to; do nothing (or show a confirmation first) if you don't.
|
|
94
|
+
#
|
|
95
|
+
# @example Confirm before quitting
|
|
96
|
+
# app.window.on_close { app.destroy('.') if app.message_box(message: 'Quit?', type: :yesno) == :yes }
|
|
97
|
+
# @example A toplevel that just hides instead of closing
|
|
98
|
+
# app.window(settings_window).on_close { app.window(settings_window).withdraw }
|
|
99
|
+
#
|
|
100
|
+
# @yield called when the window's close button is pressed
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @see App#bind
|
|
103
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/wm.htm#M46 wm protocol
|
|
104
|
+
def on_close(&block)
|
|
105
|
+
cb = @app.register_callback(block, relay_break_continue: false)
|
|
106
|
+
@app.callback_registry.reconcile([:wm_protocol, @path]) { |before| before.merge('WM_DELETE_WINDOW' => cb) }
|
|
107
|
+
@app.tcl_eval("wm protocol #{@path} WM_DELETE_WINDOW {ruby_callback #{cb}}")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Set the input grab on the window - while held, mouse and keyboard
|
|
111
|
+
# events outside it (and its descendants) are redirected to it, the
|
|
112
|
+
# building block {#modal} uses. `grab` is its own Tcl command family,
|
|
113
|
+
# separate from `wm`.
|
|
114
|
+
# @param global [Boolean] a global grab blocks input to every other
|
|
115
|
+
# application too, not just this one - almost never what you want;
|
|
116
|
+
# local (the default) is scoped to this application.
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @see #grab_release
|
|
119
|
+
# @see #modal
|
|
120
|
+
def grab_set(global: false)
|
|
121
|
+
args = ['grab', 'set']
|
|
122
|
+
args << '-global' if global
|
|
123
|
+
args << @path
|
|
124
|
+
@app.tcl_invoke(*args)
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Release a grab previously set with {#grab_set}. Safe to call even if
|
|
129
|
+
# the window never held the grab - Tk itself treats that as a no-op.
|
|
130
|
+
# @return [void]
|
|
131
|
+
def grab_release
|
|
132
|
+
@app.tcl_invoke('grab', 'release', @path)
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Make the window modal: grabs input and sets focus on it immediately.
|
|
137
|
+
# Release it explicitly with {#grab_release} (typically from the
|
|
138
|
+
# window's own dismiss/close handling) when the dialog is done - the
|
|
139
|
+
# grab is NOT released automatically just because this method returns,
|
|
140
|
+
# since a modal dialog is meant to stay grabbed for its whole visible
|
|
141
|
+
# lifetime, not just its setup.
|
|
142
|
+
#
|
|
143
|
+
# Two safety nets guard against a stuck grab locking out the rest of
|
|
144
|
+
# the display: if the window is destroyed while still grabbed (a crash
|
|
145
|
+
# mid-modal, or just forgetting to call {#grab_release} first), a
|
|
146
|
+
# <Destroy> binding releases it; if the optional setup block itself
|
|
147
|
+
# raises, the grab is released immediately rather than left dangling
|
|
148
|
+
# on a half-shown dialog.
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
# settings = app.window(settings_path)
|
|
152
|
+
# settings.modal { settings.deiconify }
|
|
153
|
+
# # ... later, from the window's own on_close/dismiss handler ...
|
|
154
|
+
# settings.grab_release
|
|
155
|
+
#
|
|
156
|
+
# @param global [Boolean] see {#grab_set}
|
|
157
|
+
# @yield optional - runs with the grab and focus already set
|
|
158
|
+
# @return [void]
|
|
159
|
+
# @see #grab_set
|
|
160
|
+
# @see #grab_release
|
|
161
|
+
def modal(global: false)
|
|
162
|
+
grab_set(global: global)
|
|
163
|
+
# -force: a modal dialog should own keyboard focus immediately, not
|
|
164
|
+
# merely be first in line whenever the app next happens to get it
|
|
165
|
+
# (plain `focus` only takes effect once the app already has input
|
|
166
|
+
# focus at the OS/WM level).
|
|
167
|
+
@app.tcl_invoke('focus', '-force', @path)
|
|
168
|
+
@app.bind(@path, 'Destroy') { grab_release }
|
|
169
|
+
yield if block_given?
|
|
170
|
+
nil
|
|
171
|
+
rescue
|
|
172
|
+
grab_release
|
|
173
|
+
raise
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/teek/wm.rb
CHANGED
|
@@ -3,20 +3,15 @@
|
|
|
3
3
|
module Teek
|
|
4
4
|
# Thin, typed wrapper around Tk's `wm` (window manager) command family -
|
|
5
5
|
# one method per subcommand, coerced to the right Ruby type, reached via
|
|
6
|
-
# {App#wm}.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# directly. App's own +set_window_title+/+window_geometry+/etc. are kept
|
|
12
|
-
# as thin delegates to this - use whichever reads better to you, they're
|
|
13
|
-
# the same underlying call.
|
|
14
|
-
#
|
|
15
|
-
# Composite behaviors that orchestrate more than a single +wm+ subcommand
|
|
16
|
-
# (App#on_close's callback tracking, for instance) stay top-level App/
|
|
17
|
-
# Widget methods rather than living here - this class is only 1:1 Tcl
|
|
18
|
-
# command wrappers, nothing with Ruby-side state of its own.
|
|
6
|
+
# {App#wm}. Kept for callers already using +app.wm.title(window: ...)+ -
|
|
7
|
+
# every method here is a one-line delegate to {Window}, which is where
|
|
8
|
+
# the actual `wm`/`grab` work now lives (see {Window}'s own doc comment
|
|
9
|
+
# for why - the same window-scoped operations kept growing as more and
|
|
10
|
+
# more +window:+-kwarg methods flattened onto {App}).
|
|
19
11
|
#
|
|
12
|
+
# @note Prefer +app.window(path)+ for new code - the same calls, without
|
|
13
|
+
# repeating +window:+ on every one when you're working with one window
|
|
14
|
+
# repeatedly.
|
|
20
15
|
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/wm.htm wm
|
|
21
16
|
class Wm
|
|
22
17
|
# @api private
|
|
@@ -27,34 +22,33 @@ module Teek
|
|
|
27
22
|
# @param window [String, Widget] (default: the root window)
|
|
28
23
|
# @return [String] the window's current title
|
|
29
24
|
def title(window: '.')
|
|
30
|
-
@app.
|
|
25
|
+
@app.window(window).title
|
|
31
26
|
end
|
|
32
27
|
|
|
33
28
|
# @param value [String] new title
|
|
34
29
|
# @param window [String, Widget] (default: the root window)
|
|
35
30
|
# @return [String] the title
|
|
36
31
|
def set_title(value, window: '.')
|
|
37
|
-
@app.
|
|
32
|
+
@app.window(window).set_title(value)
|
|
38
33
|
end
|
|
39
34
|
|
|
40
35
|
# @param window [String, Widget] (default: the root window)
|
|
41
36
|
# @return [String] geometry string (e.g. +"400x300+0+0"+)
|
|
42
37
|
def geometry(window: '.')
|
|
43
|
-
@app.
|
|
38
|
+
@app.window(window).geometry
|
|
44
39
|
end
|
|
45
40
|
|
|
46
41
|
# @param value [String] new geometry (e.g. +"400x300"+, +"400x300+100+50"+)
|
|
47
42
|
# @param window [String, Widget] (default: the root window)
|
|
48
43
|
# @return [String] the geometry
|
|
49
44
|
def set_geometry(value, window: '.')
|
|
50
|
-
@app.
|
|
45
|
+
@app.window(window).set_geometry(value)
|
|
51
46
|
end
|
|
52
47
|
|
|
53
48
|
# @param window [String, Widget] (default: the root window)
|
|
54
49
|
# @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
|
|
55
50
|
def resizable(window: '.')
|
|
56
|
-
|
|
57
|
-
[@app.tcl_to_bool(parts[0]), @app.tcl_to_bool(parts[1])]
|
|
51
|
+
@app.window(window).resizable
|
|
58
52
|
end
|
|
59
53
|
|
|
60
54
|
# @param width [Boolean] allow horizontal resize
|
|
@@ -62,21 +56,21 @@ module Teek
|
|
|
62
56
|
# @param window [String, Widget] (default: the root window)
|
|
63
57
|
# @return [void]
|
|
64
58
|
def set_resizable(width, height, window: '.')
|
|
65
|
-
@app.
|
|
59
|
+
@app.window(window).set_resizable(width, height)
|
|
66
60
|
end
|
|
67
61
|
|
|
68
62
|
# Show a window (map it if withdrawn/iconified).
|
|
69
63
|
# @param window [String, Widget] (default: the root window)
|
|
70
64
|
# @return [void]
|
|
71
65
|
def deiconify(window: '.')
|
|
72
|
-
@app.
|
|
66
|
+
@app.window(window).deiconify
|
|
73
67
|
end
|
|
74
68
|
|
|
75
69
|
# Hide a window without destroying it.
|
|
76
70
|
# @param window [String, Widget] (default: the root window)
|
|
77
71
|
# @return [void]
|
|
78
72
|
def withdraw(window: '.')
|
|
79
|
-
@app.
|
|
73
|
+
@app.window(window).withdraw
|
|
80
74
|
end
|
|
81
75
|
end
|
|
82
76
|
end
|
data/lib/teek.rb
CHANGED
|
@@ -14,6 +14,9 @@ require_relative 'teek/photo'
|
|
|
14
14
|
require_relative 'teek/dialogs'
|
|
15
15
|
require_relative 'teek/winfo'
|
|
16
16
|
require_relative 'teek/wm'
|
|
17
|
+
require_relative 'teek/window'
|
|
18
|
+
require_relative 'teek/clipboard'
|
|
19
|
+
require_relative 'teek/repeating_timer'
|
|
17
20
|
|
|
18
21
|
# Ruby interface to Tcl/Tk. Provides a thin wrapper around a Tcl interpreter
|
|
19
22
|
# with Ruby callbacks, event bindings, and background work support.
|
|
@@ -90,7 +93,7 @@ module Teek
|
|
|
90
93
|
].freeze
|
|
91
94
|
|
|
92
95
|
class App
|
|
93
|
-
attr_reader :interp, :widgets, :debugger, :callback_registry, :winfo, :wm
|
|
96
|
+
attr_reader :interp, :widgets, :debugger, :callback_registry, :winfo, :wm, :clipboard
|
|
94
97
|
attr_writer :_pending_exception # @api private
|
|
95
98
|
|
|
96
99
|
def initialize(title: nil, track_widgets: true, debug: false, &block)
|
|
@@ -98,6 +101,7 @@ module Teek
|
|
|
98
101
|
@interp.tcl_eval('package require Tk')
|
|
99
102
|
@winfo = Teek::Winfo.new(self)
|
|
100
103
|
@wm = Teek::Wm.new(self)
|
|
104
|
+
@clipboard = Teek::Clipboard.new(self)
|
|
101
105
|
hide
|
|
102
106
|
@widgets = {}
|
|
103
107
|
@widget_counters = Hash.new(0)
|
|
@@ -680,19 +684,19 @@ module Teek
|
|
|
680
684
|
# Show a window. Defaults to the root window (".").
|
|
681
685
|
# @param window [String] Tk window path
|
|
682
686
|
# @return [void]
|
|
683
|
-
# @see
|
|
687
|
+
# @see Window#deiconify
|
|
684
688
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M38 wm deiconify
|
|
685
689
|
def show(window = '.')
|
|
686
|
-
|
|
690
|
+
self.window(window).deiconify
|
|
687
691
|
end
|
|
688
692
|
|
|
689
693
|
# Hide a window without destroying it. Defaults to the root window (".").
|
|
690
694
|
# @param window [String] Tk window path
|
|
691
695
|
# @return [void]
|
|
692
|
-
# @see
|
|
696
|
+
# @see Window#withdraw
|
|
693
697
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M65 wm withdraw
|
|
694
698
|
def hide(window = '.')
|
|
695
|
-
|
|
699
|
+
self.window(window).withdraw
|
|
696
700
|
end
|
|
697
701
|
|
|
698
702
|
# Enable the Tk debug console. The console starts hidden and can be
|
|
@@ -737,38 +741,38 @@ module Teek
|
|
|
737
741
|
# @param title [String] new title
|
|
738
742
|
# @param window [String] Tk window path
|
|
739
743
|
# @return [String] the title
|
|
740
|
-
# @see
|
|
744
|
+
# @see Window#set_title
|
|
741
745
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
|
|
742
746
|
def set_window_title(title, window: '.')
|
|
743
|
-
|
|
747
|
+
self.window(window).set_title(title)
|
|
744
748
|
end
|
|
745
749
|
|
|
746
750
|
# Get a window's current title.
|
|
747
751
|
# @param window [String] Tk window path
|
|
748
752
|
# @return [String] current title
|
|
749
|
-
# @see
|
|
753
|
+
# @see Window#title
|
|
750
754
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M63 wm title
|
|
751
755
|
def window_title(window: '.')
|
|
752
|
-
|
|
756
|
+
self.window(window).title
|
|
753
757
|
end
|
|
754
758
|
|
|
755
759
|
# Set a window's geometry (e.g. "400x300", "400x300+100+50").
|
|
756
760
|
# @param geometry [String] geometry string
|
|
757
761
|
# @param window [String] Tk window path
|
|
758
762
|
# @return [String] the geometry
|
|
759
|
-
# @see
|
|
763
|
+
# @see Window#set_geometry
|
|
760
764
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
|
|
761
765
|
def set_window_geometry(geometry, window: '.')
|
|
762
|
-
|
|
766
|
+
self.window(window).set_geometry(geometry)
|
|
763
767
|
end
|
|
764
768
|
|
|
765
769
|
# Get a window's current geometry.
|
|
766
770
|
# @param window [String] Tk window path
|
|
767
771
|
# @return [String] geometry string (e.g. "400x300+0+0")
|
|
768
|
-
# @see
|
|
772
|
+
# @see Window#geometry
|
|
769
773
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M42 wm geometry
|
|
770
774
|
def window_geometry(window: '.')
|
|
771
|
-
|
|
775
|
+
self.window(window).geometry
|
|
772
776
|
end
|
|
773
777
|
|
|
774
778
|
# Set whether a window is resizable.
|
|
@@ -776,19 +780,19 @@ module Teek
|
|
|
776
780
|
# @param height [Boolean] allow vertical resize
|
|
777
781
|
# @param window [String] Tk window path
|
|
778
782
|
# @return [void]
|
|
779
|
-
# @see
|
|
783
|
+
# @see Window#set_resizable
|
|
780
784
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
|
|
781
785
|
def set_window_resizable(width, height, window: '.')
|
|
782
|
-
|
|
786
|
+
self.window(window).set_resizable(width, height)
|
|
783
787
|
end
|
|
784
788
|
|
|
785
789
|
# Get whether a window is resizable.
|
|
786
790
|
# @param window [String] Tk window path
|
|
787
791
|
# @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
|
|
788
|
-
# @see
|
|
792
|
+
# @see Window#resizable
|
|
789
793
|
# @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/wm.htm#M59 wm resizable
|
|
790
794
|
def window_resizable(window: '.')
|
|
791
|
-
|
|
795
|
+
self.window(window).resizable
|
|
792
796
|
end
|
|
793
797
|
|
|
794
798
|
# Bind a Tk event on a widget, with optional substitutions forwarded
|
|
@@ -857,6 +861,20 @@ module Teek
|
|
|
857
861
|
@interp.tcl_eval("bind #{widget} #{event_str} {}")
|
|
858
862
|
end
|
|
859
863
|
|
|
864
|
+
# A single toplevel window, addressed by path - groups `wm` subcommands
|
|
865
|
+
# and composite window-lifecycle behaviors ({#on_close}, {#grab_set}/
|
|
866
|
+
# {#grab_release}, {#modal}) into one object instead of threading
|
|
867
|
+
# +window:+ through a pile of unrelated flat methods. This app's own
|
|
868
|
+
# +window_title+/+set_window_title+/etc., {#wm}, {#on_close},
|
|
869
|
+
# {#grab_set}, {#grab_release}, and {#modal} all delegate here
|
|
870
|
+
# internally - use whichever reads better to you, they're the same
|
|
871
|
+
# underlying calls.
|
|
872
|
+
# @param path [String, Widget] (default: the root window)
|
|
873
|
+
# @return [Window]
|
|
874
|
+
def window(path = '.')
|
|
875
|
+
Window.new(self, path)
|
|
876
|
+
end
|
|
877
|
+
|
|
860
878
|
# Register a handler for the window manager's close button
|
|
861
879
|
# (WM_DELETE_WINDOW - the titlebar close box, Cmd-W, Alt-F4, etc.,
|
|
862
880
|
# depending on platform).
|
|
@@ -875,12 +893,46 @@ module Teek
|
|
|
875
893
|
# @param window [String] Tk window path (default: the root window)
|
|
876
894
|
# @yield called when the window's close button is pressed
|
|
877
895
|
# @return [void]
|
|
878
|
-
# @
|
|
896
|
+
# @note Prefer +app.window(window).on_close { }+ for new code - this
|
|
897
|
+
# flat method is kept for compatibility and just delegates there.
|
|
898
|
+
# @see Window#on_close
|
|
879
899
|
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/wm.htm#M46 wm protocol
|
|
880
900
|
def on_close(window: '.', &block)
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
901
|
+
self.window(window).on_close(&block)
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# Set the input grab on window. See {Window#grab_set}.
|
|
905
|
+
# @param window [String, Widget] (default: the root window)
|
|
906
|
+
# @param global [Boolean]
|
|
907
|
+
# @return [void]
|
|
908
|
+
# @note Prefer +app.window(window).grab_set+ for new code - this flat
|
|
909
|
+
# method is kept for compatibility and just delegates there.
|
|
910
|
+
# @see #grab_release
|
|
911
|
+
# @see #modal
|
|
912
|
+
def grab_set(window: '.', global: false)
|
|
913
|
+
self.window(window).grab_set(global: global)
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Release a grab previously set with {#grab_set}. See {Window#grab_release}.
|
|
917
|
+
# @param window [String, Widget] (default: the root window)
|
|
918
|
+
# @return [void]
|
|
919
|
+
# @note Prefer +app.window(window).grab_release+ for new code - this
|
|
920
|
+
# flat method is kept for compatibility and just delegates there.
|
|
921
|
+
def grab_release(window: '.')
|
|
922
|
+
self.window(window).grab_release
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Make window modal. See {Window#modal}.
|
|
926
|
+
# @param window [String, Widget] (default: the root window)
|
|
927
|
+
# @param global [Boolean] see {#grab_set}
|
|
928
|
+
# @note Prefer +app.window(window).modal { }+ for new code - this flat
|
|
929
|
+
# method is kept for compatibility and just delegates there.
|
|
930
|
+
# @yield optional - runs with the grab and focus already set
|
|
931
|
+
# @return [void]
|
|
932
|
+
# @see #grab_set
|
|
933
|
+
# @see #grab_release
|
|
934
|
+
def modal(window: '.', global: false, &block)
|
|
935
|
+
self.window(window).modal(global: global, &block)
|
|
884
936
|
end
|
|
885
937
|
|
|
886
938
|
# Register a widget as a file drop target.
|
|
@@ -1070,115 +1122,4 @@ module Teek
|
|
|
1070
1122
|
end
|
|
1071
1123
|
end
|
|
1072
1124
|
end
|
|
1073
|
-
|
|
1074
|
-
# A cancellable repeating timer that fires on the main thread.
|
|
1075
|
-
#
|
|
1076
|
-
# Created via {App#every}. Reschedules itself after each tick using
|
|
1077
|
-
# Tcl's +after+ command. The block runs in the event loop, so it
|
|
1078
|
-
# must complete quickly to avoid blocking the UI.
|
|
1079
|
-
#
|
|
1080
|
-
# Tracks timing drift: if a tick fires significantly late (more than
|
|
1081
|
-
# 2x the interval), a warning is printed to stderr. This helps catch
|
|
1082
|
-
# blocks that are too slow for the requested interval.
|
|
1083
|
-
#
|
|
1084
|
-
# @see App#every
|
|
1085
|
-
class RepeatingTimer
|
|
1086
|
-
# @return [Integer] interval in milliseconds
|
|
1087
|
-
attr_reader :interval
|
|
1088
|
-
|
|
1089
|
-
# @return [Exception, nil] the last error if the timer stopped due to an
|
|
1090
|
-
# unhandled exception, nil otherwise
|
|
1091
|
-
attr_reader :last_error
|
|
1092
|
-
|
|
1093
|
-
# @return [Integer] number of ticks that fired late (> 2x interval)
|
|
1094
|
-
attr_reader :late_ticks
|
|
1095
|
-
|
|
1096
|
-
# @api private
|
|
1097
|
-
def initialize(app, ms, on_error: nil, &block)
|
|
1098
|
-
raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
|
|
1099
|
-
|
|
1100
|
-
@app = app
|
|
1101
|
-
@interval = ms
|
|
1102
|
-
@block = block
|
|
1103
|
-
@on_error = on_error
|
|
1104
|
-
@cancelled = false
|
|
1105
|
-
@after_id = nil
|
|
1106
|
-
@last_error = nil
|
|
1107
|
-
@late_ticks = 0
|
|
1108
|
-
@next_expected = nil
|
|
1109
|
-
schedule
|
|
1110
|
-
end
|
|
1111
|
-
|
|
1112
|
-
# Stop the timer. Safe to call multiple times.
|
|
1113
|
-
# @return [void]
|
|
1114
|
-
def cancel
|
|
1115
|
-
return if @cancelled
|
|
1116
|
-
@cancelled = true
|
|
1117
|
-
@app.after_cancel(@after_id) if @after_id
|
|
1118
|
-
@after_id = nil
|
|
1119
|
-
end
|
|
1120
|
-
|
|
1121
|
-
# @return [Boolean] true if the timer has been cancelled
|
|
1122
|
-
def cancelled?
|
|
1123
|
-
@cancelled
|
|
1124
|
-
end
|
|
1125
|
-
|
|
1126
|
-
# Change the interval. Takes effect on the next tick.
|
|
1127
|
-
# @param ms [Integer] new interval in milliseconds
|
|
1128
|
-
def interval=(ms)
|
|
1129
|
-
raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
|
|
1130
|
-
@interval = ms
|
|
1131
|
-
end
|
|
1132
|
-
|
|
1133
|
-
private
|
|
1134
|
-
|
|
1135
|
-
def schedule
|
|
1136
|
-
return if @cancelled
|
|
1137
|
-
@next_expected = now_ms + @interval
|
|
1138
|
-
@after_id = @app.after(@interval) { tick }
|
|
1139
|
-
end
|
|
1140
|
-
|
|
1141
|
-
def tick
|
|
1142
|
-
return if @cancelled
|
|
1143
|
-
check_drift
|
|
1144
|
-
@block.call
|
|
1145
|
-
schedule
|
|
1146
|
-
rescue => e
|
|
1147
|
-
@last_error = e
|
|
1148
|
-
case @on_error
|
|
1149
|
-
when :raise
|
|
1150
|
-
@cancelled = true
|
|
1151
|
-
# Store on App so it raises from the next app.update call.
|
|
1152
|
-
# Don't re-raise here — that would go through rb_protect → bgerror.
|
|
1153
|
-
@app._pending_exception = e
|
|
1154
|
-
when Proc
|
|
1155
|
-
begin
|
|
1156
|
-
@on_error.call(e)
|
|
1157
|
-
rescue => handler_err
|
|
1158
|
-
@last_error = handler_err
|
|
1159
|
-
@cancelled = true
|
|
1160
|
-
@app._pending_exception = handler_err
|
|
1161
|
-
return
|
|
1162
|
-
end
|
|
1163
|
-
schedule
|
|
1164
|
-
when nil
|
|
1165
|
-
@cancelled = true
|
|
1166
|
-
end
|
|
1167
|
-
end
|
|
1168
|
-
|
|
1169
|
-
def check_drift
|
|
1170
|
-
return unless @next_expected
|
|
1171
|
-
actual = now_ms
|
|
1172
|
-
drift = actual - @next_expected
|
|
1173
|
-
if drift > @interval
|
|
1174
|
-
@late_ticks += 1
|
|
1175
|
-
warn "Teek::RepeatingTimer: tick #{@late_ticks} fired #{drift.round}ms late " \
|
|
1176
|
-
"(interval=#{@interval}ms)"
|
|
1177
|
-
end
|
|
1178
|
-
end
|
|
1179
|
-
|
|
1180
|
-
def now_ms
|
|
1181
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
1182
|
-
end
|
|
1183
|
-
end
|
|
1184
1125
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: teek
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Cook
|
|
@@ -139,6 +139,7 @@ files:
|
|
|
139
139
|
- lib/teek/background_thread.rb
|
|
140
140
|
- lib/teek/callback_registry.rb
|
|
141
141
|
- lib/teek/canvas_bind_interceptor.rb
|
|
142
|
+
- lib/teek/clipboard.rb
|
|
142
143
|
- lib/teek/command_interceptors.rb
|
|
143
144
|
- lib/teek/debugger.rb
|
|
144
145
|
- lib/teek/demo_support.rb
|
|
@@ -148,9 +149,11 @@ files:
|
|
|
148
149
|
- lib/teek/photo.rb
|
|
149
150
|
- lib/teek/platform.rb
|
|
150
151
|
- lib/teek/ractor_support.rb
|
|
152
|
+
- lib/teek/repeating_timer.rb
|
|
151
153
|
- lib/teek/tag_bind_interceptor.rb
|
|
152
154
|
- lib/teek/version.rb
|
|
153
155
|
- lib/teek/widget.rb
|
|
156
|
+
- lib/teek/window.rb
|
|
154
157
|
- lib/teek/winfo.rb
|
|
155
158
|
- lib/teek/wm.rb
|
|
156
159
|
- teek.gemspec
|