teek 0.1.5 → 0.2.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 +36 -7
- data/Rakefile +76 -0
- data/ext/teek/tcltkbridge.c +69 -4
- data/ext/teek/tkdrop_x11.c +2 -2
- data/lib/teek/callback_registry.rb +80 -0
- data/lib/teek/canvas_bind_interceptor.rb +68 -0
- data/lib/teek/command_interceptors.rb +47 -0
- data/lib/teek/debugger.rb +7 -10
- data/lib/teek/demo_support.rb +2 -2
- data/lib/teek/dialogs.rb +137 -0
- data/lib/teek/menu_interceptor.rb +61 -0
- data/lib/teek/photo.rb +55 -0
- data/lib/teek/tag_bind_interceptor.rb +66 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek/widget.rb +25 -2
- data/lib/teek/winfo.rb +104 -0
- data/lib/teek/wm.rb +82 -0
- data/lib/teek.rb +317 -57
- data/teek.gemspec +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ff08eb5884f74dc6461c5ff1163a025b44b798e2e480bd4f2855210dd0f0fdf
|
|
4
|
+
data.tar.gz: 8e012ccf6a6fffe427630c033b288f784ed455552b064870b0bce054d11e62ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 878e28a9a7493abdccdce1a4b28e372ade4648b12392cdb0815581b4590ab69b601eceb1da686e01719c6930fe1e88c333d4751e80f3af7875ce84a2a391957c
|
|
7
|
+
data.tar.gz: 0cd4b0ae71353720e185455e3966b954b9b2e2237417ce0efc596ad9915234f6b79923d318a54a1f42ddf9a839e8e58857462017192f750cd1d4ef31ce92710a
|
data/README.md
CHANGED
|
@@ -104,25 +104,54 @@ If a callback raises a Ruby exception, it becomes a Tcl error. The exception mes
|
|
|
104
104
|
|
|
105
105
|
## Menus
|
|
106
106
|
|
|
107
|
-
Build a menu bar with
|
|
107
|
+
Build a menu bar with `App#menu`:
|
|
108
108
|
|
|
109
109
|
```ruby
|
|
110
110
|
app = Teek::App.new(title: 'My App')
|
|
111
111
|
|
|
112
|
-
app.
|
|
113
|
-
app.command('.', :configure, menu:
|
|
112
|
+
menubar = app.menu('.menubar')
|
|
113
|
+
app.command('.', :configure, menu: menubar)
|
|
114
114
|
|
|
115
|
-
app.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
label: 'Quit', command: proc { app.command(:destroy, '.') })
|
|
115
|
+
file_menu = app.menu('.menubar.file')
|
|
116
|
+
menubar.command(:add, :cascade, label: 'File', menu: file_menu)
|
|
117
|
+
file_menu.command(:add, :command, label: 'Quit', command: proc { app.command(:destroy, '.') })
|
|
119
118
|
|
|
120
119
|
app.show
|
|
121
120
|
app.mainloop
|
|
122
121
|
```
|
|
123
122
|
|
|
123
|
+
`app.menu(path)` creates the underlying Tk menu the first time it's called for a given path (tearoff disabled) and just returns a handle to it on later calls, so it's safe to call again whenever you're about to rebuild a menu's entries (e.g. on every right-click). Every entry-mutating call — `add`, `insert`, `entryconfigure`, `delete` — tracks any `command:` callback and releases it when the entry is replaced, deleted, or the menu itself is destroyed. That's true whether you call it through the `Widget` handle (`menubar.command(:add, ...)`) or as a raw `app.command(path, :add, ...)`; both go through the same tracking, so there's no less-safe way to build a menu.
|
|
124
|
+
|
|
124
125
|
> **macOS note:** On macOS, Tk always displays a menu bar. If you don't configure one, Tk shows a default menu with items like "Run Widget Demo" that are meant for the Tcl interpreter shell. Attach a custom menu bar (even an empty one) to suppress it. See the [TkDocs menu tutorial](https://tkdocs.com/tutorial/menus.html) for details.
|
|
125
126
|
|
|
127
|
+
## Dialogs
|
|
128
|
+
|
|
129
|
+
`App` has safe wrappers for the standard Tk dialogs — `choose_open_file`, `choose_save_file`, `message_box`, `choose_color`, and `popup_menu` — built without any string interpolation, so titles, paths, and messages containing spaces or braces are passed through correctly:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
path = app.choose_open_file(
|
|
133
|
+
title: 'Open Image',
|
|
134
|
+
filetypes: [['Images', ['.png', '.jpg']], ['All Files', '*']]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
app.message_box(message: 'Really delete this?', type: :yesno, icon: :warning) # => :yes / :no
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
File/color pickers return `nil` when cancelled (an array of paths if `multiple: true`); `message_box` returns the pressed button as a symbol. See `sample/dialogs/dialogs_demo.rb` for a runnable demo of all five.
|
|
141
|
+
|
|
142
|
+
## Window info
|
|
143
|
+
|
|
144
|
+
`app.winfo` groups typed wrappers for Tk's `winfo` command family — one method per query, coerced to the right Ruby type:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
app.winfo.width(btn) # => 90 (Integer)
|
|
148
|
+
app.winfo.exists?(btn) # => true / false
|
|
149
|
+
app.winfo.class_name(btn) # => "TButton"
|
|
150
|
+
app.winfo.pointerx # => current mouse x, screen pixels
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Every method accepts a path string or anything with a matching `to_s` (a `Widget`, for instance). `Widget` also has `#width`, `#height`, and `#exist?` convenience methods that delegate here for its own path.
|
|
154
|
+
|
|
126
155
|
## List operations
|
|
127
156
|
|
|
128
157
|
Convert between Ruby arrays and Tcl list strings:
|
data/Rakefile
CHANGED
|
@@ -5,6 +5,82 @@ require 'rake/clean'
|
|
|
5
5
|
# Sub-project Rakefiles (define sdl2:compile)
|
|
6
6
|
import 'teek-sdl2/Rakefile'
|
|
7
7
|
|
|
8
|
+
desc 'Run clang-tidy on C code'
|
|
9
|
+
task :lint do
|
|
10
|
+
# Find clang-tidy binary
|
|
11
|
+
clang_tidy = nil
|
|
12
|
+
|
|
13
|
+
# Try system PATH first (Linux, or if user has llvm in PATH)
|
|
14
|
+
if system('which clang-tidy > /dev/null 2>&1')
|
|
15
|
+
clang_tidy = 'clang-tidy'
|
|
16
|
+
# On macOS, check Homebrew LLVM (keg-only, not in PATH by default)
|
|
17
|
+
elsif system('which brew > /dev/null 2>&1')
|
|
18
|
+
llvm_prefix = `brew --prefix llvm 2>/dev/null`.strip
|
|
19
|
+
clang_tidy = "#{llvm_prefix}/bin/clang-tidy" if !llvm_prefix.empty? && File.exist?("#{llvm_prefix}/bin/clang-tidy")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless clang_tidy
|
|
23
|
+
abort("clang-tidy not installed.\n " \
|
|
24
|
+
"macOS: brew install llvm\n " \
|
|
25
|
+
"Ubuntu/Debian: apt-get install clang-tidy\n " \
|
|
26
|
+
'Fedora/RHEL: dnf install clang-tools-extra')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
puts 'Running clang-tidy on C code...'
|
|
30
|
+
|
|
31
|
+
# Find all .c files in ext/cataract/ and ext/cataract_color/
|
|
32
|
+
c_files = Dir.glob('ext/teek/**/*.c') + Dir.glob('teek-sdl2/ext/**/*.c')
|
|
33
|
+
|
|
34
|
+
# Run clang-tidy on each file
|
|
35
|
+
# Note: clang-tidy uses the .clang-tidy config file automatically
|
|
36
|
+
# We pass Ruby include path so it can find ruby.h
|
|
37
|
+
ruby_include = RbConfig::CONFIG['rubyhdrdir']
|
|
38
|
+
ruby_arch_include = RbConfig::CONFIG['rubyarchhdrdir']
|
|
39
|
+
|
|
40
|
+
# Without an explicit -I, clang-tidy falls back to whatever tcl.h/tk.h it
|
|
41
|
+
# finds on the default system search path - on macOS that's Apple's own
|
|
42
|
+
# ancient bundled Tcl/Tk 8.5 (symlinked into the Command Line Tools SDK),
|
|
43
|
+
# not the Homebrew tcl-tk this project actually builds against. That
|
|
44
|
+
# header unconditionally needs X11/Xlib.h, which isn't in the SDK -
|
|
45
|
+
# hence "file not found" even though the real build works fine.
|
|
46
|
+
# pkg-config points at the same tcl-tk include dir extconf.rb uses,
|
|
47
|
+
# which also bundles its own vendored X11 headers alongside tk.h, so no
|
|
48
|
+
# separate X11 lookup is needed.
|
|
49
|
+
tcltk_cflags = `pkg-config --cflags tcl tk 2>/dev/null`.strip.split
|
|
50
|
+
if tcltk_cflags.empty?
|
|
51
|
+
warn "Warning: pkg-config couldn't find tcl/tk - clang-tidy may pick up " \
|
|
52
|
+
"the wrong (or no) tcl.h/tk.h. Install tcl-tk via Homebrew/apt, or " \
|
|
53
|
+
"ensure pkg-config can see it."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Same story for teek-sdl2: no explicit -I means clang-tidy won't find
|
|
57
|
+
# SDL2/SDL.h (and friends) at all, since SDL2 has no system-bundled
|
|
58
|
+
# fallback to silently misresolve to - it just fails outright.
|
|
59
|
+
sdl2_cflags = `pkg-config --cflags sdl2 SDL2_ttf SDL2_image SDL2_mixer 2>/dev/null`.strip.split
|
|
60
|
+
if sdl2_cflags.empty?
|
|
61
|
+
warn "Warning: pkg-config couldn't find SDL2 - clang-tidy will fail on " \
|
|
62
|
+
"teek-sdl2/ext/**/*.c. Install SDL2 (+ ttf/image/mixer) via " \
|
|
63
|
+
"Homebrew/apt, or ensure pkg-config can see it."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
success = c_files.all? do |file|
|
|
67
|
+
puts " Checking #{file}..."
|
|
68
|
+
system(clang_tidy, '--quiet', file, '--',
|
|
69
|
+
"-I#{ruby_include}",
|
|
70
|
+
"-I#{ruby_arch_include}",
|
|
71
|
+
'-Iext/teek',
|
|
72
|
+
'-Iteek-sdl2/ext',
|
|
73
|
+
*tcltk_cflags,
|
|
74
|
+
*sdl2_cflags)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if success
|
|
78
|
+
puts '✓ clang-tidy passed'
|
|
79
|
+
else
|
|
80
|
+
abort('clang-tidy found issues!')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
8
84
|
# Documentation tasks - all doc gems are in docs_site/Gemfile
|
|
9
85
|
namespace :docs do
|
|
10
86
|
desc "Install docs dependencies (docs_site/Gemfile)"
|
data/ext/teek/tcltkbridge.c
CHANGED
|
@@ -584,6 +584,70 @@ interp_unregister_callback(VALUE self, VALUE id)
|
|
|
584
584
|
return Qnil;
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
+
/* ---------------------------------------------------------
|
|
588
|
+
* Interp#callback_ids - Currently registered callback id strings
|
|
589
|
+
* (test/introspection use: asserting exactly which ids survive a
|
|
590
|
+
* release, not just how many)
|
|
591
|
+
* --------------------------------------------------------- */
|
|
592
|
+
|
|
593
|
+
static VALUE
|
|
594
|
+
interp_callback_ids(VALUE self)
|
|
595
|
+
{
|
|
596
|
+
struct tcltk_interp *tip = get_interp(self);
|
|
597
|
+
return rb_funcall(tip->callbacks, rb_intern("keys"), 0);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* ---------------------------------------------------------
|
|
601
|
+
* Raise Teek::TclError for a Tcl_Eval/Tcl_EvalObjv failure, attaching
|
|
602
|
+
* errorInfo/errorCode via Tcl_GetReturnOptions - the return options are
|
|
603
|
+
* tied to the specific result code just returned, not the mutable
|
|
604
|
+
* ::errorInfo/::errorCode globals, so this is safe even though it runs
|
|
605
|
+
* after control has already left Tcl_Eval/Tcl_EvalObjv.
|
|
606
|
+
* --------------------------------------------------------- */
|
|
607
|
+
|
|
608
|
+
static VALUE
|
|
609
|
+
tclobj_to_rb_str_or_nil(Tcl_Obj *obj)
|
|
610
|
+
{
|
|
611
|
+
Tcl_Size len;
|
|
612
|
+
const char *str;
|
|
613
|
+
|
|
614
|
+
if (obj == NULL) return Qnil;
|
|
615
|
+
str = Tcl_GetStringFromObj(obj, &len);
|
|
616
|
+
return rb_utf8_str_new(str, len);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
NORETURN(static void raise_tcl_error(Tcl_Interp *interp, int code));
|
|
620
|
+
|
|
621
|
+
static void
|
|
622
|
+
raise_tcl_error(Tcl_Interp *interp, int code)
|
|
623
|
+
{
|
|
624
|
+
Tcl_Obj *options, *einfo_key, *ecode_key;
|
|
625
|
+
Tcl_Obj *einfo_val = NULL, *ecode_val = NULL;
|
|
626
|
+
VALUE msg, backtrace, error_code, exc;
|
|
627
|
+
|
|
628
|
+
options = Tcl_GetReturnOptions(interp, code);
|
|
629
|
+
Tcl_IncrRefCount(options);
|
|
630
|
+
|
|
631
|
+
einfo_key = Tcl_NewStringObj("-errorinfo", -1);
|
|
632
|
+
ecode_key = Tcl_NewStringObj("-errorcode", -1);
|
|
633
|
+
Tcl_IncrRefCount(einfo_key);
|
|
634
|
+
Tcl_IncrRefCount(ecode_key);
|
|
635
|
+
|
|
636
|
+
Tcl_DictObjGet(interp, options, einfo_key, &einfo_val);
|
|
637
|
+
Tcl_DictObjGet(interp, options, ecode_key, &ecode_val);
|
|
638
|
+
|
|
639
|
+
msg = rb_utf8_str_new_cstr(Tcl_GetStringResult(interp));
|
|
640
|
+
backtrace = tclobj_to_rb_str_or_nil(einfo_val);
|
|
641
|
+
error_code = tclobj_to_rb_str_or_nil(ecode_val);
|
|
642
|
+
|
|
643
|
+
Tcl_DecrRefCount(einfo_key);
|
|
644
|
+
Tcl_DecrRefCount(ecode_key);
|
|
645
|
+
Tcl_DecrRefCount(options);
|
|
646
|
+
|
|
647
|
+
exc = rb_funcall(eTclError, rb_intern("new"), 3, msg, backtrace, error_code);
|
|
648
|
+
rb_exc_raise(exc);
|
|
649
|
+
}
|
|
650
|
+
|
|
587
651
|
/* ---------------------------------------------------------
|
|
588
652
|
* Thread-safe event queue: run Ruby proc on main Tcl thread
|
|
589
653
|
*
|
|
@@ -606,7 +670,7 @@ execute_queued_eval(VALUE arg)
|
|
|
606
670
|
int result = Tcl_Eval(tip->interp, script_cstr);
|
|
607
671
|
|
|
608
672
|
if (result != TCL_OK) {
|
|
609
|
-
|
|
673
|
+
raise_tcl_error(tip->interp, result);
|
|
610
674
|
}
|
|
611
675
|
return rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
|
|
612
676
|
}
|
|
@@ -647,7 +711,7 @@ execute_queued_invoke(VALUE arg)
|
|
|
647
711
|
}
|
|
648
712
|
|
|
649
713
|
if (result != TCL_OK) {
|
|
650
|
-
|
|
714
|
+
raise_tcl_error(tip->interp, result);
|
|
651
715
|
}
|
|
652
716
|
return rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
|
|
653
717
|
}
|
|
@@ -818,7 +882,7 @@ interp_tcl_eval(VALUE self, VALUE script)
|
|
|
818
882
|
result = Tcl_Eval(tip->interp, script_cstr);
|
|
819
883
|
|
|
820
884
|
if (result != TCL_OK) {
|
|
821
|
-
|
|
885
|
+
raise_tcl_error(tip->interp, result);
|
|
822
886
|
}
|
|
823
887
|
|
|
824
888
|
return rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
|
|
@@ -883,7 +947,7 @@ interp_tcl_invoke(int argc, VALUE *argv, VALUE self)
|
|
|
883
947
|
}
|
|
884
948
|
|
|
885
949
|
if (result != TCL_OK) {
|
|
886
|
-
|
|
950
|
+
raise_tcl_error(tip->interp, result);
|
|
887
951
|
}
|
|
888
952
|
|
|
889
953
|
ret = rb_utf8_str_new_cstr(Tcl_GetStringResult(tip->interp));
|
|
@@ -1504,6 +1568,7 @@ Init_tcltklib(void)
|
|
|
1504
1568
|
rb_define_method(cInterp, "mainloop", interp_mainloop, 0);
|
|
1505
1569
|
rb_define_method(cInterp, "register_callback", interp_register_callback, 1);
|
|
1506
1570
|
rb_define_method(cInterp, "unregister_callback", interp_unregister_callback, 1);
|
|
1571
|
+
rb_define_method(cInterp, "callback_ids", interp_callback_ids, 0);
|
|
1507
1572
|
rb_define_method(cInterp, "create_slave", interp_create_slave, -1);
|
|
1508
1573
|
rb_define_method(cInterp, "thread_timer_ms", interp_get_thread_timer_ms, 0);
|
|
1509
1574
|
rb_define_method(cInterp, "thread_timer_ms=", interp_set_thread_timer_ms, 1);
|
data/ext/teek/tkdrop_x11.c
CHANGED
|
@@ -48,8 +48,8 @@ static int
|
|
|
48
48
|
hex_decode(const char *s)
|
|
49
49
|
{
|
|
50
50
|
int hi, lo;
|
|
51
|
-
hi = s[0];
|
|
52
|
-
lo = s[1];
|
|
51
|
+
hi = (unsigned char)s[0];
|
|
52
|
+
lo = (unsigned char)s[1];
|
|
53
53
|
if (hi >= '0' && hi <= '9') hi -= '0';
|
|
54
54
|
else if (hi >= 'a' && hi <= 'f') hi = hi - 'a' + 10;
|
|
55
55
|
else if (hi >= 'A' && hi <= 'F') hi = hi - 'A' + 10;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Teek
|
|
6
|
+
# Tracks Ruby callback ids scoped to something narrower than a whole
|
|
7
|
+
# widget - keyed by (container, key) pairs - so they can be released
|
|
8
|
+
# again without waiting for a <Destroy> that may never come for the
|
|
9
|
+
# thing the callback is actually attached to (an event binding, a menu
|
|
10
|
+
# entry, ...). A single instance is shared across every feature that
|
|
11
|
+
# needs this; callers namespace their own container keys (by
|
|
12
|
+
# convention, [feature_tag, path] tuples) so two features tracking the
|
|
13
|
+
# same underlying path never collide.
|
|
14
|
+
#
|
|
15
|
+
# There is exactly one way to keep the registry in sync: {#reconcile}.
|
|
16
|
+
# Its block is handed the {key => id} hash tracked last time and must
|
|
17
|
+
# return the {key => id} hash that should be tracked now; whatever id
|
|
18
|
+
# drops out between the two gets released. What the block *does* with
|
|
19
|
+
# the hash it's handed is entirely up to the caller - reuse its values
|
|
20
|
+
# as a starting point for a cheap in-memory update (nothing external
|
|
21
|
+
# can silently change an event binding), or ignore it and recompute
|
|
22
|
+
# the truth from scratch by asking Tk (Tk silently renumbers menu
|
|
23
|
+
# entries, so nothing short of asking can be trusted there). The
|
|
24
|
+
# registry itself never knows or cares which one a caller chose.
|
|
25
|
+
#
|
|
26
|
+
# The one hard rule either way: the returned hash must be a DIFFERENT
|
|
27
|
+
# object from the one the block was handed - e.g. +before.merge(...)+,
|
|
28
|
+
# never +before.merge!(...)+ returned as-is. Released ids are computed
|
|
29
|
+
# as +before.values - after.values+; if the block mutates +before+ in
|
|
30
|
+
# place and returns that same object, +before+ and +after+ are
|
|
31
|
+
# identical by the time that subtraction runs, so any id that was
|
|
32
|
+
# dropped or replaced is silently never released - a leak, the exact
|
|
33
|
+
# thing this class exists to prevent.
|
|
34
|
+
#
|
|
35
|
+
# {#forget_all_for_path} is the only thing a <Destroy> handler needs to
|
|
36
|
+
# call: it releases every container ever registered under a path,
|
|
37
|
+
# regardless of which feature created it, via a reverse index built
|
|
38
|
+
# automatically as a side effect of {#reconcile}.
|
|
39
|
+
class CallbackRegistry
|
|
40
|
+
def initialize(app)
|
|
41
|
+
@app = app
|
|
42
|
+
@entries = Hash.new { |h, k| h[k] = {} }
|
|
43
|
+
@containers_by_path = Hash.new { |h, k| h[k] = Set.new }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @yieldparam before [Hash] the {key => id} hash tracked for +container+
|
|
47
|
+
# as of the last call (empty on the first call)
|
|
48
|
+
# @yieldreturn [Hash] the {key => id} hash that should be tracked now -
|
|
49
|
+
# must be a different object from +before+ (see class docs above);
|
|
50
|
+
# do not mutate +before+ in place and return it, or dropped/replaced
|
|
51
|
+
# ids silently never get released
|
|
52
|
+
# @return [void]
|
|
53
|
+
def reconcile(container)
|
|
54
|
+
track(container)
|
|
55
|
+
before = @entries[container]
|
|
56
|
+
after = yield(before)
|
|
57
|
+
(before.values - after.values).each { |id| @app.unregister_callback(id) }
|
|
58
|
+
@entries[container] = after
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Release every callback tracked under any container registered for
|
|
62
|
+
# +path+, regardless of which feature created it, and forget them.
|
|
63
|
+
# @return [void]
|
|
64
|
+
def forget_all_for_path(path)
|
|
65
|
+
containers = @containers_by_path.delete(path)
|
|
66
|
+
return unless containers
|
|
67
|
+
containers.each do |container|
|
|
68
|
+
ids = @entries.delete(container)
|
|
69
|
+
ids&.each_value { |id| @app.unregister_callback(id) }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Convention, not a type check: every container is [feature_tag, path].
|
|
76
|
+
def track(container)
|
|
77
|
+
@containers_by_path[container.last] << container
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'command_interceptors'
|
|
4
|
+
require_relative 'callback_registry'
|
|
5
|
+
|
|
6
|
+
module Teek
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
# Registered for the "canvas" {CommandInterceptors} entry below.
|
|
10
|
+
#
|
|
11
|
+
# Canvas items aren't windows (only the canvas itself is one), so a bound
|
|
12
|
+
# item's callback never fires <Destroy> on its own, and `canvas delete`
|
|
13
|
+
# is silent - the same leak shape menu entries have. Unlike menu or
|
|
14
|
+
# text/treeview tags, canvas has no "list every live binding" enumeration
|
|
15
|
+
# command (no analogue to menu's `index end` or text's `tag names`), so
|
|
16
|
+
# this can't do a full-scan reconcile. Instead it re-queries only the
|
|
17
|
+
# (tagOrId, sequence) keys it already knows about - via `canvas bind
|
|
18
|
+
# tagOrId sequence`, the 2-arg read form - after every bind/delete call,
|
|
19
|
+
# and lets whatever no longer resolves drop out.
|
|
20
|
+
#
|
|
21
|
+
# A binding on a numeric item id is released this way once that item is
|
|
22
|
+
# deleted (Tk's Tk_DeleteAllBindings clears its binding-table entries
|
|
23
|
+
# along with it). A binding on a tag is NOT released by deleting a
|
|
24
|
+
# tagged item - the tag itself isn't an item, so its binding-table entry
|
|
25
|
+
# persists independent of which (if any) items currently carry that tag.
|
|
26
|
+
module CanvasBindInterceptor
|
|
27
|
+
MUTATING_SUBCOMMANDS = %w[bind delete].freeze
|
|
28
|
+
|
|
29
|
+
def self.call(app, path, args, kwargs)
|
|
30
|
+
sub = args[0]&.to_s
|
|
31
|
+
return nil unless MUTATING_SUBCOMMANDS.include?(sub)
|
|
32
|
+
|
|
33
|
+
result = app.raw_command(path, *args, **kwargs)
|
|
34
|
+
app.callback_registry.reconcile([:canvas_bind, path]) { |before| requery(app, path, before, sub, args) }
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.requery(app, path, before, sub, args)
|
|
39
|
+
keys = before.keys
|
|
40
|
+
keys += [[args[1].to_s, args[2].to_s]] if sub == 'bind' && args.length >= 4
|
|
41
|
+
|
|
42
|
+
keys.uniq.each_with_object({}) do |(tag_or_id, seq), after|
|
|
43
|
+
# Unlike a tag (a plain Tcl string, always a valid query target even
|
|
44
|
+
# if nothing currently carries it), a numeric item id is a hash key
|
|
45
|
+
# into the canvas's item table - querying one after its item is
|
|
46
|
+
# deleted raises "item \"N\" doesn't exist" rather than returning
|
|
47
|
+
# empty, so a deleted item's binding has to be dropped via rescue,
|
|
48
|
+
# not by checking the result.
|
|
49
|
+
current = begin
|
|
50
|
+
app.tcl_eval("#{path} bind #{tag_or_id} #{seq}")
|
|
51
|
+
rescue Teek::TclError
|
|
52
|
+
''
|
|
53
|
+
end
|
|
54
|
+
# \z anchors this to a BARE `ruby_callback <id>` with nothing
|
|
55
|
+
# after. raw_command's generic positional-Proc handling
|
|
56
|
+
# technically allows a caller to pass %-substitutions to a
|
|
57
|
+
# canvas-bind-shaped app.command call (the same mechanism
|
|
58
|
+
# App#bind uses), but nothing in teek does that today. If a
|
|
59
|
+
# caller ever does, this regex silently stops matching and that
|
|
60
|
+
# id leaks on rebuild/delete - drop the \z anchor (match just
|
|
61
|
+
# the leading `ruby_callback <id>`) if that changes.
|
|
62
|
+
after[[tag_or_id, seq]] = Regexp.last_match(1) if current =~ /\Aruby_callback (\S+)\z/
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
CommandInterceptors.register('canvas', 'canvas_bind') { |*a| CanvasBindInterceptor.call(*a) }
|
|
68
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# Registry of per-Tk-widget-type interceptors that App#command consults
|
|
7
|
+
# before falling back to its own generic handling. Each interceptor is a
|
|
8
|
+
# labeled block registered under a widget type string (the same strings
|
|
9
|
+
# used in WIDGET_COMMANDS); App#command looks up the type for the path it
|
|
10
|
+
# was given (see App#record_widget_type) and tries every interceptor
|
|
11
|
+
# registered for that type.
|
|
12
|
+
#
|
|
13
|
+
# An interceptor block receives (app, path, args, kwargs) and must
|
|
14
|
+
# return nil if this call isn't its concern - Tcl results from
|
|
15
|
+
# App#raw_command are always Strings, never nil, so nil is an
|
|
16
|
+
# unambiguous "not mine" sentinel - or the Tcl result if it handled the
|
|
17
|
+
# call itself (typically by calling App#raw_command internally).
|
|
18
|
+
#
|
|
19
|
+
# Multiple widget types can share the same interceptor logic (text and
|
|
20
|
+
# ttk::treeview tag bindings are byte-identical in Tcl shape) by
|
|
21
|
+
# registering the same block under each type. The +label+ is what
|
|
22
|
+
# App#command reports if two DIFFERENT interceptors both claim the same
|
|
23
|
+
# call for the same type - it raises AmbiguousCommandError naming both
|
|
24
|
+
# labels rather than silently picking one, so whoever's debugging can
|
|
25
|
+
# tell which interceptors collided (built-in shape-matching bug, a
|
|
26
|
+
# custom interceptor overlapping a built-in one, two custom interceptors
|
|
27
|
+
# overlapping each other, ...).
|
|
28
|
+
class CommandInterceptors
|
|
29
|
+
Entry = Struct.new(:label, :block)
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def register(type, label, &block)
|
|
33
|
+
interceptors[type.to_s] << Entry.new(label.to_s, block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def for_type(type)
|
|
37
|
+
interceptors[type.to_s]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def interceptors
|
|
43
|
+
@interceptors ||= Hash.new { |h, k| h[k] = [] }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/teek/debugger.rb
CHANGED
|
@@ -27,8 +27,7 @@ module Teek
|
|
|
27
27
|
app.command(:wm, 'geometry', TOP, '400x500')
|
|
28
28
|
|
|
29
29
|
# Don't let closing the debugger kill the app
|
|
30
|
-
|
|
31
|
-
app.command(:wm, 'protocol', TOP, 'WM_DELETE_WINDOW', close_proc)
|
|
30
|
+
app.on_close(window: TOP) { app.command(:wm, 'withdraw', TOP) }
|
|
32
31
|
|
|
33
32
|
setup_ui
|
|
34
33
|
sync_widget_tree
|
|
@@ -246,10 +245,9 @@ module Teek
|
|
|
246
245
|
@app.command(:pack, vars_tree, fill: :both, expand: 1)
|
|
247
246
|
|
|
248
247
|
# Right-click context menu
|
|
249
|
-
@app.
|
|
248
|
+
vars_ctx = @app.menu("#{NB}.vars.ctx")
|
|
250
249
|
watch_proc = proc { |*| watch_selected_variable }
|
|
251
|
-
|
|
252
|
-
label: 'Watch', command: watch_proc)
|
|
250
|
+
vars_ctx.command(:add, :command, label: 'Watch', command: watch_proc)
|
|
253
251
|
|
|
254
252
|
@app.command(:bind, vars_tree, '<Button-3>', proc { |*|
|
|
255
253
|
# Select the row under cursor, then show context menu
|
|
@@ -257,7 +255,7 @@ module Teek
|
|
|
257
255
|
set item [#{vars_tree} identify item [winfo pointerx #{vars_tree}] [winfo pointery #{vars_tree}]]
|
|
258
256
|
if {$item ne {}} { #{vars_tree} selection set $item }
|
|
259
257
|
")
|
|
260
|
-
@app.
|
|
258
|
+
@app.popup_menu(vars_ctx, x: @app.winfo.pointerx, y: @app.winfo.pointery)
|
|
261
259
|
})
|
|
262
260
|
|
|
263
261
|
# Double-click to watch
|
|
@@ -530,17 +528,16 @@ module Teek
|
|
|
530
528
|
@app.command(:bind, watch_tree, '<<TreeviewSelect>>', select_proc)
|
|
531
529
|
|
|
532
530
|
# Right-click to unwatch
|
|
533
|
-
@app.
|
|
531
|
+
watches_ctx = @app.menu("#{NB}.watches.ctx")
|
|
534
532
|
unwatch_proc = proc { |*| unwatch_selected }
|
|
535
|
-
|
|
536
|
-
label: 'Unwatch', command: unwatch_proc)
|
|
533
|
+
watches_ctx.command(:add, :command, label: 'Unwatch', command: unwatch_proc)
|
|
537
534
|
|
|
538
535
|
@app.command(:bind, watch_tree, '<Button-3>', proc { |*|
|
|
539
536
|
@app.tcl_eval("
|
|
540
537
|
set item [#{watch_tree} identify item [winfo pointerx #{watch_tree}] [winfo pointery #{watch_tree}]]
|
|
541
538
|
if {$item ne {}} { #{watch_tree} selection set $item }
|
|
542
539
|
")
|
|
543
|
-
@app.
|
|
540
|
+
@app.popup_menu(watches_ctx, x: @app.winfo.pointerx, y: @app.winfo.pointery)
|
|
544
541
|
})
|
|
545
542
|
end
|
|
546
543
|
|
data/lib/teek/demo_support.rb
CHANGED
|
@@ -94,8 +94,8 @@ module TeekDemo
|
|
|
94
94
|
|
|
95
95
|
try_signal = proc do
|
|
96
96
|
app.tcl_eval('update idletasks')
|
|
97
|
-
width = app.
|
|
98
|
-
height = app.
|
|
97
|
+
width = app.winfo.width(window)
|
|
98
|
+
height = app.winfo.height(window)
|
|
99
99
|
|
|
100
100
|
if width >= 10 && height >= 10
|
|
101
101
|
@_recording_ready_sent = true
|