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
data/lib/teek/dialogs.rb
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
class App
|
|
5
|
+
# Show the native "choose file to open" dialog.
|
|
6
|
+
#
|
|
7
|
+
# @param filetypes [Array<Array>, nil] e.g.
|
|
8
|
+
# +[["PNG Images", ".png"], ["All Files", "*"]]+ - the second
|
|
9
|
+
# element of each pair can also be an array of extensions
|
|
10
|
+
# (+["Images", [".png", ".jpg"]]+)
|
|
11
|
+
# @param initialdir [String, nil] directory the dialog starts in
|
|
12
|
+
# @param initialfile [String, nil] filename pre-filled in the dialog
|
|
13
|
+
# @param title [String, nil] dialog window title
|
|
14
|
+
# @param multiple [Boolean] allow selecting more than one file
|
|
15
|
+
# @param parent [String, nil] parent window (defaults to the root window)
|
|
16
|
+
# @return [String, Array<String>, nil] the chosen path (an array of
|
|
17
|
+
# paths if +multiple:+), or +nil+ if the dialog was cancelled
|
|
18
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/getOpenFile.htm tk_getOpenFile
|
|
19
|
+
def choose_open_file(filetypes: nil, initialdir: nil, initialfile: nil,
|
|
20
|
+
title: nil, multiple: false, parent: nil)
|
|
21
|
+
args = ['tk_getOpenFile']
|
|
22
|
+
args.push('-filetypes', build_filetypes(filetypes)) if filetypes
|
|
23
|
+
args.push('-initialdir', initialdir) if initialdir
|
|
24
|
+
args.push('-initialfile', initialfile) if initialfile
|
|
25
|
+
args.push('-title', title) if title
|
|
26
|
+
args.push('-parent', parent) if parent
|
|
27
|
+
args.push('-multiple', bool_to_tcl(true)) if multiple
|
|
28
|
+
|
|
29
|
+
result = tcl_invoke(*args)
|
|
30
|
+
return nil if result.empty?
|
|
31
|
+
|
|
32
|
+
multiple ? split_list(result) : result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Show the native "choose file to save" dialog.
|
|
36
|
+
#
|
|
37
|
+
# @param filetypes [Array<Array>, nil] see {#choose_open_file}
|
|
38
|
+
# @param initialdir [String, nil] directory the dialog starts in
|
|
39
|
+
# @param initialfile [String, nil] filename pre-filled in the dialog
|
|
40
|
+
# @param title [String, nil] dialog window title
|
|
41
|
+
# @param defaultextension [String, nil] extension appended if the
|
|
42
|
+
# typed filename doesn't already have one
|
|
43
|
+
# @param confirmoverwrite [Boolean] ask before overwriting an
|
|
44
|
+
# existing file (Tk's own default is true; pass false to skip the
|
|
45
|
+
# confirmation)
|
|
46
|
+
# @param parent [String, nil] parent window (defaults to the root window)
|
|
47
|
+
# @return [String, nil] the chosen path, or +nil+ if cancelled
|
|
48
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/getOpenFile.htm tk_getSaveFile
|
|
49
|
+
def choose_save_file(filetypes: nil, initialdir: nil, initialfile: nil,
|
|
50
|
+
title: nil, defaultextension: nil, confirmoverwrite: true, parent: nil)
|
|
51
|
+
args = ['tk_getSaveFile']
|
|
52
|
+
args.push('-filetypes', build_filetypes(filetypes)) if filetypes
|
|
53
|
+
args.push('-initialdir', initialdir) if initialdir
|
|
54
|
+
args.push('-initialfile', initialfile) if initialfile
|
|
55
|
+
args.push('-title', title) if title
|
|
56
|
+
args.push('-defaultextension', defaultextension) if defaultextension
|
|
57
|
+
args.push('-confirmoverwrite', bool_to_tcl(false)) unless confirmoverwrite
|
|
58
|
+
args.push('-parent', parent) if parent
|
|
59
|
+
|
|
60
|
+
result = tcl_invoke(*args)
|
|
61
|
+
result.empty? ? nil : result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Show a message box with one or more buttons.
|
|
65
|
+
#
|
|
66
|
+
# @param message [String] the main message text
|
|
67
|
+
# @param title [String, nil] dialog window title
|
|
68
|
+
# @param detail [String, nil] additional explanatory text, shown
|
|
69
|
+
# smaller below +message+
|
|
70
|
+
# @param icon [:error, :info, :question, :warning] icon to display
|
|
71
|
+
# @param type [:ok, :okcancel, :abortretryignore, :yesno, :yesnocancel, :retrycancel]
|
|
72
|
+
# which button(s) to show
|
|
73
|
+
# @param default [Symbol, nil] which button is focused by default
|
|
74
|
+
# (e.g. +:cancel+); defaults to Tk's own choice if omitted
|
|
75
|
+
# @param parent [String, nil] parent window (defaults to the root window)
|
|
76
|
+
# @return [Symbol] the pressed button - +:ok+, +:cancel+, +:yes+,
|
|
77
|
+
# +:no+, +:abort+, +:retry+, or +:ignore+
|
|
78
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/messageBox.htm tk_messageBox
|
|
79
|
+
def message_box(message:, title: nil, detail: nil, icon: :info, type: :ok,
|
|
80
|
+
default: nil, parent: nil)
|
|
81
|
+
args = ['tk_messageBox', '-message', message.to_s]
|
|
82
|
+
args.push('-title', title) if title
|
|
83
|
+
args.push('-detail', detail) if detail
|
|
84
|
+
args.push('-icon', icon.to_s) if icon
|
|
85
|
+
args.push('-type', type.to_s) if type
|
|
86
|
+
args.push('-default', default.to_s) if default
|
|
87
|
+
args.push('-parent', parent) if parent
|
|
88
|
+
|
|
89
|
+
tcl_invoke(*args).to_sym
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Show the native color picker dialog.
|
|
93
|
+
#
|
|
94
|
+
# @param initial [String, nil] initial color (e.g. +"#ff0000"+)
|
|
95
|
+
# @param title [String, nil] dialog window title
|
|
96
|
+
# @param parent [String, nil] parent window (defaults to the root window)
|
|
97
|
+
# @return [String, nil] the chosen color as +"#rrggbb"+, or +nil+ if cancelled
|
|
98
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/chooseColor.htm tk_chooseColor
|
|
99
|
+
def choose_color(initial: nil, title: nil, parent: nil)
|
|
100
|
+
args = ['tk_chooseColor']
|
|
101
|
+
args.push('-initialcolor', initial) if initial
|
|
102
|
+
args.push('-title', title) if title
|
|
103
|
+
args.push('-parent', parent) if parent
|
|
104
|
+
|
|
105
|
+
result = tcl_invoke(*args)
|
|
106
|
+
result.empty? ? nil : result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Pop up a menu at the given screen coordinates.
|
|
110
|
+
#
|
|
111
|
+
# @param menu [Widget, String] the menu to pop up
|
|
112
|
+
# @param x [Integer] screen x coordinate
|
|
113
|
+
# @param y [Integer] screen y coordinate
|
|
114
|
+
# @param entry [Integer, String, nil] index or label of the entry to
|
|
115
|
+
# show as active
|
|
116
|
+
# @return [void]
|
|
117
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/menu.htm#M42 tk_popup
|
|
118
|
+
def popup_menu(menu, x:, y:, entry: nil)
|
|
119
|
+
args = ['tk_popup', menu.to_s, x.to_s, y.to_s]
|
|
120
|
+
args << entry.to_s if entry
|
|
121
|
+
tcl_invoke(*args)
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Builds the nested Tcl list -filetypes expects:
|
|
128
|
+
# {{name extensionOrExtensionList} {name2 ...}}
|
|
129
|
+
def build_filetypes(filetypes)
|
|
130
|
+
entries = filetypes.map do |name, exts|
|
|
131
|
+
ext_arg = exts.is_a?(Array) ? make_list(*exts) : exts.to_s
|
|
132
|
+
make_list(name.to_s, ext_arg)
|
|
133
|
+
end
|
|
134
|
+
make_list(*entries)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
# Live-scan helper for the "menu" {CommandInterceptors} entry below.
|
|
10
|
+
#
|
|
11
|
+
# Menu entries are not windows (only the menu itself is one), so they
|
|
12
|
+
# never fire <Destroy>; entry deletion is silent and Tk renumbers the
|
|
13
|
+
# survivors internally. Because of that, entry-level callbacks can't be
|
|
14
|
+
# tracked by index or by any per-entry event - the only sound way to know
|
|
15
|
+
# which callbacks are still needed is to ask Tk what's actually live
|
|
16
|
+
# after every mutating call and release whatever dropped out.
|
|
17
|
+
module MenuInterceptor
|
|
18
|
+
ENTRY_SUBCOMMANDS = %w[add insert entryconfigure delete].freeze
|
|
19
|
+
|
|
20
|
+
LIVE_COMMANDS_TCL_PROC = <<~TCL.freeze
|
|
21
|
+
proc ::teek_menu_live_commands {path} {
|
|
22
|
+
set result {}
|
|
23
|
+
if {![winfo exists $path]} { return $result }
|
|
24
|
+
set last [$path index end]
|
|
25
|
+
if {$last eq "none"} { return $result }
|
|
26
|
+
for {set i 0} {$i <= $last} {incr i} {
|
|
27
|
+
set cmd ""
|
|
28
|
+
catch {set cmd [$path entrycget $i -command]}
|
|
29
|
+
lappend result $cmd
|
|
30
|
+
}
|
|
31
|
+
return $result
|
|
32
|
+
}
|
|
33
|
+
TCL
|
|
34
|
+
|
|
35
|
+
def self.live_command_ids(app, path)
|
|
36
|
+
app.ensure_tcl_helper(:menu_live_commands) { LIVE_COMMANDS_TCL_PROC }
|
|
37
|
+
raw = app.tcl_eval("::teek_menu_live_commands #{path}")
|
|
38
|
+
# \z anchors this to a BARE `ruby_callback <id>` with nothing after -
|
|
39
|
+
# correct today because teek never appends %-substitutions to a menu
|
|
40
|
+
# entry's -command (that only happens for App#bind's widget bindings).
|
|
41
|
+
# If substitution support is ever added here, this regex silently
|
|
42
|
+
# stops matching and those ids leak on rebuild/delete - drop the \z
|
|
43
|
+
# anchor (match just the leading `ruby_callback <id>`) if that changes.
|
|
44
|
+
app.split_list(raw).each_with_object({}) do |cmd, ids|
|
|
45
|
+
ids[Regexp.last_match(1)] = Regexp.last_match(1) if cmd =~ /\Aruby_callback (\S+)\z/
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Any add/insert/entryconfigure/delete on a menu goes through raw_command
|
|
51
|
+
# unchanged (any command: Proc it carries is already registered correctly
|
|
52
|
+
# by raw_command itself), then reconciles tracked entry callbacks against
|
|
53
|
+
# Tk's live entrycget values - see {MenuInterceptor}.
|
|
54
|
+
CommandInterceptors.register('menu', 'menu_entry') do |app, path, args, kwargs|
|
|
55
|
+
next nil unless MenuInterceptor::ENTRY_SUBCOMMANDS.include?(args[0]&.to_s)
|
|
56
|
+
|
|
57
|
+
result = app.raw_command(path, *args, **kwargs)
|
|
58
|
+
app.callback_registry.reconcile([:menu, path]) { |_before| MenuInterceptor.live_command_ids(app, path) }
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/teek/photo.rb
CHANGED
|
@@ -50,10 +50,45 @@ module Teek
|
|
|
50
50
|
@counter += 1
|
|
51
51
|
"teek_photo#{@counter}"
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# @api private
|
|
55
|
+
#
|
|
56
|
+
# Builds the proc registered as this instance's finalizer. Must not
|
|
57
|
+
# close over +self+ (the Photo instance) or any of its ivars
|
|
58
|
+
# directly - a finalizer that references its own object keeps that
|
|
59
|
+
# object permanently reachable, so it would never actually be
|
|
60
|
+
# collected. +name+/+app+ are plain local captures instead.
|
|
61
|
+
#
|
|
62
|
+
# Finalizers can run on any thread, so the delete goes through
|
|
63
|
+
# {Interp#queue_for_main} (fire-and-forget) rather than #tcl_eval,
|
|
64
|
+
# which would block on a cross-thread queue wait if called from a
|
|
65
|
+
# background thread - risky from inside a GC finalizer. The +catch+
|
|
66
|
+
# means a concurrent explicit #delete (or an interpreter that's
|
|
67
|
+
# already torn down by the time this runs) is a silent no-op, not
|
|
68
|
+
# an error with nowhere to go.
|
|
69
|
+
def finalizer_for(name, app)
|
|
70
|
+
proc do
|
|
71
|
+
begin
|
|
72
|
+
app.interp.queue_for_main(proc { app.tcl_eval("catch {image delete #{name}}") })
|
|
73
|
+
rescue Teek::TclError
|
|
74
|
+
# interpreter already torn down - nothing to clean up
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
53
78
|
end
|
|
54
79
|
|
|
55
80
|
# Create a new photo image.
|
|
56
81
|
#
|
|
82
|
+
# The underlying Tk image is automatically deleted once this Photo
|
|
83
|
+
# object is garbage collected and nothing else references it - keep
|
|
84
|
+
# it around (an ivar, a collection, etc.) for as long as you need the
|
|
85
|
+
# image, the same contract as File or Socket. If only the image
|
|
86
|
+
# *name* is kept (e.g. passed into a widget's +image:+) and this
|
|
87
|
+
# wrapper is dropped, the image can be reclaimed out from under that
|
|
88
|
+
# reference; Tk shows a broken image rather than crashing, but the
|
|
89
|
+
# image won't come back - call {#delete} explicitly rather than
|
|
90
|
+
# relying on GC timing if you need deterministic cleanup.
|
|
91
|
+
#
|
|
57
92
|
# @param app [Teek::App] the application instance
|
|
58
93
|
# @param name [String, nil] Tcl image name (auto-generated if nil)
|
|
59
94
|
# @param width [Integer, nil] image width in pixels
|
|
@@ -78,6 +113,21 @@ module Teek
|
|
|
78
113
|
kwargs[:gamma] = gamma if gamma
|
|
79
114
|
|
|
80
115
|
@app.command(:image, :create, :photo, @name, **kwargs)
|
|
116
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer_for(@name, @app))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Invoke a photo subcommand not covered by a dedicated method above
|
|
120
|
+
# (e.g. +copy+, +read+, +write+). Prepends the image name as the Tcl
|
|
121
|
+
# command, exactly like {Widget#command} does for widgets.
|
|
122
|
+
#
|
|
123
|
+
# @example Subsample a copy of another photo
|
|
124
|
+
# thumb = Teek::Photo.new(app)
|
|
125
|
+
# thumb.command(:copy, source_photo, subsample: 4)
|
|
126
|
+
# @param args positional arguments
|
|
127
|
+
# @param kwargs keyword arguments mapped to +-key value+ pairs
|
|
128
|
+
# @return [String] the Tcl result
|
|
129
|
+
def command(*args, **kwargs)
|
|
130
|
+
@app.command(@name, *args, **kwargs)
|
|
81
131
|
end
|
|
82
132
|
|
|
83
133
|
# Write RGBA pixel data to the image.
|
|
@@ -197,8 +247,13 @@ module Teek
|
|
|
197
247
|
|
|
198
248
|
# Delete this photo image and free its resources.
|
|
199
249
|
#
|
|
250
|
+
# Cancels the GC finalizer registered by {#initialize} - otherwise a
|
|
251
|
+
# later collection could delete an unrelated image if this name gets
|
|
252
|
+
# reused (e.g. an explicit +name:+ passed to a later Photo).
|
|
253
|
+
#
|
|
200
254
|
# @return [void]
|
|
201
255
|
def delete
|
|
256
|
+
ObjectSpace.undefine_finalizer(self)
|
|
202
257
|
@app.tcl_eval("image delete #{@name}")
|
|
203
258
|
end
|
|
204
259
|
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
# Live-scan helper for the shared "text"/"ttk::treeview" {CommandInterceptors}
|
|
10
|
+
# entry below - both widgets have byte-identical `tag bind`/`tag names` shapes.
|
|
11
|
+
#
|
|
12
|
+
# Tags aren't windows, so a tag's bound callback never fires <Destroy> on
|
|
13
|
+
# its own; the widget that owns it is typically long-lived and reused
|
|
14
|
+
# (log panes, editors, tree views), so tags churn while the widget
|
|
15
|
+
# persists. Unlike menu entries, a tag name is a stable hash key Tk never
|
|
16
|
+
# renumbers, so reconciling is a straightforward full scan: enumerate
|
|
17
|
+
# every live tag (`tag names`), read back what's bound to each
|
|
18
|
+
# (`tag bind $tag` / `tag bind $tag $seq`), and release whatever dropped out.
|
|
19
|
+
module TagBindInterceptor
|
|
20
|
+
MUTATING_SUBCOMMANDS = %w[bind delete].freeze
|
|
21
|
+
|
|
22
|
+
LIVE_COMMANDS_TCL_PROC = <<~TCL.freeze
|
|
23
|
+
proc ::teek_tag_live_commands {path} {
|
|
24
|
+
set result {}
|
|
25
|
+
foreach tag [$path tag names] {
|
|
26
|
+
foreach seq [$path tag bind $tag] {
|
|
27
|
+
lappend result [$path tag bind $tag $seq]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return $result
|
|
31
|
+
}
|
|
32
|
+
TCL
|
|
33
|
+
|
|
34
|
+
def self.live_command_ids(app, path)
|
|
35
|
+
app.ensure_tcl_helper(:tag_live_commands) { LIVE_COMMANDS_TCL_PROC }
|
|
36
|
+
raw = app.tcl_eval("::teek_tag_live_commands #{path}")
|
|
37
|
+
# \z anchors this to a BARE `ruby_callback <id>` with nothing after.
|
|
38
|
+
# raw_command's generic positional-Proc handling technically allows a
|
|
39
|
+
# caller to pass %-substitutions to a tag-bind-shaped app.command
|
|
40
|
+
# call (the same mechanism App#bind uses), but nothing in teek does
|
|
41
|
+
# that today. If a caller ever does, this regex silently stops
|
|
42
|
+
# matching and that id leaks on rebuild/delete - drop the \z anchor
|
|
43
|
+
# (match just the leading `ruby_callback <id>`) if that changes.
|
|
44
|
+
app.split_list(raw).each_with_object({}) do |cmd, ids|
|
|
45
|
+
ids[Regexp.last_match(1)] = Regexp.last_match(1) if cmd =~ /\Aruby_callback (\S+)\z/
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A `tag bind`/`tag delete` call goes through raw_command unchanged (a
|
|
50
|
+
# bound Proc is registered exactly like any other bind-shaped
|
|
51
|
+
# positional arg), then reconciles tracked tag callbacks against Tk's
|
|
52
|
+
# live tag-bind state.
|
|
53
|
+
def self.call(app, path, args, kwargs)
|
|
54
|
+
return nil unless args[0]&.to_s == 'tag' && MUTATING_SUBCOMMANDS.include?(args[1]&.to_s)
|
|
55
|
+
|
|
56
|
+
result = app.raw_command(path, *args, **kwargs)
|
|
57
|
+
app.callback_registry.reconcile([:tag_bind, path]) { |_before| live_command_ids(app, path) }
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# text and ttk::treeview share byte-identical `tag bind`/`tag names`
|
|
63
|
+
# shapes, so both register the same interceptor method.
|
|
64
|
+
CommandInterceptors.register('text', 'tag_bind') { |*a| TagBindInterceptor.call(*a) }
|
|
65
|
+
CommandInterceptors.register('ttk::treeview', 'tag_bind') { |*a| TagBindInterceptor.call(*a) }
|
|
66
|
+
end
|
data/lib/teek/version.rb
CHANGED
data/lib/teek/widget.rb
CHANGED
|
@@ -36,7 +36,9 @@ module Teek
|
|
|
36
36
|
# btn.command(:invoke) # => .ttkbutton1 invoke
|
|
37
37
|
#
|
|
38
38
|
# @param args positional arguments
|
|
39
|
-
# @param kwargs keyword arguments mapped to -key value pairs
|
|
39
|
+
# @param kwargs keyword arguments mapped to -key value pairs; any Proc
|
|
40
|
+
# value (e.g. command:) is tracked and released if reconfigured or
|
|
41
|
+
# when this widget is destroyed
|
|
40
42
|
# @return [String] the Tcl result
|
|
41
43
|
def command(*args, **kwargs)
|
|
42
44
|
@app.command(@path, *args, **kwargs)
|
|
@@ -51,7 +53,19 @@ module Teek
|
|
|
51
53
|
# Check if this widget still exists in the Tk interpreter.
|
|
52
54
|
# @return [Boolean]
|
|
53
55
|
def exist?
|
|
54
|
-
@app.
|
|
56
|
+
@app.winfo.exists?(@path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Integer] current width in pixels
|
|
60
|
+
# @see Winfo#width
|
|
61
|
+
def width
|
|
62
|
+
@app.winfo.width(@path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Integer] current height in pixels
|
|
66
|
+
# @see Winfo#height
|
|
67
|
+
def height
|
|
68
|
+
@app.winfo.height(@path)
|
|
55
69
|
end
|
|
56
70
|
|
|
57
71
|
# Pack this widget.
|
|
@@ -88,6 +102,15 @@ module Teek
|
|
|
88
102
|
@app.unbind(@path, event)
|
|
89
103
|
end
|
|
90
104
|
|
|
105
|
+
# Register a handler for this window's close button (WM_DELETE_WINDOW).
|
|
106
|
+
# Meant for toplevels; see {App#on_close} for the full behavior.
|
|
107
|
+
# @yield called when the window's close button is pressed
|
|
108
|
+
# @return [void]
|
|
109
|
+
# @see App#on_close
|
|
110
|
+
def on_close(&block)
|
|
111
|
+
@app.on_close(window: @path, &block)
|
|
112
|
+
end
|
|
113
|
+
|
|
91
114
|
def inspect
|
|
92
115
|
"#<Teek::Widget #{@path}>"
|
|
93
116
|
end
|
data/lib/teek/winfo.rb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# Thin, typed wrapper around Tk's `winfo` command family - one method
|
|
5
|
+
# per subquery, coerced to the right Ruby type, reached via {App#winfo}.
|
|
6
|
+
#
|
|
7
|
+
# Grouped behind a single accessor instead of a dozen-plus flat App
|
|
8
|
+
# methods, since `winfo` is itself one big, well-known Tcl command
|
|
9
|
+
# namespace - knowing Tcl's `winfo width` gets you to {#width} directly.
|
|
10
|
+
# Every method accepts a path String or anything that responds to
|
|
11
|
+
# +to_s+ with one (a {Widget}, for instance).
|
|
12
|
+
#
|
|
13
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/winfo.htm winfo
|
|
14
|
+
class Winfo
|
|
15
|
+
# @api private
|
|
16
|
+
def initialize(app)
|
|
17
|
+
@app = app
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param path [String, Widget]
|
|
21
|
+
# @return [Integer] current width in pixels
|
|
22
|
+
def width(path)
|
|
23
|
+
query('width', path).to_i
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param path [String, Widget]
|
|
27
|
+
# @return [Integer] current height in pixels
|
|
28
|
+
def height(path)
|
|
29
|
+
query('height', path).to_i
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param path [String, Widget]
|
|
33
|
+
# @return [Integer] requested (natural) width in pixels
|
|
34
|
+
def reqwidth(path)
|
|
35
|
+
query('reqwidth', path).to_i
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param path [String, Widget]
|
|
39
|
+
# @return [Integer] requested (natural) height in pixels
|
|
40
|
+
def reqheight(path)
|
|
41
|
+
query('reqheight', path).to_i
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param path [String, Widget]
|
|
45
|
+
# @return [Integer] x coordinate of the window's top-left corner, in screen pixels
|
|
46
|
+
def rootx(path)
|
|
47
|
+
query('rootx', path).to_i
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param path [String, Widget]
|
|
51
|
+
# @return [Integer] y coordinate of the window's top-left corner, in screen pixels
|
|
52
|
+
def rooty(path)
|
|
53
|
+
query('rooty', path).to_i
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param path [String, Widget]
|
|
57
|
+
# @return [Integer] x coordinate relative to the parent widget
|
|
58
|
+
def x(path)
|
|
59
|
+
query('x', path).to_i
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param path [String, Widget]
|
|
63
|
+
# @return [Integer] y coordinate relative to the parent widget
|
|
64
|
+
def y(path)
|
|
65
|
+
query('y', path).to_i
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param path [String, Widget] any window on the same screen (default: the root window)
|
|
69
|
+
# @return [Integer] the mouse pointer's current x coordinate, in screen pixels
|
|
70
|
+
def pointerx(path = '.')
|
|
71
|
+
query('pointerx', path).to_i
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param path [String, Widget] any window on the same screen (default: the root window)
|
|
75
|
+
# @return [Integer] the mouse pointer's current y coordinate, in screen pixels
|
|
76
|
+
def pointery(path = '.')
|
|
77
|
+
query('pointery', path).to_i
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param path [String, Widget]
|
|
81
|
+
# @return [Boolean] whether a window currently exists at +path+
|
|
82
|
+
def exists?(path)
|
|
83
|
+
query('exists', path) == '1'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param path [String, Widget]
|
|
87
|
+
# @return [String] the Tk widget class (e.g. +"TButton"+, +"Frame"+)
|
|
88
|
+
def class_name(path)
|
|
89
|
+
query('class', path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param path [String, Widget]
|
|
93
|
+
# @return [Boolean] whether the window is currently mapped (actually displayed)
|
|
94
|
+
def ismapped?(path)
|
|
95
|
+
query('ismapped', path) == '1'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def query(subcommand, path)
|
|
101
|
+
@app.tcl_invoke('winfo', subcommand, path.to_s)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/teek/wm.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# Thin, typed wrapper around Tk's `wm` (window manager) command family -
|
|
5
|
+
# one method per subcommand, coerced to the right Ruby type, reached via
|
|
6
|
+
# {App#wm}.
|
|
7
|
+
#
|
|
8
|
+
# Grouped behind a single accessor instead of more flat App methods, for
|
|
9
|
+
# the same reason {Winfo} is: `wm` is itself one big, well-known Tcl
|
|
10
|
+
# command namespace, so knowing Tcl's `wm title` gets you to {#title}
|
|
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.
|
|
19
|
+
#
|
|
20
|
+
# @see https://www.tcl-lang.org/man/tcl9.0/TkCmd/wm.htm wm
|
|
21
|
+
class Wm
|
|
22
|
+
# @api private
|
|
23
|
+
def initialize(app)
|
|
24
|
+
@app = app
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param window [String, Widget] (default: the root window)
|
|
28
|
+
# @return [String] the window's current title
|
|
29
|
+
def title(window: '.')
|
|
30
|
+
@app.tcl_invoke('wm', 'title', window.to_s)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param value [String] new title
|
|
34
|
+
# @param window [String, Widget] (default: the root window)
|
|
35
|
+
# @return [String] the title
|
|
36
|
+
def set_title(value, window: '.')
|
|
37
|
+
@app.tcl_invoke('wm', 'title', window.to_s, value.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param window [String, Widget] (default: the root window)
|
|
41
|
+
# @return [String] geometry string (e.g. +"400x300+0+0"+)
|
|
42
|
+
def geometry(window: '.')
|
|
43
|
+
@app.tcl_invoke('wm', 'geometry', window.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param value [String] new geometry (e.g. +"400x300"+, +"400x300+100+50"+)
|
|
47
|
+
# @param window [String, Widget] (default: the root window)
|
|
48
|
+
# @return [String] the geometry
|
|
49
|
+
def set_geometry(value, window: '.')
|
|
50
|
+
@app.tcl_invoke('wm', 'geometry', window.to_s, value.to_s)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param window [String, Widget] (default: the root window)
|
|
54
|
+
# @return [Array(Boolean, Boolean)] [width_resizable, height_resizable]
|
|
55
|
+
def resizable(window: '.')
|
|
56
|
+
parts = @app.tcl_invoke('wm', 'resizable', window.to_s).split
|
|
57
|
+
[@app.tcl_to_bool(parts[0]), @app.tcl_to_bool(parts[1])]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param width [Boolean] allow horizontal resize
|
|
61
|
+
# @param height [Boolean] allow vertical resize
|
|
62
|
+
# @param window [String, Widget] (default: the root window)
|
|
63
|
+
# @return [void]
|
|
64
|
+
def set_resizable(width, height, window: '.')
|
|
65
|
+
@app.tcl_invoke('wm', 'resizable', window.to_s, @app.bool_to_tcl(width), @app.bool_to_tcl(height))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Show a window (map it if withdrawn/iconified).
|
|
69
|
+
# @param window [String, Widget] (default: the root window)
|
|
70
|
+
# @return [void]
|
|
71
|
+
def deiconify(window: '.')
|
|
72
|
+
@app.tcl_invoke('wm', 'deiconify', window.to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Hide a window without destroying it.
|
|
76
|
+
# @param window [String, Widget] (default: the root window)
|
|
77
|
+
# @return [void]
|
|
78
|
+
def withdraw(window: '.')
|
|
79
|
+
@app.tcl_invoke('wm', 'withdraw', window.to_s)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|