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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teek
4
- VERSION = "0.1.5"
4
+ VERSION = "0.2.0"
5
5
  end
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.tcl_eval("winfo exists #{@path}") == '1'
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