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,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'widget_validators'
|
|
4
|
+
|
|
5
|
+
module Teek
|
|
6
|
+
module UI
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
# The opposite direction from {WidgetDSL#overlay}'s own guard: a node
|
|
10
|
+
# carrying overlay placement (+layout[:overlay]+) whose actual parent
|
|
11
|
+
# isn't a :canvas at all - only reachable via direct Node/Document
|
|
12
|
+
# manipulation, since {WidgetDSL#overlay} already refuses to run
|
|
13
|
+
# outside a ui.canvas block. Mirrors {GridValidator.check_stray_cell}
|
|
14
|
+
# exactly; overlay intent can land on any node type (whatever
|
|
15
|
+
# #overlay's block happens to build), so like that check - and unlike
|
|
16
|
+
# a type-dispatched {WidgetValidators} entry - this can't be keyed off
|
|
17
|
+
# a single node type. {Validator} calls this directly for every node
|
|
18
|
+
# in the same single tree walk.
|
|
19
|
+
module OverlayValidator
|
|
20
|
+
# @param node [Node]
|
|
21
|
+
# @param parent [Node, nil]
|
|
22
|
+
# @param errors [Array<String>]
|
|
23
|
+
# @return [void]
|
|
24
|
+
def self.check_stray_overlay(node, parent, errors)
|
|
25
|
+
return unless node.layout && node.layout[:overlay]
|
|
26
|
+
return if parent && parent.type == :canvas
|
|
27
|
+
|
|
28
|
+
errors << "#{WidgetValidators.describe(node)} has an overlay position but its parent " \
|
|
29
|
+
"(#{WidgetValidators.describe(parent)}) isn't a ui.canvas - its placement would be silently ignored"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'widget_validators'
|
|
4
|
+
|
|
5
|
+
module Teek
|
|
6
|
+
module UI
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
# A pane's own contract: it must be declared directly inside a
|
|
10
|
+
# ui.split. Only reachable via direct Node/Document manipulation,
|
|
11
|
+
# since {WidgetDSL#pane} already refuses to run outside a ui.split
|
|
12
|
+
# block - the same defense-in-depth {TabValidator} does for tabs.
|
|
13
|
+
# Composed into {WidgetValidators} via :pane's own {WidgetType#validator}
|
|
14
|
+
# (see +widget_types/pane.rb+).
|
|
15
|
+
module PaneValidator
|
|
16
|
+
# @param node [Node] a :pane node - {WidgetValidators} only dispatches
|
|
17
|
+
# here for that type
|
|
18
|
+
# @param parent [Node, nil]
|
|
19
|
+
# @param document [Document]
|
|
20
|
+
# @param errors [Array<String>] appended to, never raised
|
|
21
|
+
# @return [void]
|
|
22
|
+
def self.call(node, parent, document, errors)
|
|
23
|
+
return if parent && parent.type == :split
|
|
24
|
+
|
|
25
|
+
errors << "#{WidgetValidators.describe(node)} is a :pane but its parent " \
|
|
26
|
+
"(#{WidgetValidators.describe(parent)}) isn't a ui.split"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
module UI
|
|
5
|
+
# What a {Node} gets in its +realized+ slot once the realizer creates a
|
|
6
|
+
# live widget for it: which app owns it, its live Tk path (what a
|
|
7
|
+
# {Handle} acts on - +#configure+, event bindings, +on_close+, ...),
|
|
8
|
+
# and the path its parent's own layout should actually place
|
|
9
|
+
# (+arrange_path+, defaulting to the same as +path+).
|
|
10
|
+
#
|
|
11
|
+
# These two paths only diverge for a node the realizer auto-wraps in a
|
|
12
|
+
# scrollbar (a bare list/text_area/table/tree/canvas, when scrolling
|
|
13
|
+
# applies - see Realizer#create_native_scrollable): +path+ stays the
|
|
14
|
+
# real widget, so a {Handle} keeps acting on it directly, but the
|
|
15
|
+
# widget's actual Tk *parent* is now the wrapper frame the scrollbar
|
|
16
|
+
# lives in - +arrange_path+ points there instead, since that's what
|
|
17
|
+
# has to be packed/gridded into the surrounding layout.
|
|
18
|
+
RealizedNode = Data.define(:app, :path, :arrange_path) do
|
|
19
|
+
# @api private
|
|
20
|
+
def initialize(app:, path:, arrange_path: path)
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'realized_node'
|
|
4
|
+
require_relative 'widget_types'
|
|
5
|
+
require_relative 'overlay_anchors'
|
|
6
|
+
|
|
7
|
+
module Teek
|
|
8
|
+
module UI
|
|
9
|
+
# Walks a {Document} and realizes it into a live {Teek::App} - two
|
|
10
|
+
# passes (Resolved decision #4 in the architecture doc):
|
|
11
|
+
#
|
|
12
|
+
# 1. +create+ - creates every widget, allocates a hierarchical/meaningful
|
|
13
|
+
# Tk path per node, fills each node's +realized+ slot.
|
|
14
|
+
# 2. +link+ - applies (placeholder, see below) geometry and wires event
|
|
15
|
+
# bindings, resolving +target:+ references by name. Runs after
|
|
16
|
+
# +create+ has finished the WHOLE tree, so a target declared later in
|
|
17
|
+
# the build already has a live path by the time it's looked up - that
|
|
18
|
+
# ordering is what makes forward references work.
|
|
19
|
+
#
|
|
20
|
+
# Every widget creation and mutation goes through {Teek::App#command}, so
|
|
21
|
+
# teek's interceptor/leak-cleanup layer applies automatically.
|
|
22
|
+
#
|
|
23
|
+
# @note Layout is real for +:column+/+:row+ (flow packing driven by
|
|
24
|
+
# +gap:+/+align:+/+pad:+ and each child's +grow:+), +:grid+ (real Tk
|
|
25
|
+
# grid arrangement driven by +#cell+/+#stretch+), and a +ui.canvas+
|
|
26
|
+
# child positioned via +#overlay+ (Tk +place+, driven by an anchor -
|
|
27
|
+
# see {OverlayAnchors}) - everything else is still a placeholder,
|
|
28
|
+
# its children just packing top-to-bottom with no options.
|
|
29
|
+
class Realizer
|
|
30
|
+
# DSL-reserved opts keys - layout keywords (gap:/align:/pad:/
|
|
31
|
+
# stretch_columns/stretch_rows) plus other entries the DSL stashes on
|
|
32
|
+
# node.opts for the realizer to pick up later (on_close:, and title:/
|
|
33
|
+
# geometry:/resizable:/transient:/modal: for :window nodes, applied by
|
|
34
|
+
# {WindowRealize}; x:/y: for :scrollable and native-scrollable nodes,
|
|
35
|
+
# applied by #create_scrollable/#create_native_scrollable; scroll:
|
|
36
|
+
# for native-scrollable nodes, applied by #auto_scrollable?; tab_label
|
|
37
|
+
# for :tab nodes, applied by {TabRealize}; pane_weight for :pane
|
|
38
|
+
# nodes, applied by {PaneRealize}) - none of these are real Tk
|
|
39
|
+
# options, so none are ever passed through to a widget-creation call.
|
|
40
|
+
# +:split+'s own +orient:+ isn't reserved - it's a real
|
|
41
|
+
# `ttk::panedwindow` option already, so it passes straight through.
|
|
42
|
+
RESERVED_OPTIONS = %i[
|
|
43
|
+
gap align pad stretch_columns stretch_rows on_close
|
|
44
|
+
title geometry resizable transient modal x y scroll tab_label pane_weight
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
# @api private
|
|
48
|
+
# @param default_scroll [Boolean, nil] app-wide override for whether
|
|
49
|
+
# native scrollable widgets auto-attach a scrollbar - see
|
|
50
|
+
# {Teek::UI.app}'s own +scroll:+. +nil+ defers to the global
|
|
51
|
+
# default ({Teek::UI.auto_scroll}/{Teek::UI.auto_scroll_canvas}).
|
|
52
|
+
def initialize(app, document, default_scroll: nil)
|
|
53
|
+
@app = app
|
|
54
|
+
@document = document
|
|
55
|
+
@default_scroll = default_scroll
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [void]
|
|
59
|
+
def realize
|
|
60
|
+
create(@document.root, '.')
|
|
61
|
+
link(@document.root)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Realize a single already-built (but not-yet-realized) node - and its
|
|
65
|
+
# descendants - into an already-running app, scoped under a parent
|
|
66
|
+
# that's realized already. Reuses the exact same create/link machinery
|
|
67
|
+
# {#realize} uses for the initial tree, just entered at an arbitrary
|
|
68
|
+
# node instead of the document root - see {Session#add}.
|
|
69
|
+
# @param node [Node] freshly built, not yet realized
|
|
70
|
+
# @param parent_node [Node] already realized - node becomes its child
|
|
71
|
+
# @return [void]
|
|
72
|
+
def realize_subtree(node, parent_node)
|
|
73
|
+
create(node, parent_node.realized.path)
|
|
74
|
+
# re-arrange ALL of parent_node's children (old + new), not just the
|
|
75
|
+
# new one in isolation - gap:/align: positioning depends on a
|
|
76
|
+
# child's index relative to every sibling, not just itself.
|
|
77
|
+
arrange_children(parent_node)
|
|
78
|
+
link(node)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Node types with no Tk representation of their own - skipped by
|
|
84
|
+
# create's widget-creation step, and (for :raw_op) by every
|
|
85
|
+
# container's arrangement step too, since they have no realized path.
|
|
86
|
+
NON_WIDGET_TYPES = %i[root raw_op].freeze
|
|
87
|
+
|
|
88
|
+
# {WidgetTypes} only - every node type reaching here is registered
|
|
89
|
+
# (either a real widget type, or handled earlier via
|
|
90
|
+
# {WidgetType#custom_create?} - see #create). Raises for anything
|
|
91
|
+
# else, e.g. a hand-built {Document} carrying a bogus type the DSL
|
|
92
|
+
# itself could never produce.
|
|
93
|
+
def tk_command_for(type)
|
|
94
|
+
registered = WidgetTypes.for_type(type)
|
|
95
|
+
return registered.tk_command if registered
|
|
96
|
+
|
|
97
|
+
raise ArgumentError, "no Tk command mapped for node type :#{type}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def create(node, parent_path)
|
|
101
|
+
registered = WidgetTypes.for_type(node.type)
|
|
102
|
+
|
|
103
|
+
# A type whose entire realize model doesn't fit the generic
|
|
104
|
+
# sequence below at all (e.g. :menu_bar/:context_menu, realized
|
|
105
|
+
# via #create_menu_tree's own bespoke traversal) owns everything
|
|
106
|
+
# from here - no generic widget-creation call, no #post_create, no
|
|
107
|
+
# child-creation/#arrange, no normal #link processing either (see
|
|
108
|
+
# #link, which checks the same flag).
|
|
109
|
+
if registered&.custom_create?
|
|
110
|
+
registered.custom_create(self, node, parent_path)
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if auto_scrollable?(node)
|
|
115
|
+
create_native_scrollable(node, allocate_path(node, parent_path))
|
|
116
|
+
return
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
path =
|
|
120
|
+
if NON_WIDGET_TYPES.include?(node.type)
|
|
121
|
+
parent_path
|
|
122
|
+
else
|
|
123
|
+
allocate_path(node, parent_path)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
unless NON_WIDGET_TYPES.include?(node.type)
|
|
127
|
+
@app.command(tk_command_for(node.type), path, **node.opts.except(*RESERVED_OPTIONS))
|
|
128
|
+
node.realized = RealizedNode.new(app: @app, path: path)
|
|
129
|
+
registered&.post_create(@app, node, path, parent_path)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if registered&.custom_children?
|
|
133
|
+
registered.custom_children(self, node, path)
|
|
134
|
+
else
|
|
135
|
+
node.children.each { |child| create(child, path) unless child.lazy? }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Whether +node+ should auto-attach a scrollbar with no explicit
|
|
140
|
+
# +ui.scrollable+ wrapper - only ever true for a
|
|
141
|
+
# {WidgetType#natively_scrollable?} type, and then only once
|
|
142
|
+
# #resolve_scroll settles the 3-level override (widget's own
|
|
143
|
+
# +scroll:+, then the session's app-wide default, then the global one).
|
|
144
|
+
def auto_scrollable?(node)
|
|
145
|
+
natively_scrollable?(node.type) && resolve_scroll(node)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A registered type's own +natively_scrollable?+ - +false+ for
|
|
149
|
+
# anything unregistered.
|
|
150
|
+
def natively_scrollable?(type)
|
|
151
|
+
WidgetTypes.for_type(type)&.natively_scrollable? || false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A registered type's own {WidgetType#global_scroll_default} (canvas
|
|
155
|
+
# points at +Teek::UI.auto_scroll_canvas+; everything else at the
|
|
156
|
+
# shared +Teek::UI.auto_scroll+) - +auto_scroll+ itself for anything
|
|
157
|
+
# unregistered, since #auto_scrollable? only ever calls this for an
|
|
158
|
+
# already natively-scrollable (and therefore registered) type.
|
|
159
|
+
def resolve_scroll(node)
|
|
160
|
+
opt = node.opts[:scroll]
|
|
161
|
+
return opt unless opt.nil?
|
|
162
|
+
return @default_scroll unless @default_scroll.nil?
|
|
163
|
+
|
|
164
|
+
registered = WidgetTypes.for_type(node.type)
|
|
165
|
+
registered ? registered.global_scroll_default : Teek::UI.auto_scroll
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Wraps a bare native-scrollable widget (list/text_area/table/tree/
|
|
169
|
+
# canvas) in the same wrapper-frame-plus-scrollbar structure
|
|
170
|
+
# +ui.scrollable+'s native case used to build explicitly - except
|
|
171
|
+
# nothing in the DSL asked for it, so it has to happen without
|
|
172
|
+
# disturbing what +node+'s own path means to the rest of the app.
|
|
173
|
+
#
|
|
174
|
+
# +path+ (the node's own allocated path, e.g. `.panel.log`) becomes
|
|
175
|
+
# an invisible wrapper frame instead of the widget itself; the real
|
|
176
|
+
# widget lives one level deeper, at +path+.widget. +node.realized+
|
|
177
|
+
# points at the real widget (so a {Handle}'s +#configure+/events/
|
|
178
|
+
# +#path+ keep acting on it directly, unchanged from the
|
|
179
|
+
# non-scrolling case) - only its +arrange_path+ is the wrapper,
|
|
180
|
+
# since that's the widget's actual Tk parent now, and what the
|
|
181
|
+
# surrounding layout needs to pack/grid in the widget's place. See
|
|
182
|
+
# {RealizedNode}.
|
|
183
|
+
def create_native_scrollable(node, path)
|
|
184
|
+
widget_path = "#{path}.widget"
|
|
185
|
+
tk_command = tk_command_for(node.type)
|
|
186
|
+
|
|
187
|
+
@app.command('ttk::frame', path)
|
|
188
|
+
@app.command(tk_command, widget_path, **node.opts.except(*RESERVED_OPTIONS))
|
|
189
|
+
node.realized = RealizedNode.new(app: @app, path: widget_path, arrange_path: path)
|
|
190
|
+
|
|
191
|
+
wire_scrollbars(path, widget_path, x: node.opts.fetch(:x, false), y: node.opts.fetch(:y, true))
|
|
192
|
+
node.children.each { |child| create(child, widget_path) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# A :scrollable's own widget (created just before this runs) is a
|
|
196
|
+
# plain ttk::frame at +path+ - this fills it in, taking over child
|
|
197
|
+
# creation instead of the generic node.children.each loop #create
|
|
198
|
+
# otherwise uses. There's no Tk protocol to hook a scrollbar into
|
|
199
|
+
# arbitrary widgets (unlike the native-scrollable case above, which
|
|
200
|
+
# is why this is the ONLY thing left for +ui.scrollable+ to do -
|
|
201
|
+
# see #auto_scrollable?), so children are created inside an embedded
|
|
202
|
+
# frame instead - +path+.canvas.viewport - packed inside a canvas
|
|
203
|
+
# that the scrollbar drives. The viewport's own size changes (as its
|
|
204
|
+
# content changes) keep the canvas's -scrollregion in sync; unless
|
|
205
|
+
# horizontal scrolling is on, the canvas's own size changes keep the
|
|
206
|
+
# viewport's width matched to it too, so content isn't left
|
|
207
|
+
# narrower than the visible area.
|
|
208
|
+
def create_scrollable(node, path)
|
|
209
|
+
y = node.opts.fetch(:y, true)
|
|
210
|
+
x = node.opts.fetch(:x, false)
|
|
211
|
+
|
|
212
|
+
canvas_path = "#{path}.canvas"
|
|
213
|
+
viewport_path = "#{canvas_path}.viewport"
|
|
214
|
+
@app.command('canvas', canvas_path, highlightthickness: 0)
|
|
215
|
+
@app.command('ttk::frame', viewport_path)
|
|
216
|
+
window_id = @app.command(canvas_path, :create, :window, 0, 0, window: viewport_path, anchor: 'nw')
|
|
217
|
+
|
|
218
|
+
node.children.each { |grandchild| create(grandchild, viewport_path) }
|
|
219
|
+
|
|
220
|
+
@app.bind(viewport_path, '<Configure>') {
|
|
221
|
+
@app.command(canvas_path, :configure, scrollregion: @app.command(canvas_path, :bbox, :all))
|
|
222
|
+
}
|
|
223
|
+
unless x
|
|
224
|
+
@app.bind(canvas_path, '<Configure>', :width) { |width|
|
|
225
|
+
@app.command(canvas_path, :itemconfigure, window_id, width: width)
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
wire_scrollbars(path, canvas_path, x: x, y: y)
|
|
230
|
+
wire_wheel_scroll(canvas_path, viewport_path, node, x: x, y: y)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The frame case's own arrangement (see :scrollable's own +arrange:+) -
|
|
234
|
+
# children live in the embedded viewport frame (see #create_scrollable)
|
|
235
|
+
# - fill/expand by default so content stretches to the visible width
|
|
236
|
+
# instead of hugging its own natural size.
|
|
237
|
+
def arrange_scrollable_frame(node, children)
|
|
238
|
+
children.each { |child| @app.command(:pack, child.realized.arrange_path, fill: 'both', expand: true) }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Grids +target_path+ (the scrollable widget/canvas) against a
|
|
242
|
+
# scrollbar per requested axis, inside +path+ (their shared parent).
|
|
243
|
+
# The scrollbar auto-hides once its own -yscrollcommand/
|
|
244
|
+
# -xscrollcommand tells it the content fully fits (Tk hands that
|
|
245
|
+
# callback the visible fraction as +first+/+last+ - 0.0/1.0 means
|
|
246
|
+
# "all of it" - see #auto_hide_scrollbar) - real "overflow: auto",
|
|
247
|
+
# not a bar that's always there whether it's needed or not.
|
|
248
|
+
def wire_scrollbars(path, target_path, x:, y:)
|
|
249
|
+
vsb_path = "#{path}.vsb"
|
|
250
|
+
hsb_path = "#{path}.hsb"
|
|
251
|
+
|
|
252
|
+
if y
|
|
253
|
+
@app.command('ttk::scrollbar', vsb_path, orient: 'vertical', command: "#{target_path} yview")
|
|
254
|
+
@app.command(:grid, vsb_path, row: 0, column: 1, sticky: 'ns')
|
|
255
|
+
auto_hide_scrollbar(target_path, vsb_path, option: :yscrollcommand)
|
|
256
|
+
end
|
|
257
|
+
if x
|
|
258
|
+
@app.command('ttk::scrollbar', hsb_path, orient: 'horizontal', command: "#{target_path} xview")
|
|
259
|
+
@app.command(:grid, hsb_path, row: 1, column: 0, sticky: 'ew')
|
|
260
|
+
auto_hide_scrollbar(target_path, hsb_path, option: :xscrollcommand)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
@app.command(:grid, target_path, row: 0, column: 0, sticky: 'nsew')
|
|
264
|
+
@app.command(:grid, :columnconfigure, path, 0, weight: 1)
|
|
265
|
+
@app.command(:grid, :rowconfigure, path, 0, weight: 1)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Tk's own `grid remove` un-maps a widget but (unlike `grid forget`)
|
|
269
|
+
# remembers its grid options, so showing it again later is just a
|
|
270
|
+
# bare `grid <path>` - no need to re-derive/re-pass row:/column:/
|
|
271
|
+
# sticky: ourselves.
|
|
272
|
+
#
|
|
273
|
+
# Tk only re-invokes -yscrollcommand/-xscrollcommand when the
|
|
274
|
+
# reported fraction actually *changes* - an empty widget that gains
|
|
275
|
+
# a few rows can go straight from "0.0 1.0" (nothing to scroll) to
|
|
276
|
+
# "0.0 1.0" again (still nothing to scroll), with the callback never
|
|
277
|
+
# firing in between. That would leave the eagerly-gridded scrollbar
|
|
278
|
+
# stuck shown forever, so #after_idle also checks the real, current
|
|
279
|
+
# fraction directly (via a plain +yview+ query) once - after every
|
|
280
|
+
# widget this build creates has gone through its first geometry
|
|
281
|
+
# pass, same as the callback itself would eventually see.
|
|
282
|
+
def auto_hide_scrollbar(target_path, scrollbar_path, option:)
|
|
283
|
+
shown = true
|
|
284
|
+
apply = lambda do |first, last|
|
|
285
|
+
fits = first.to_f <= 0.0 && last.to_f >= 1.0
|
|
286
|
+
if fits && shown
|
|
287
|
+
@app.command(:grid, :remove, scrollbar_path)
|
|
288
|
+
shown = false
|
|
289
|
+
elsif !fits && !shown
|
|
290
|
+
@app.command(:grid, scrollbar_path)
|
|
291
|
+
shown = true
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
@app.command(target_path, :configure, option => proc { |first, last|
|
|
296
|
+
@app.command(scrollbar_path, :set, first, last)
|
|
297
|
+
apply.call(first, last)
|
|
298
|
+
})
|
|
299
|
+
@app.after_idle {
|
|
300
|
+
first, last = @app.command(target_path, :yview).split
|
|
301
|
+
apply.call(first, last)
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Tk's own Scrollbar class binds <MouseWheel> at this same ratio (see
|
|
306
|
+
# scrlbar.tcl) - matched here so wheeling directly over the frame
|
|
307
|
+
# case's content feels identical to wheeling over its scrollbar.
|
|
308
|
+
WHEEL_UNITS_PER_NOTCH = 40.0
|
|
309
|
+
|
|
310
|
+
# The frame case's scrollbar and -yscrollcommand/-xscrollcommand
|
|
311
|
+
# wiring (see #wire_scrollbars) only covers dragging the scrollbar
|
|
312
|
+
# itself - the canvas has no default wheel handling of its own (a
|
|
313
|
+
# bare canvas isn't a Scrollbar), and neither do any of the
|
|
314
|
+
# arbitrary widgets embedded in its viewport. A <MouseWheel>/
|
|
315
|
+
# <Button-4>/<Button-5> binding placed directly on the canvas alone
|
|
316
|
+
# wouldn't fire for those descendants either: Tk delivers pointer
|
|
317
|
+
# events to whichever widget is actually under the cursor, and a
|
|
318
|
+
# child widget layered inside the viewport intercepts them before
|
|
319
|
+
# the canvas ever sees them.
|
|
320
|
+
#
|
|
321
|
+
# The fix is the classic one - give the canvas, the viewport, and
|
|
322
|
+
# every widget already inside it (walked recursively, since new
|
|
323
|
+
# widgets can nest arbitrarily deep) a shared custom bindtag, and
|
|
324
|
+
# bind the wheel handler once on that tag instead of on any single
|
|
325
|
+
# widget. Every widget carrying the tag then responds identically,
|
|
326
|
+
# regardless of which one the pointer happens to be over - the same
|
|
327
|
+
# mechanism Tk's own class bindings (Button, Entry, ...) use, just
|
|
328
|
+
# scoped to this one scrollable region instead of a widget class.
|
|
329
|
+
#
|
|
330
|
+
# @note widgets added later via +Session#add+ don't pick up the tag
|
|
331
|
+
# automatically - wheel-scrolling a scrollable frame's dynamically
|
|
332
|
+
# added content isn't covered yet.
|
|
333
|
+
def wire_wheel_scroll(canvas_path, viewport_path, node, x:, y:)
|
|
334
|
+
return unless x || y
|
|
335
|
+
|
|
336
|
+
tag = "TeekScrollRegion#{canvas_path.tr('.', '_')}"
|
|
337
|
+
add_bindtag(canvas_path, tag)
|
|
338
|
+
add_bindtag(viewport_path, tag)
|
|
339
|
+
node.children.each { |child| child.each { |descendant| add_bindtag(descendant.realized.path, tag) if descendant.realized } }
|
|
340
|
+
|
|
341
|
+
if y
|
|
342
|
+
@app.bind(tag, '<MouseWheel>', :mouse_wheel) { |delta|
|
|
343
|
+
@app.command(canvas_path, :yview, :scroll, wheel_units(delta), :units)
|
|
344
|
+
}
|
|
345
|
+
@app.bind(tag, '<Button-4>') { @app.command(canvas_path, :yview, :scroll, -1, :units) }
|
|
346
|
+
@app.bind(tag, '<Button-5>') { @app.command(canvas_path, :yview, :scroll, 1, :units) }
|
|
347
|
+
end
|
|
348
|
+
if x
|
|
349
|
+
@app.bind(tag, '<Shift-MouseWheel>', :mouse_wheel) { |delta|
|
|
350
|
+
@app.command(canvas_path, :xview, :scroll, wheel_units(delta), :units)
|
|
351
|
+
}
|
|
352
|
+
@app.bind(tag, '<Shift-Button-4>') { @app.command(canvas_path, :xview, :scroll, -1, :units) }
|
|
353
|
+
@app.bind(tag, '<Shift-Button-5>') { @app.command(canvas_path, :xview, :scroll, 1, :units) }
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Tk 9's own `scroll number units` accepts (and documents rounding
|
|
358
|
+
# for) a fractional number - Tcl 8.6's stricter integer parsing
|
|
359
|
+
# rejects a value like +"3.0"+ outright ("expected integer but got
|
|
360
|
+
# ..."), raised deep inside the widget's own command implementation
|
|
361
|
+
# from within this very callback. Rounding to a real Ruby Integer
|
|
362
|
+
# here (away from zero, matching Tk 9's own documented rounding)
|
|
363
|
+
# keeps the string Tcl sees free of a decimal point on every
|
|
364
|
+
# version, rather than relying on 9.x's more lenient parsing.
|
|
365
|
+
def wheel_units(delta)
|
|
366
|
+
(delta.to_f / -WHEEL_UNITS_PER_NOTCH).round
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def add_bindtag(path, tag)
|
|
370
|
+
current = @app.split_list(@app.command(:bindtags, path))
|
|
371
|
+
@app.command(:bindtags, path, current + [tag])
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Builds one menu widget (a menu_bar, a nested cascade, or a
|
|
375
|
+
# standalone context_menu) plus every entry it holds, recursing into
|
|
376
|
+
# nested cascades depth-first so a cascade's own menu exists before
|
|
377
|
+
# the `add cascade` entry that references it is added to its parent.
|
|
378
|
+
# A menu_bar additionally attaches itself to parent_path's own -menu
|
|
379
|
+
# option once its whole subtree is built. Registered as :menu_bar/
|
|
380
|
+
# :context_menu's own `custom_create:` (see #create) - takes
|
|
381
|
+
# +parent_path+, not an already-allocated +path+, since it replaces
|
|
382
|
+
# #create's entire per-node handling for this node, allocation included.
|
|
383
|
+
def create_menu_tree(node, parent_path)
|
|
384
|
+
path = allocate_path(node, parent_path)
|
|
385
|
+
@app.menu(path)
|
|
386
|
+
node.realized = RealizedNode.new(app: @app, path: path)
|
|
387
|
+
|
|
388
|
+
node.children.each do |child|
|
|
389
|
+
case child.type
|
|
390
|
+
when :menu
|
|
391
|
+
create_menu_tree(child, path)
|
|
392
|
+
@app.command(path, :add, :cascade, **child.opts, menu: child.realized.path)
|
|
393
|
+
when :menu_item
|
|
394
|
+
@app.command(path, :add, :command, **menu_entry_opts(child))
|
|
395
|
+
when :menu_separator
|
|
396
|
+
@app.command(path, :add, :separator)
|
|
397
|
+
when :menu_checkbox
|
|
398
|
+
@app.command(path, :add, :checkbutton, **menu_entry_opts(child))
|
|
399
|
+
when :menu_radio
|
|
400
|
+
@app.command(path, :add, :radiobutton, **menu_entry_opts(child))
|
|
401
|
+
else
|
|
402
|
+
raise ArgumentError, "#{describe(child)} isn't valid inside a menu"
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
@app.command(parent_path, :configure, menu: path) if node.type == :menu_bar
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def menu_entry_opts(node)
|
|
410
|
+
bind = node.opts[:bind]
|
|
411
|
+
bind ? node.opts.except(:bind).merge(variable: bind.name) : node.opts
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def link(node)
|
|
415
|
+
# A #create custom_create? type owns its whole subtree (see
|
|
416
|
+
# #create) - no arrangement, events, close-handler, or child
|
|
417
|
+
# recursion to do here either.
|
|
418
|
+
return if WidgetTypes.for_type(node.type)&.custom_create?
|
|
419
|
+
|
|
420
|
+
arrange_children(node)
|
|
421
|
+
node.events.each { |binding| wire_event(node, binding) }
|
|
422
|
+
run_raw_op(node) if node.type == :raw_op
|
|
423
|
+
wire_close_handler(node) if node.opts[:on_close]
|
|
424
|
+
node.children.each { |child| link(child) unless child.lazy? }
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def run_raw_op(node)
|
|
428
|
+
node.opts[:block].call(@app)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def wire_close_handler(node)
|
|
432
|
+
@app.on_close(window: node.realized.path, &node.opts[:on_close])
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Whether a geometry manager should skip this child entirely: :root/
|
|
436
|
+
# :raw_op have no realized path at all (and aren't {WidgetType}s, so
|
|
437
|
+
# there's nothing to register this against); everything else reports
|
|
438
|
+
# it via its own {WidgetType#arranged?} - false for a type placed
|
|
439
|
+
# some other way entirely (a toplevel window, a tab/pane placed by
|
|
440
|
+
# its own container's `add`, a menu_bar/context_menu attached via
|
|
441
|
+
# -menu config).
|
|
442
|
+
def unarranged?(type)
|
|
443
|
+
return true if NON_WIDGET_TYPES.include?(type)
|
|
444
|
+
|
|
445
|
+
registered = WidgetTypes.for_type(type)
|
|
446
|
+
registered ? !registered.arranged? : false
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def arrange_children(node)
|
|
450
|
+
# Any not-yet-realized child has no +.realized+ to pack/place -
|
|
451
|
+
# excluded here regardless of type or WHY it isn't realized yet
|
|
452
|
+
# (a lazy: true node not explicitly realized, see {Node#lazy?};
|
|
453
|
+
# or a batch-mate #realize_subtree hasn't reached yet, when
|
|
454
|
+
# #add's own block declared more than one new sibling at once -
|
|
455
|
+
# every new child gets #create'd, and this same
|
|
456
|
+
# #arrange_children called again for the parent, in its own
|
|
457
|
+
# separate iteration of that same loop). Once a child IS
|
|
458
|
+
# realized, a later #arrange_children call on this same node
|
|
459
|
+
# (e.g. from a LATER iteration of that same loop, or a further
|
|
460
|
+
# sibling added afterward) picks it up normally - this only ever
|
|
461
|
+
# excludes a STILL-unrealized one.
|
|
462
|
+
candidates = node.children.reject { |child| !child.realized }
|
|
463
|
+
overlaid, rest = candidates.partition { |child| child.layout && child.layout[:overlay] }
|
|
464
|
+
arrangeable = rest.reject { |child| unarranged?(child.type) }
|
|
465
|
+
registered = WidgetTypes.for_type(node.type)
|
|
466
|
+
|
|
467
|
+
if registered&.arrange?
|
|
468
|
+
registered.arrange(self, node, arrangeable)
|
|
469
|
+
else
|
|
470
|
+
arrangeable.each { |child| @app.command(:pack, child.realized.arrange_path) }
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
overlaid.each { |child| place_overlay(node, child) }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# `place`s an overlay-tagged child (see {WidgetDSL#overlay}) at its
|
|
477
|
+
# anchor's -relx/-rely/-anchor, `-in` its parent - the canvas
|
|
478
|
+
# #arrange_children is currently walking, whatever container that
|
|
479
|
+
# turns out to be validated to (see {OverlayValidator}). Runs
|
|
480
|
+
# independently of whatever geometry manager arranges the rest of
|
|
481
|
+
# that parent's children, since `place` coexists with `pack`/`grid`
|
|
482
|
+
# on the same master as long as it targets a different slave.
|
|
483
|
+
def place_overlay(parent, child)
|
|
484
|
+
at = child.layout[:overlay][:at]
|
|
485
|
+
position = OverlayAnchors::POSITIONS.fetch(at)
|
|
486
|
+
@app.command(:place, child.realized.arrange_path, in: parent.realized.path, **position)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def arrange_flow(node, children, flow)
|
|
490
|
+
gap = node.opts.fetch(:gap, 0)
|
|
491
|
+
align = node.opts.fetch(:align, :start)
|
|
492
|
+
pad = node.opts.fetch(:pad, 0)
|
|
493
|
+
last_index = children.length - 1
|
|
494
|
+
|
|
495
|
+
children.each_with_index do |child, index|
|
|
496
|
+
opts = flow_pack_opts(
|
|
497
|
+
flow: flow, child: child, index: index, last_index: last_index,
|
|
498
|
+
gap: gap, align: align, pad: pad
|
|
499
|
+
)
|
|
500
|
+
@app.command(:pack, child.realized.arrange_path, **opts)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def arrange_grid(node, children)
|
|
505
|
+
gap = node.opts.fetch(:gap, 0)
|
|
506
|
+
|
|
507
|
+
children.each do |child|
|
|
508
|
+
cell = child.layout && child.layout[:cell]
|
|
509
|
+
unless cell
|
|
510
|
+
# {GridValidator.check_missing_cell} is the primary,
|
|
511
|
+
# pre-realize detection for this - this stays as a
|
|
512
|
+
# belt-and-suspenders backstop for the one path that skips
|
|
513
|
+
# validation entirely, {Session#add}'s incremental realize.
|
|
514
|
+
raise ArgumentError, "#{describe(child)} is a direct child of a grid but was never placed with " \
|
|
515
|
+
"g.cell(row:, col:) { ... }"
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
opts = { row: cell[:row], column: cell[:col], sticky: 'ew', padx: gap, pady: gap }
|
|
519
|
+
opts[:columnspan] = cell[:span] if cell[:span].to_i > 1
|
|
520
|
+
@app.command(:grid, child.realized.arrange_path, **opts)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
Array(node.opts[:stretch_columns]).each { |col| @app.command(:grid, :columnconfigure, node.realized.path, col, weight: 1) }
|
|
524
|
+
Array(node.opts[:stretch_rows]).each { |row| @app.command(:grid, :rowconfigure, node.realized.path, row, weight: 1) }
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def describe(node)
|
|
528
|
+
node.name ? "##{node.type}(:#{node.name})" : "an unnamed ##{node.type}"
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def flow_pack_opts(flow:, child:, index:, last_index:, gap:, align:, pad:)
|
|
532
|
+
opts = { side: flow[:side] }
|
|
533
|
+
opts[flow[:main_pad]] = [index.zero? ? pad : gap, index == last_index ? pad : 0]
|
|
534
|
+
opts[flow[:cross_pad]] = pad
|
|
535
|
+
|
|
536
|
+
grow = child.layout && child.layout[:grow]
|
|
537
|
+
stretch = align == :stretch
|
|
538
|
+
fills = [(flow[:main_fill] if grow), (flow[:cross_fill] if stretch)].compact
|
|
539
|
+
opts[:fill] = fills.length == 2 ? 'both' : fills.first unless fills.empty?
|
|
540
|
+
opts[:expand] = true if grow
|
|
541
|
+
unless stretch
|
|
542
|
+
opts[:anchor] = flow[:anchor].fetch(align) {
|
|
543
|
+
raise ArgumentError, "invalid align: #{align.inspect} (expected :start, :center, :end, or :stretch)"
|
|
544
|
+
}
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
opts
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def wire_event(node, binding)
|
|
551
|
+
target_node =
|
|
552
|
+
if binding.target
|
|
553
|
+
@document.find(binding.target, scope: node.scope) or
|
|
554
|
+
raise ArgumentError, "event target :#{binding.target} not found in the document"
|
|
555
|
+
else
|
|
556
|
+
node
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
@app.bind(target_node.realized.path, binding.event, *binding.subs) { |*args| binding.handler.call(*args) }
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def allocate_path(node, parent_path)
|
|
563
|
+
# node.key is unique for the whole Document, persisted at node
|
|
564
|
+
# creation (Document#create). Disambiguation against a same-
|
|
565
|
+
# parent repeat (a reusable component mounted more than once,
|
|
566
|
+
# including a lazily-{Handle#realize!}d one realized well after
|
|
567
|
+
# the initial pass) lives on {Document#claim_path_segment}, not
|
|
568
|
+
# here - a per-Realizer-instance counter would lose memory of
|
|
569
|
+
# earlier claims across separate realize_subtree calls (each
|
|
570
|
+
# gets its own Realizer instance, e.g. one per Session#add call
|
|
571
|
+
# or lazy screen realize).
|
|
572
|
+
segment = @document.claim_path_segment(parent_path, node.name ? node.name.to_s : node.key)
|
|
573
|
+
parent_path == '.' ? ".#{segment}" : "#{parent_path}.#{segment}"
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|