teek-ui 0.1.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 +7 -0
- data/CHANGELOG.md +41 -0
- data/README.md +452 -0
- data/lib/teek/ui/canvas_item.rb +270 -0
- data/lib/teek/ui/component_handle.rb +32 -0
- data/lib/teek/ui/document.rb +185 -0
- data/lib/teek/ui/errors.rb +41 -0
- data/lib/teek/ui/event_binding.rb +22 -0
- data/lib/teek/ui/event_bus.rb +53 -0
- data/lib/teek/ui/grid_validator.rb +89 -0
- data/lib/teek/ui/handle.rb +585 -0
- data/lib/teek/ui/image.rb +58 -0
- data/lib/teek/ui/keysyms.rb +72 -0
- data/lib/teek/ui/menu_builder.rb +130 -0
- data/lib/teek/ui/menu_entry_addressing.rb +62 -0
- data/lib/teek/ui/modal_stack.rb +99 -0
- data/lib/teek/ui/mouse_events.rb +31 -0
- data/lib/teek/ui/node.rb +141 -0
- data/lib/teek/ui/option_dump_parsing.rb +31 -0
- data/lib/teek/ui/overlay_anchors.rb +29 -0
- data/lib/teek/ui/overlay_validator.rb +33 -0
- data/lib/teek/ui/pane_validator.rb +30 -0
- data/lib/teek/ui/realized_node.rb +25 -0
- data/lib/teek/ui/realizer.rb +577 -0
- data/lib/teek/ui/scope.rb +40 -0
- data/lib/teek/ui/screens.rb +141 -0
- data/lib/teek/ui/session.rb +417 -0
- data/lib/teek/ui/tab_validator.rb +30 -0
- data/lib/teek/ui/text_content.rb +321 -0
- data/lib/teek/ui/tree_inspector.rb +97 -0
- data/lib/teek/ui/validator.rb +131 -0
- data/lib/teek/ui/var.rb +98 -0
- data/lib/teek/ui/version.rb +7 -0
- data/lib/teek/ui/widget_addressing.rb +47 -0
- data/lib/teek/ui/widget_dsl.rb +512 -0
- data/lib/teek/ui/widget_type.rb +242 -0
- data/lib/teek/ui/widget_types/button.rb +7 -0
- data/lib/teek/ui/widget_types/canvas.rb +15 -0
- data/lib/teek/ui/widget_types/checkbox.rb +7 -0
- data/lib/teek/ui/widget_types/column.rb +14 -0
- data/lib/teek/ui/widget_types/context_menu.rb +15 -0
- data/lib/teek/ui/widget_types/divider.rb +10 -0
- data/lib/teek/ui/widget_types/dropdown.rb +7 -0
- data/lib/teek/ui/widget_types/grid.rb +12 -0
- data/lib/teek/ui/widget_types/group.rb +7 -0
- data/lib/teek/ui/widget_types/label.rb +7 -0
- data/lib/teek/ui/widget_types/list.rb +7 -0
- data/lib/teek/ui/widget_types/menu_bar.rb +26 -0
- data/lib/teek/ui/widget_types/menu_checkbox.rb +14 -0
- data/lib/teek/ui/widget_types/menu_item.rb +20 -0
- data/lib/teek/ui/widget_types/menu_radio.rb +14 -0
- data/lib/teek/ui/widget_types/number_box.rb +7 -0
- data/lib/teek/ui/widget_types/pane.rb +37 -0
- data/lib/teek/ui/widget_types/panel.rb +7 -0
- data/lib/teek/ui/widget_types/progress.rb +7 -0
- data/lib/teek/ui/widget_types/radio.rb +7 -0
- data/lib/teek/ui/widget_types/row.rb +14 -0
- data/lib/teek/ui/widget_types/scrollable.rb +17 -0
- data/lib/teek/ui/widget_types/slider.rb +7 -0
- data/lib/teek/ui/widget_types/spacer.rb +14 -0
- data/lib/teek/ui/widget_types/split.rb +17 -0
- data/lib/teek/ui/widget_types/tab.rb +35 -0
- data/lib/teek/ui/widget_types/table.rb +9 -0
- data/lib/teek/ui/widget_types/tabs.rb +10 -0
- data/lib/teek/ui/widget_types/text_area.rb +7 -0
- data/lib/teek/ui/widget_types/text_box.rb +7 -0
- data/lib/teek/ui/widget_types/tree.rb +9 -0
- data/lib/teek/ui/widget_types/window.rb +50 -0
- data/lib/teek/ui/widget_types.rb +121 -0
- data/lib/teek/ui/widget_validators.rb +62 -0
- data/lib/teek/ui.rb +54 -0
- data/teek-ui.gemspec +24 -0
- data/test/support/fake_app.rb +57 -0
- data/test/test_busy.rb +82 -0
- data/test/test_canvas_items.rb +551 -0
- data/test/test_clipboard.rb +87 -0
- data/test/test_close_handling.rb +118 -0
- data/test/test_component.rb +151 -0
- data/test/test_component_realtk.rb +120 -0
- data/test/test_debug_info.rb +158 -0
- data/test/test_document.rb +317 -0
- data/test/test_event_bus.rb +163 -0
- data/test/test_events.rb +164 -0
- data/test/test_fake_app_contract.rb +62 -0
- data/test/test_grid.rb +97 -0
- data/test/test_handle.rb +386 -0
- data/test/test_handle_destroy_realtk.rb +344 -0
- data/test/test_helper.rb +20 -0
- data/test/test_image.rb +24 -0
- data/test/test_image_realtk.rb +146 -0
- data/test/test_incremental_realize.rb +203 -0
- data/test/test_keysyms.rb +76 -0
- data/test/test_layout.rb +185 -0
- data/test/test_lazy_realize.rb +128 -0
- data/test/test_managed_window.rb +304 -0
- data/test/test_menu_dsl.rb +230 -0
- data/test/test_menu_entry_addressing.rb +73 -0
- data/test/test_menu_realize.rb +296 -0
- data/test/test_modal_handle.rb +147 -0
- data/test/test_modal_stack.rb +219 -0
- data/test/test_modal_stack_realtk.rb +131 -0
- data/test/test_native_scrollable.rb +250 -0
- data/test/test_node.rb +193 -0
- data/test/test_overlay.rb +139 -0
- data/test/test_raw.rb +58 -0
- data/test/test_reactive_vars.rb +118 -0
- data/test/test_realizer.rb +264 -0
- data/test/test_scope.rb +38 -0
- data/test/test_screens.rb +260 -0
- data/test/test_screens_realtk.rb +314 -0
- data/test/test_scrollable.rb +185 -0
- data/test/test_scrollable_wheel.rb +172 -0
- data/test/test_scrollbar_auto_hide.rb +127 -0
- data/test/test_split.rb +145 -0
- data/test/test_tabs.rb +201 -0
- data/test/test_text_content.rb +664 -0
- data/test/test_toast.rb +126 -0
- data/test/test_tree_inspector.rb +183 -0
- data/test/test_ui.rb +428 -0
- data/test/test_validator.rb +209 -0
- data/test/test_var.rb +32 -0
- data/test/test_widget_dsl.rb +621 -0
- data/test/test_widget_introspection.rb +126 -0
- data/test/test_widget_types.rb +429 -0
- data/test/test_widget_types_realtk.rb +82 -0
- data/test/test_widget_validators.rb +82 -0
- metadata +207 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../widget_type'
|
|
4
|
+
|
|
5
|
+
# `tree` and `table` are two DSL names over the same Tk widget
|
|
6
|
+
# (ttk::treeview, used with/without -show tree) - see table.rb.
|
|
7
|
+
Teek::UI::WidgetTypes.register(
|
|
8
|
+
Teek::UI::WidgetType.new(type: :tree, tk_command: 'ttk::treeview', natively_scrollable: true)
|
|
9
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../widget_type'
|
|
4
|
+
|
|
5
|
+
module Teek
|
|
6
|
+
module UI
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
# A freshly created :window's post-creation setup: title/geometry/
|
|
10
|
+
# resizable setup, transient-to-parent, the macOS shared-menubar quirk
|
|
11
|
+
# (each platform other than macOS gets its own menu bar per window;
|
|
12
|
+
# macOS has a single app-wide menu bar, so without this a new window
|
|
13
|
+
# falls back to Tk's default "wish" menu instead of the parent's), and
|
|
14
|
+
# withdrawn by default - shown explicitly via Handle#show. Registered
|
|
15
|
+
# as :window's own `post_create:` below.
|
|
16
|
+
module WindowRealize
|
|
17
|
+
def self.post_create(app, node, path, parent_path)
|
|
18
|
+
opts = node.opts
|
|
19
|
+
window = app.window(path)
|
|
20
|
+
|
|
21
|
+
window.set_title(opts[:title]) if opts[:title]
|
|
22
|
+
window.set_geometry(opts[:geometry]) if opts[:geometry]
|
|
23
|
+
if opts.key?(:resizable)
|
|
24
|
+
pair = opts[:resizable]
|
|
25
|
+
width, height = pair.is_a?(Array) ? pair : [pair, pair]
|
|
26
|
+
window.set_resizable(width, height)
|
|
27
|
+
end
|
|
28
|
+
app.command(:wm, :transient, path, parent_path) unless opts[:transient] == false
|
|
29
|
+
share_macos_menu(app, path, parent_path) if Teek.platform.darwin?
|
|
30
|
+
window.withdraw
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.share_macos_menu(app, path, parent_path)
|
|
34
|
+
parent_menu = app.command(parent_path, :cget, '-menu')
|
|
35
|
+
app.command(path, :configure, menu: parent_menu) unless parent_menu.nil? || parent_menu.empty?
|
|
36
|
+
rescue Teek::TclError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A toplevel - placed by the window manager, never pack/grid-managed by
|
|
44
|
+
# its nominal parent.
|
|
45
|
+
Teek::UI::WidgetTypes.register(
|
|
46
|
+
Teek::UI::WidgetType.new(
|
|
47
|
+
type: :window, tk_command: 'toplevel', leaf: false, arranged: false,
|
|
48
|
+
post_create: Teek::UI::WindowRealize.method(:post_create)
|
|
49
|
+
)
|
|
50
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'widget_type'
|
|
4
|
+
require_relative 'widget_validators'
|
|
5
|
+
|
|
6
|
+
module Teek
|
|
7
|
+
module UI
|
|
8
|
+
# @api private
|
|
9
|
+
#
|
|
10
|
+
# Central registry of {WidgetType} descriptors, mirroring
|
|
11
|
+
# {Teek::CommandInterceptors}'s own register/for_type shape - one
|
|
12
|
+
# registered descriptor per node type is the single source of truth
|
|
13
|
+
# {WidgetDSL}, {Realizer}, and {Validator} each dispatch to for that
|
|
14
|
+
# type's own build/realize/validate behavior.
|
|
15
|
+
#
|
|
16
|
+
# Unlike {Teek::CommandInterceptors} (consulted per-call with a type
|
|
17
|
+
# already in hand) or {WidgetValidators} (many validators can share one
|
|
18
|
+
# type), exactly one descriptor owns a given type here, and consumers
|
|
19
|
+
# need the full set up front to drive codegen - so this adds two things
|
|
20
|
+
# CommandInterceptors doesn't need: {.each} (enumerate every registered
|
|
21
|
+
# type) and {.on_register} (subscribe to every past AND future
|
|
22
|
+
# registration, so load order between this file and a consumer like
|
|
23
|
+
# {WidgetDSL} never matters).
|
|
24
|
+
#
|
|
25
|
+
# A descriptor's +validator:+ (see {WidgetType}) is forwarded into
|
|
26
|
+
# {WidgetValidators} right here, at registration time, so it's
|
|
27
|
+
# dispatched through the exact same +WidgetValidators.for_type+ call
|
|
28
|
+
# every other validator goes through - {Validator} itself carries no
|
|
29
|
+
# awareness of descriptors at all.
|
|
30
|
+
class WidgetTypes
|
|
31
|
+
class << self
|
|
32
|
+
# @param widget_type [WidgetType]
|
|
33
|
+
# @return [WidgetType] +widget_type+, for chaining
|
|
34
|
+
# @raise [ArgumentError] if +widget_type.type+ is already registered
|
|
35
|
+
def register(widget_type)
|
|
36
|
+
key = widget_type.type.to_s
|
|
37
|
+
if types.key?(key)
|
|
38
|
+
raise ArgumentError, "widget type :#{widget_type.type} is already registered"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
types[key] = widget_type
|
|
42
|
+
if widget_type.validator
|
|
43
|
+
WidgetValidators.register(widget_type.type) { |*args| widget_type.validator.call(*args) }
|
|
44
|
+
end
|
|
45
|
+
callbacks.each { |callback| callback.call(widget_type) }
|
|
46
|
+
|
|
47
|
+
widget_type
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param type [Symbol, String]
|
|
51
|
+
# @return [WidgetType, nil]
|
|
52
|
+
def for_type(type)
|
|
53
|
+
types[type.to_s]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @yieldparam widget_type [WidgetType]
|
|
57
|
+
# @return [Enumerator] if no block given
|
|
58
|
+
def each(&block)
|
|
59
|
+
return enum_for(:each) unless block
|
|
60
|
+
|
|
61
|
+
types.each_value(&block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Subscribes +block+ to every type registered from now on, and
|
|
65
|
+
# immediately replays every type already registered - so a
|
|
66
|
+
# subscriber (namely {WidgetDSL}'s own codegen) sees every type
|
|
67
|
+
# regardless of whether it loaded before or after this file
|
|
68
|
+
# populated its own built-ins.
|
|
69
|
+
# @yieldparam widget_type [WidgetType]
|
|
70
|
+
# @return [void]
|
|
71
|
+
def on_register(&block)
|
|
72
|
+
types.each_value(&block)
|
|
73
|
+
callbacks << block
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def types
|
|
79
|
+
@types ||= {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def callbacks
|
|
83
|
+
@callbacks ||= []
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
require_relative 'widget_types/divider'
|
|
91
|
+
require_relative 'widget_types/text_box'
|
|
92
|
+
require_relative 'widget_types/text_area'
|
|
93
|
+
require_relative 'widget_types/label'
|
|
94
|
+
require_relative 'widget_types/button'
|
|
95
|
+
require_relative 'widget_types/checkbox'
|
|
96
|
+
require_relative 'widget_types/radio'
|
|
97
|
+
require_relative 'widget_types/slider'
|
|
98
|
+
require_relative 'widget_types/dropdown'
|
|
99
|
+
require_relative 'widget_types/number_box'
|
|
100
|
+
require_relative 'widget_types/list'
|
|
101
|
+
require_relative 'widget_types/table'
|
|
102
|
+
require_relative 'widget_types/tree'
|
|
103
|
+
require_relative 'widget_types/progress'
|
|
104
|
+
require_relative 'widget_types/panel'
|
|
105
|
+
require_relative 'widget_types/group'
|
|
106
|
+
require_relative 'widget_types/canvas'
|
|
107
|
+
require_relative 'widget_types/window'
|
|
108
|
+
require_relative 'widget_types/column'
|
|
109
|
+
require_relative 'widget_types/row'
|
|
110
|
+
require_relative 'widget_types/spacer'
|
|
111
|
+
require_relative 'widget_types/grid'
|
|
112
|
+
require_relative 'widget_types/scrollable'
|
|
113
|
+
require_relative 'widget_types/tabs'
|
|
114
|
+
require_relative 'widget_types/tab'
|
|
115
|
+
require_relative 'widget_types/split'
|
|
116
|
+
require_relative 'widget_types/pane'
|
|
117
|
+
require_relative 'widget_types/menu_bar'
|
|
118
|
+
require_relative 'widget_types/context_menu'
|
|
119
|
+
require_relative 'widget_types/menu_item'
|
|
120
|
+
require_relative 'widget_types/menu_checkbox'
|
|
121
|
+
require_relative 'widget_types/menu_radio'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
module UI
|
|
5
|
+
# @api private
|
|
6
|
+
#
|
|
7
|
+
# Registry of per-node-type validators, mirroring {Teek::CommandInterceptors}'
|
|
8
|
+
# own register(type, ...)/for_type(type) shape. {Validator} (the
|
|
9
|
+
# coordinator) walks the whole {Document} exactly once; at each node it
|
|
10
|
+
# looks up +WidgetValidators.for_type(node.type)+ and calls every
|
|
11
|
+
# validator registered there.
|
|
12
|
+
#
|
|
13
|
+
# A registered validator receives +(node, parent, document, errors)+ and
|
|
14
|
+
# must APPEND problem strings to +errors+ - it must never raise itself.
|
|
15
|
+
# Unlike a {Teek::CommandInterceptors} entry (which "claims" a call and
|
|
16
|
+
# returns its result), a widget validator has nothing to return and
|
|
17
|
+
# nothing to claim exclusively - multiple validators for the same type
|
|
18
|
+
# can coexist freely, and there's no ambiguity to detect.
|
|
19
|
+
#
|
|
20
|
+
# Registering a validator for a type is purely additive: a custom or
|
|
21
|
+
# third-party widget can call {.register} to get its own contract
|
|
22
|
+
# checked without editing this file or {Validator}, the same way a
|
|
23
|
+
# custom widget can register a {Teek::CommandInterceptors} entry without
|
|
24
|
+
# editing +teek.rb+.
|
|
25
|
+
class WidgetValidators
|
|
26
|
+
class << self
|
|
27
|
+
# @param type [Symbol, String] the node type this validator applies to
|
|
28
|
+
# @yieldparam node [Node]
|
|
29
|
+
# @yieldparam parent [Node, nil]
|
|
30
|
+
# @yieldparam document [Document]
|
|
31
|
+
# @yieldparam errors [Array<String>]
|
|
32
|
+
# @return [void]
|
|
33
|
+
def register(type, &block)
|
|
34
|
+
validators[type.to_s] << block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param type [Symbol, String]
|
|
38
|
+
# @return [Array<Proc>] every validator registered for +type+, in
|
|
39
|
+
# registration order - empty if none are
|
|
40
|
+
def for_type(type)
|
|
41
|
+
validators[type.to_s]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Shared node-describing helper every validator's error messages use
|
|
45
|
+
# ("#label(:name)" or "an unnamed #label") - kept here rather than
|
|
46
|
+
# duplicated per validator file, since every one of them needs it.
|
|
47
|
+
# @param node [Node, nil]
|
|
48
|
+
# @return [String]
|
|
49
|
+
def describe(node)
|
|
50
|
+
return 'the document root' unless node
|
|
51
|
+
node.name ? "##{node.type}(:#{node.name})" : "an unnamed ##{node.type}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validators
|
|
57
|
+
@validators ||= Hash.new { |h, k| h[k] = [] }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/teek/ui.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "teek"
|
|
4
|
+
require_relative "ui/version"
|
|
5
|
+
require_relative "ui/event_binding"
|
|
6
|
+
require_relative "ui/session"
|
|
7
|
+
require_relative "ui/tree_inspector"
|
|
8
|
+
|
|
9
|
+
module Teek
|
|
10
|
+
# A DSL for building teek (Tk) apps - sugar over teek, not a wall around it.
|
|
11
|
+
# Everything here compiles down to plain teek calls, and every handle keeps
|
|
12
|
+
# an escape hatch back to the underlying {Teek::App}.
|
|
13
|
+
module UI
|
|
14
|
+
class << self
|
|
15
|
+
# @return [Boolean] whether a bare list/text_area/table/tree
|
|
16
|
+
# auto-attaches a scrollbar with no `ui.scrollable` wrapper needed -
|
|
17
|
+
# true by default. Three levels can override it, most specific
|
|
18
|
+
# wins: a widget's own `scroll:` option, then `Teek::UI.app`'s own
|
|
19
|
+
# `scroll:`, then this global default.
|
|
20
|
+
attr_accessor :auto_scroll
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] the same default, but for canvas specifically -
|
|
23
|
+
# false by default, since a canvas is as often fixed drawing as
|
|
24
|
+
# scrollable content, unlike the other native types.
|
|
25
|
+
attr_accessor :auto_scroll_canvas
|
|
26
|
+
end
|
|
27
|
+
self.auto_scroll = true
|
|
28
|
+
self.auto_scroll_canvas = false
|
|
29
|
+
|
|
30
|
+
# Build an app. Constructs a {Session} (Tk-free - no {Teek::App} exists
|
|
31
|
+
# yet), yields it to the block, and returns that same session so
|
|
32
|
+
# `.run`/`.run_async` can be chained directly off the call. The
|
|
33
|
+
# underlying app is created lazily, at realize (see {Session#realize}).
|
|
34
|
+
#
|
|
35
|
+
# @param title [String, nil] window title
|
|
36
|
+
# @param scroll [Boolean, nil] app-wide override for whether native
|
|
37
|
+
# scrollable widgets auto-attach a scrollbar - between a widget's own
|
|
38
|
+
# `scroll:` (most specific) and {.auto_scroll}/{.auto_scroll_canvas}
|
|
39
|
+
# (global default). `nil` (the default) defers straight to the global.
|
|
40
|
+
# @param app_opts [Hash] forwarded to {Teek::App.new} at realize (e.g. +debug:+, +track_widgets:+)
|
|
41
|
+
# @yieldparam ui [Session]
|
|
42
|
+
# @return [Session]
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# Teek::UI.app(title: "Hello") do |ui|
|
|
46
|
+
# # widget/layout DSL calls go here
|
|
47
|
+
# end.run
|
|
48
|
+
def self.app(title: nil, scroll: nil, **app_opts, &block)
|
|
49
|
+
session = Session.new(title: title, scroll: scroll, app_opts: app_opts)
|
|
50
|
+
block.call(session) if block
|
|
51
|
+
session
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/teek-ui.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative "lib/teek/ui/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "teek-ui"
|
|
5
|
+
spec.version = Teek::UI::VERSION
|
|
6
|
+
spec.authors = ["James Cook"]
|
|
7
|
+
spec.email = ["jcook.rubyist@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "The friendly way to build Tk apps in Ruby"
|
|
10
|
+
spec.description = "Declarative widgets, layout, events, and reactive state on top of teek"
|
|
11
|
+
spec.homepage = "https://github.com/jamescook/teek"
|
|
12
|
+
spec.licenses = ["MIT"]
|
|
13
|
+
|
|
14
|
+
spec.files = Dir.glob("{lib,test}/**/*").select { |f|
|
|
15
|
+
File.file?(f) && f !~ /\.log$/
|
|
16
|
+
} + %w[teek-ui.gemspec CHANGELOG.md README.md]
|
|
17
|
+
spec.require_paths = ["lib"]
|
|
18
|
+
spec.required_ruby_version = ">= 3.2"
|
|
19
|
+
|
|
20
|
+
spec.add_dependency "teek", "~> 0.3"
|
|
21
|
+
|
|
22
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
23
|
+
spec.add_development_dependency "minitest", "~> 6.0"
|
|
24
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A minimal stand-in for Teek::App, for headless tests that need
|
|
4
|
+
# something Realizer/Handle-shaped without a real Tk interpreter -
|
|
5
|
+
# Realizer#create/#link/#realize_subtree only ever call .command/.bind/
|
|
6
|
+
# .on_close (and, for a menu/window handle, .popup_menu/.window) on
|
|
7
|
+
# whatever app they're given, so a fake recording every call is enough
|
|
8
|
+
# to assert on exactly what WOULD have happened against real Tk. Shared
|
|
9
|
+
# across every headless suite that needs one, so there's a single
|
|
10
|
+
# definition to keep in sync with the real Teek::App/Teek::Window
|
|
11
|
+
# signatures (see test_fake_app_contract.rb) rather than one per file.
|
|
12
|
+
FakeWindow = Struct.new(:path, :modal_calls, :grab_releases) do
|
|
13
|
+
def initialize(path, modal_calls = [], grab_releases = [])
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def modal(global: false, &block)
|
|
18
|
+
modal_calls << { global: global }
|
|
19
|
+
block.call if block
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def grab_release
|
|
23
|
+
grab_releases << true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
FakeApp = Struct.new(:calls, :binds, :on_closes, :popups, :windows) do
|
|
28
|
+
def initialize(calls = [], binds = [], on_closes = [], popups = [], windows = [])
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def command(cmd, *args, **kwargs)
|
|
33
|
+
calls << [[cmd, *args], kwargs]
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bind(path, event, *subs, &block)
|
|
38
|
+
binds << { path: path, event: event, subs: subs, block: block }
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def on_close(window:, &block)
|
|
43
|
+
on_closes << { window: window, block: block }
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def popup_menu(menu, x:, y:, entry: nil)
|
|
48
|
+
popups << { menu: menu, x: x, y: y, entry: entry }
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def window(path = '.')
|
|
53
|
+
win = FakeWindow.new(path)
|
|
54
|
+
windows << win
|
|
55
|
+
win
|
|
56
|
+
end
|
|
57
|
+
end
|
data/test/test_busy.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../../test/tk_test_helper'
|
|
5
|
+
|
|
6
|
+
# session.busy is a thin, realize-only delegation to Teek::App#busy (see
|
|
7
|
+
# teek core's own test/test_busy.rb for the underlying busy-cursor
|
|
8
|
+
# set/clear behavior, including exception safety). What's new to verify
|
|
9
|
+
# at this layer: the delegation itself (window:, block value, exception
|
|
10
|
+
# safety all still work through Session), and that it raises before
|
|
11
|
+
# realize like every other realize-only Session method.
|
|
12
|
+
class TestBusy < Minitest::Test
|
|
13
|
+
include TeekTestHelper
|
|
14
|
+
|
|
15
|
+
def test_busy_raises_before_realize
|
|
16
|
+
assert_tk_app("session.busy should raise a clear error before realize, not queue") do
|
|
17
|
+
require 'teek/ui'
|
|
18
|
+
|
|
19
|
+
session = Teek::UI.app(title: 'Busy Test')
|
|
20
|
+
|
|
21
|
+
assert_raises(Teek::UI::NotRealizedError) { session.busy { } }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_busy_sets_and_clears_busy_state_and_returns_the_blocks_value
|
|
26
|
+
assert_tk_app("session.busy should show the busy cursor for the duration of the block, then clear it") do
|
|
27
|
+
require 'teek/ui'
|
|
28
|
+
|
|
29
|
+
session = Teek::UI.app(title: 'Busy Test')
|
|
30
|
+
session.run_async
|
|
31
|
+
session.app.update
|
|
32
|
+
|
|
33
|
+
was_busy = nil
|
|
34
|
+
result = session.busy {
|
|
35
|
+
was_busy = session.app.tcl_eval('tk busy status .') == '1'
|
|
36
|
+
42
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
assert was_busy, "expected the window to be busy during the block"
|
|
40
|
+
assert_equal '0', session.app.tcl_eval('tk busy status .')
|
|
41
|
+
assert_equal 42, result
|
|
42
|
+
|
|
43
|
+
session.app.destroy
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_busy_clears_on_exception
|
|
48
|
+
assert_tk_app("session.busy should clear the busy cursor even if the block raises") do
|
|
49
|
+
require 'teek/ui'
|
|
50
|
+
|
|
51
|
+
session = Teek::UI.app(title: 'Busy Test')
|
|
52
|
+
session.run_async
|
|
53
|
+
session.app.update
|
|
54
|
+
|
|
55
|
+
assert_raises(RuntimeError) { session.busy { raise 'boom' } }
|
|
56
|
+
assert_equal '0', session.app.tcl_eval('tk busy status .')
|
|
57
|
+
|
|
58
|
+
session.app.destroy
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_busy_works_on_a_specific_window
|
|
63
|
+
assert_tk_app("session.busy's window: should target that window, not just the root") do
|
|
64
|
+
require 'teek/ui'
|
|
65
|
+
|
|
66
|
+
session = Teek::UI.app(title: 'Busy Test') { |ui| ui.window(:extra) }
|
|
67
|
+
session.run_async
|
|
68
|
+
session[:extra].show
|
|
69
|
+
session.app.update
|
|
70
|
+
extra_path = session[:extra].path
|
|
71
|
+
|
|
72
|
+
was_busy = nil
|
|
73
|
+
session.busy(window: extra_path) {
|
|
74
|
+
was_busy = session.app.tcl_eval("tk busy status #{extra_path}") == '1'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
assert was_busy, "expected the extra window to be busy during the block"
|
|
78
|
+
|
|
79
|
+
session.app.destroy
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|