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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fdcad5500b3501e3d2b83d29678b76f8cc043b2f44e5e0af66a300fd8fa03ac
4
- data.tar.gz: ca0b209ebe033d7e246271bda5491c8192279e063fdb5e45f2bb719fcd0cb5dc
3
+ metadata.gz: 3ff08eb5884f74dc6461c5ff1163a025b44b798e2e480bd4f2855210dd0f0fdf
4
+ data.tar.gz: 8e012ccf6a6fffe427630c033b288f784ed455552b064870b0bce054d11e62ee
5
5
  SHA512:
6
- metadata.gz: d69b03be50a1ad912ba7f8f0610ae7ba6163b19bc342a9f763112a467ddef39f2b8d0e5e2317463c91096d02674f6641d345370c35c09b71f37055d40ceab926
7
- data.tar.gz: b720547bd7a11f6a3f7c15ec33c78d2c86c42e80ea9b273dd50803e5d2012c6dd286ae2ebd30275ca9b85eaa26866da00df8d0982a21cde97361d8fa98a1df10
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 standard `menu` widgets:
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.command(:menu, '.menubar')
113
- app.command('.', :configure, menu: '.menubar')
112
+ menubar = app.menu('.menubar')
113
+ app.command('.', :configure, menu: menubar)
114
114
 
115
- app.command(:menu, '.menubar.file', tearoff: 0)
116
- app.command('.menubar', :add, :cascade, label: 'File', menu: '.menubar.file')
117
- app.command('.menubar.file', :add, :command,
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)"
@@ -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
- rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
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
- rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
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
- rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
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
- rb_raise(eTclError, "%s", Tcl_GetStringResult(tip->interp));
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);
@@ -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
- close_proc = proc { |*| app.command(:wm, 'withdraw', TOP) }
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.command(:menu, "#{NB}.vars.ctx", tearoff: 0)
248
+ vars_ctx = @app.menu("#{NB}.vars.ctx")
250
249
  watch_proc = proc { |*| watch_selected_variable }
251
- @app.command("#{NB}.vars.ctx", 'add', 'command',
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.tcl_eval("tk_popup #{NB}.vars.ctx [winfo pointerx .] [winfo pointery .]")
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.command(:menu, "#{NB}.watches.ctx", tearoff: 0)
531
+ watches_ctx = @app.menu("#{NB}.watches.ctx")
534
532
  unwatch_proc = proc { |*| unwatch_selected }
535
- @app.command("#{NB}.watches.ctx", 'add', 'command',
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.tcl_eval("tk_popup #{NB}.watches.ctx [winfo pointerx .] [winfo pointery .]")
540
+ @app.popup_menu(watches_ctx, x: @app.winfo.pointerx, y: @app.winfo.pointery)
544
541
  })
545
542
  end
546
543
 
@@ -94,8 +94,8 @@ module TeekDemo
94
94
 
95
95
  try_signal = proc do
96
96
  app.tcl_eval('update idletasks')
97
- width = app.tcl_eval("winfo width #{window}").to_i
98
- height = app.tcl_eval("winfo height #{window}").to_i
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