py2max 0.2.1__py3-none-any.whl
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.
- py2max/__init__.py +67 -0
- py2max/__main__.py +6 -0
- py2max/cli.py +1251 -0
- py2max/core/__init__.py +39 -0
- py2max/core/abstract.py +146 -0
- py2max/core/box.py +231 -0
- py2max/core/common.py +19 -0
- py2max/core/patcher.py +1658 -0
- py2max/core/patchline.py +68 -0
- py2max/exceptions.py +385 -0
- py2max/export/__init__.py +20 -0
- py2max/export/converters.py +345 -0
- py2max/export/svg.py +393 -0
- py2max/layout/__init__.py +26 -0
- py2max/layout/base.py +463 -0
- py2max/layout/flow.py +405 -0
- py2max/layout/grid.py +374 -0
- py2max/layout/matrix.py +628 -0
- py2max/log.py +338 -0
- py2max/maxref/__init__.py +78 -0
- py2max/maxref/category.py +163 -0
- py2max/maxref/db.py +1082 -0
- py2max/maxref/legacy.py +324 -0
- py2max/maxref/parser.py +703 -0
- py2max/py.typed +0 -0
- py2max/server/__init__.py +54 -0
- py2max/server/client.py +295 -0
- py2max/server/inline.py +312 -0
- py2max/server/repl.py +561 -0
- py2max/server/rpc.py +240 -0
- py2max/server/websocket.py +997 -0
- py2max/static/cola.min.js +4 -0
- py2max/static/d3.v7.min.js +2 -0
- py2max/static/dagre-bundle.js +328 -0
- py2max/static/elk.bundled.js +6663 -0
- py2max/static/index.html +168 -0
- py2max/static/interactive.html +589 -0
- py2max/static/interactive.js +2111 -0
- py2max/static/live-preview.js +324 -0
- py2max/static/svg.min.js +13 -0
- py2max/static/svg.min.js.map +1 -0
- py2max/transformers.py +168 -0
- py2max/utils.py +83 -0
- py2max-0.2.1.dist-info/METADATA +390 -0
- py2max-0.2.1.dist-info/RECORD +48 -0
- py2max-0.2.1.dist-info/WHEEL +4 -0
- py2max-0.2.1.dist-info/entry_points.txt +3 -0
- py2max-0.2.1.dist-info/licenses/LICENSE +19 -0
py2max/export/svg.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""SVG export functionality for py2max patches.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to export Max/MSP patch layouts to SVG format,
|
|
4
|
+
enabling offline visual validation without requiring Max.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Renders boxes with correct positioning and sizing
|
|
8
|
+
- Draws patchlines with connection points
|
|
9
|
+
- Supports different box types (comments, messages, objects)
|
|
10
|
+
- Generates clean, scalable SVG output
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from py2max import Patcher
|
|
14
|
+
>>> from py2max.svg import export_svg
|
|
15
|
+
>>> p = Patcher('my-patch.maxpat')
|
|
16
|
+
>>> osc = p.add_textbox('cycle~ 440')
|
|
17
|
+
>>> dac = p.add_textbox('ezdac~')
|
|
18
|
+
>>> p.add_line(osc, dac)
|
|
19
|
+
>>> export_svg(p, '/tmp/my-patch.svg')
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import html
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from ..core import Box, Patcher
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# SVG styling constants
|
|
33
|
+
BOX_FILL = "#f0f0f0"
|
|
34
|
+
BOX_STROKE = "#333333"
|
|
35
|
+
BOX_STROKE_WIDTH = 1
|
|
36
|
+
COMMENT_FILL = "#ffffd0"
|
|
37
|
+
MESSAGE_FILL = "#e0e0e0"
|
|
38
|
+
TEXT_COLOR = "#000000"
|
|
39
|
+
TEXT_FONT_FAMILY = "Monaco, Courier, monospace"
|
|
40
|
+
TEXT_FONT_SIZE = 12
|
|
41
|
+
LINE_COLOR = "#666666"
|
|
42
|
+
LINE_WIDTH = 2
|
|
43
|
+
INLET_COLOR = "#4080ff"
|
|
44
|
+
OUTLET_COLOR = "#ff8040"
|
|
45
|
+
PORT_RADIUS = 3
|
|
46
|
+
PADDING = 20
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _escape_text(text: str) -> str:
|
|
50
|
+
"""Escape text for SVG."""
|
|
51
|
+
return html.escape(str(text))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_box_fill(box: Box) -> str:
|
|
55
|
+
"""Determine fill color based on box type."""
|
|
56
|
+
maxclass = getattr(box, "maxclass", "newobj")
|
|
57
|
+
if maxclass == "comment":
|
|
58
|
+
return COMMENT_FILL
|
|
59
|
+
elif maxclass == "message":
|
|
60
|
+
return MESSAGE_FILL
|
|
61
|
+
return BOX_FILL
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_box_text(box: Box) -> str:
|
|
65
|
+
"""Extract display text from a box."""
|
|
66
|
+
text = getattr(box, "text", None)
|
|
67
|
+
if text:
|
|
68
|
+
return str(text)
|
|
69
|
+
|
|
70
|
+
maxclass = getattr(box, "maxclass", "newobj")
|
|
71
|
+
if maxclass == "comment":
|
|
72
|
+
return getattr(box, "text", "")
|
|
73
|
+
|
|
74
|
+
# For other object types, use maxclass name
|
|
75
|
+
return maxclass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _render_box(box, show_ports: bool = True) -> str:
|
|
79
|
+
"""Render a single box to SVG elements."""
|
|
80
|
+
rect = getattr(box, "patching_rect", None)
|
|
81
|
+
if not rect:
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
# Handle both Rect objects and list/tuple representations
|
|
85
|
+
if hasattr(rect, "x"):
|
|
86
|
+
x, y, w, h = rect.x, rect.y, rect.w, rect.h
|
|
87
|
+
elif isinstance(rect, (list, tuple)) and len(rect) >= 4:
|
|
88
|
+
x, y, w, h = rect[0], rect[1], rect[2], rect[3]
|
|
89
|
+
else:
|
|
90
|
+
return ""
|
|
91
|
+
fill = _get_box_fill(box)
|
|
92
|
+
text = _get_box_text(box)
|
|
93
|
+
|
|
94
|
+
svg_parts = []
|
|
95
|
+
|
|
96
|
+
# Draw box rectangle
|
|
97
|
+
svg_parts.append(
|
|
98
|
+
f'<rect x="{x}" y="{y}" width="{w}" height="{h}" '
|
|
99
|
+
f'fill="{fill}" stroke="{BOX_STROKE}" stroke-width="{BOX_STROKE_WIDTH}" '
|
|
100
|
+
f'rx="2" />'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Draw text (centered vertically, with padding)
|
|
104
|
+
text_x = x + 5
|
|
105
|
+
text_y = y + h / 2 + TEXT_FONT_SIZE / 3
|
|
106
|
+
escaped_text = _escape_text(text)
|
|
107
|
+
|
|
108
|
+
# Note: We don't truncate text to match Max's behavior where text can overflow
|
|
109
|
+
# the box visually. SVG will handle rendering even if text extends beyond box bounds.
|
|
110
|
+
|
|
111
|
+
svg_parts.append(
|
|
112
|
+
f'<text x="{text_x}" y="{text_y}" '
|
|
113
|
+
f'font-family="{TEXT_FONT_FAMILY}" font-size="{TEXT_FONT_SIZE}" '
|
|
114
|
+
f'fill="{TEXT_COLOR}">{escaped_text}</text>'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Draw inlet/outlet ports if enabled
|
|
118
|
+
if show_ports:
|
|
119
|
+
# Try to get inlet/outlet count via methods (for maxref-enabled boxes)
|
|
120
|
+
inlet_count = 0
|
|
121
|
+
outlet_count = 0
|
|
122
|
+
|
|
123
|
+
if hasattr(box, "get_inlet_count"):
|
|
124
|
+
try:
|
|
125
|
+
inlet_count = box.get_inlet_count() or 0
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
if hasattr(box, "get_outlet_count"):
|
|
130
|
+
try:
|
|
131
|
+
outlet_count = box.get_outlet_count() or 0
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# Fallback to private attributes if methods don't exist
|
|
136
|
+
if inlet_count == 0:
|
|
137
|
+
inlet_count = getattr(box, "_inlet_count", 0) or 0
|
|
138
|
+
if outlet_count == 0:
|
|
139
|
+
outlet_count = getattr(box, "_outlet_count", 0) or 0
|
|
140
|
+
|
|
141
|
+
# Draw inlets at top
|
|
142
|
+
if inlet_count > 0:
|
|
143
|
+
inlet_spacing = w / (inlet_count + 1)
|
|
144
|
+
for i in range(inlet_count):
|
|
145
|
+
inlet_x = x + inlet_spacing * (i + 1)
|
|
146
|
+
inlet_y = y
|
|
147
|
+
svg_parts.append(
|
|
148
|
+
f'<circle cx="{inlet_x}" cy="{inlet_y}" r="{PORT_RADIUS}" '
|
|
149
|
+
f'fill="{INLET_COLOR}" stroke="{BOX_STROKE}" stroke-width="0.5" />'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Draw outlets at bottom
|
|
153
|
+
if outlet_count > 0:
|
|
154
|
+
outlet_spacing = w / (outlet_count + 1)
|
|
155
|
+
for i in range(outlet_count):
|
|
156
|
+
outlet_x = x + outlet_spacing * (i + 1)
|
|
157
|
+
outlet_y = y + h
|
|
158
|
+
svg_parts.append(
|
|
159
|
+
f'<circle cx="{outlet_x}" cy="{outlet_y}" r="{PORT_RADIUS}" '
|
|
160
|
+
f'fill="{OUTLET_COLOR}" stroke="{BOX_STROKE}" stroke-width="0.5" />'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return "\n".join(svg_parts)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _get_port_position(box, port_index: int, is_outlet: bool) -> tuple[float, float]:
|
|
167
|
+
"""Calculate the x,y position of an inlet or outlet port."""
|
|
168
|
+
rect = getattr(box, "patching_rect", None)
|
|
169
|
+
if not rect:
|
|
170
|
+
return (0, 0)
|
|
171
|
+
|
|
172
|
+
# Handle both Rect objects and list/tuple representations
|
|
173
|
+
if hasattr(rect, "x"):
|
|
174
|
+
x, y, w, h = rect.x, rect.y, rect.w, rect.h
|
|
175
|
+
elif isinstance(rect, (list, tuple)) and len(rect) >= 4:
|
|
176
|
+
x, y, w, h = rect[0], rect[1], rect[2], rect[3]
|
|
177
|
+
else:
|
|
178
|
+
return (0, 0)
|
|
179
|
+
|
|
180
|
+
# Try to get port count via methods first
|
|
181
|
+
if is_outlet:
|
|
182
|
+
count = 1
|
|
183
|
+
if hasattr(box, "get_outlet_count"):
|
|
184
|
+
try:
|
|
185
|
+
count = box.get_outlet_count() or 1
|
|
186
|
+
except Exception:
|
|
187
|
+
count = getattr(box, "_outlet_count", 1) or 1
|
|
188
|
+
else:
|
|
189
|
+
count = getattr(box, "_outlet_count", 1) or 1
|
|
190
|
+
|
|
191
|
+
spacing = w / (count + 1)
|
|
192
|
+
port_x = x + spacing * (port_index + 1)
|
|
193
|
+
port_y = y + h
|
|
194
|
+
else:
|
|
195
|
+
count = 1
|
|
196
|
+
if hasattr(box, "get_inlet_count"):
|
|
197
|
+
try:
|
|
198
|
+
count = box.get_inlet_count() or 1
|
|
199
|
+
except Exception:
|
|
200
|
+
count = getattr(box, "_inlet_count", 1) or 1
|
|
201
|
+
else:
|
|
202
|
+
count = getattr(box, "_inlet_count", 1) or 1
|
|
203
|
+
|
|
204
|
+
spacing = w / (count + 1)
|
|
205
|
+
port_x = x + spacing * (port_index + 1)
|
|
206
|
+
port_y = y
|
|
207
|
+
|
|
208
|
+
return (port_x, port_y)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _render_patchline(line, patcher: Patcher) -> str:
|
|
212
|
+
"""Render a patchline connection to SVG."""
|
|
213
|
+
# Get source and destination boxes
|
|
214
|
+
src_id = getattr(line, "src", None)
|
|
215
|
+
dst_id = getattr(line, "dst", None)
|
|
216
|
+
|
|
217
|
+
if not src_id or not dst_id:
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
src_box = patcher._objects.get(src_id)
|
|
221
|
+
dst_box = patcher._objects.get(dst_id)
|
|
222
|
+
|
|
223
|
+
if not src_box or not dst_box:
|
|
224
|
+
return ""
|
|
225
|
+
|
|
226
|
+
# Get port indices
|
|
227
|
+
source = getattr(line, "source", [src_id, 0])
|
|
228
|
+
destination = getattr(line, "destination", [dst_id, 0])
|
|
229
|
+
|
|
230
|
+
src_port = int(source[1]) if len(source) > 1 else 0
|
|
231
|
+
dst_port = int(destination[1]) if len(destination) > 1 else 0
|
|
232
|
+
|
|
233
|
+
# Calculate connection points
|
|
234
|
+
x1, y1 = _get_port_position(src_box, src_port, is_outlet=True)
|
|
235
|
+
x2, y2 = _get_port_position(dst_box, dst_port, is_outlet=False)
|
|
236
|
+
|
|
237
|
+
# Draw line
|
|
238
|
+
return (
|
|
239
|
+
f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
|
|
240
|
+
f'stroke="{LINE_COLOR}" stroke-width="{LINE_WIDTH}" '
|
|
241
|
+
f'stroke-linecap="round" />'
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _calculate_viewbox(patcher: Patcher) -> tuple[float, float, float, float]:
|
|
246
|
+
"""Calculate SVG viewBox to fit all objects with padding."""
|
|
247
|
+
boxes = patcher._boxes
|
|
248
|
+
|
|
249
|
+
if not boxes:
|
|
250
|
+
return (0, 0, 800, 600)
|
|
251
|
+
|
|
252
|
+
min_x = float("inf")
|
|
253
|
+
min_y = float("inf")
|
|
254
|
+
max_x = float("-inf")
|
|
255
|
+
max_y = float("-inf")
|
|
256
|
+
|
|
257
|
+
for box in boxes:
|
|
258
|
+
rect = getattr(box, "patching_rect", None)
|
|
259
|
+
if rect:
|
|
260
|
+
# Handle both Rect objects and list/tuple representations
|
|
261
|
+
if hasattr(rect, "x"):
|
|
262
|
+
x, y, w, h = rect.x, rect.y, rect.w, rect.h
|
|
263
|
+
elif isinstance(rect, (list, tuple)) and len(rect) >= 4:
|
|
264
|
+
x, y, w, h = rect[0], rect[1], rect[2], rect[3]
|
|
265
|
+
else:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
min_x = min(min_x, x)
|
|
269
|
+
min_y = min(min_y, y)
|
|
270
|
+
max_x = max(max_x, x + w)
|
|
271
|
+
max_y = max(max_y, y + h)
|
|
272
|
+
|
|
273
|
+
# Add padding
|
|
274
|
+
min_x -= PADDING
|
|
275
|
+
min_y -= PADDING
|
|
276
|
+
max_x += PADDING
|
|
277
|
+
max_y += PADDING
|
|
278
|
+
|
|
279
|
+
width = max_x - min_x
|
|
280
|
+
height = max_y - min_y
|
|
281
|
+
|
|
282
|
+
return (min_x, min_y, width, height)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def export_svg(
|
|
286
|
+
patcher: Patcher,
|
|
287
|
+
output_path: Union[str, Path],
|
|
288
|
+
show_ports: bool = True,
|
|
289
|
+
title: Optional[str] = None,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Export a patcher to SVG format.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
patcher: The Patcher object to export.
|
|
295
|
+
output_path: Output file path for the SVG.
|
|
296
|
+
show_ports: Whether to show inlet/outlet ports on boxes.
|
|
297
|
+
title: Optional title to display at top of SVG.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> p = Patcher('test.maxpat')
|
|
301
|
+
>>> osc = p.add_textbox('cycle~ 440')
|
|
302
|
+
>>> dac = p.add_textbox('ezdac~')
|
|
303
|
+
>>> p.add_line(osc, dac)
|
|
304
|
+
>>> export_svg(p, '/tmp/test.svg')
|
|
305
|
+
"""
|
|
306
|
+
output_path = Path(output_path)
|
|
307
|
+
|
|
308
|
+
# Calculate viewBox
|
|
309
|
+
vx, vy, vw, vh = _calculate_viewbox(patcher)
|
|
310
|
+
|
|
311
|
+
# Start SVG
|
|
312
|
+
svg_parts = [
|
|
313
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
314
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
|
315
|
+
f'viewBox="{vx} {vy} {vw} {vh}" '
|
|
316
|
+
f'width="{vw}" height="{vh}">',
|
|
317
|
+
"",
|
|
318
|
+
"<defs>",
|
|
319
|
+
" <style>",
|
|
320
|
+
" text { user-select: none; }",
|
|
321
|
+
" </style>",
|
|
322
|
+
"</defs>",
|
|
323
|
+
"",
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
# Add title if provided
|
|
327
|
+
if title:
|
|
328
|
+
title_y = vy + 20
|
|
329
|
+
svg_parts.extend(
|
|
330
|
+
[
|
|
331
|
+
f'<text x="{vx + vw / 2}" y="{title_y}" '
|
|
332
|
+
f'font-family="{TEXT_FONT_FAMILY}" font-size="16" font-weight="bold" '
|
|
333
|
+
f'fill="{TEXT_COLOR}" text-anchor="middle">{_escape_text(title)}</text>',
|
|
334
|
+
"",
|
|
335
|
+
]
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Render patchlines first (so they appear behind boxes)
|
|
339
|
+
svg_parts.append("<!-- Patchlines -->")
|
|
340
|
+
for line in patcher._lines:
|
|
341
|
+
line_svg = _render_patchline(line, patcher)
|
|
342
|
+
if line_svg:
|
|
343
|
+
svg_parts.append(line_svg)
|
|
344
|
+
svg_parts.append("")
|
|
345
|
+
|
|
346
|
+
# Render boxes
|
|
347
|
+
svg_parts.append("<!-- Boxes -->")
|
|
348
|
+
for box in patcher._boxes:
|
|
349
|
+
box_svg = _render_box(box, show_ports=show_ports)
|
|
350
|
+
if box_svg:
|
|
351
|
+
svg_parts.append(box_svg)
|
|
352
|
+
svg_parts.append("")
|
|
353
|
+
|
|
354
|
+
# Close SVG
|
|
355
|
+
svg_parts.append("</svg>")
|
|
356
|
+
|
|
357
|
+
# Write to file
|
|
358
|
+
svg_content = "\n".join(svg_parts)
|
|
359
|
+
output_path.write_text(svg_content, encoding="utf-8")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def export_svg_string(
|
|
363
|
+
patcher: Patcher,
|
|
364
|
+
show_ports: bool = True,
|
|
365
|
+
title: Optional[str] = None,
|
|
366
|
+
) -> str:
|
|
367
|
+
"""Export a patcher to SVG format as a string.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
patcher: The Patcher object to export.
|
|
371
|
+
show_ports: Whether to show inlet/outlet ports on boxes.
|
|
372
|
+
title: Optional title to display at top of SVG.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
SVG content as a string.
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
>>> p = Patcher('test.maxpat')
|
|
379
|
+
>>> svg_str = export_svg_string(p)
|
|
380
|
+
"""
|
|
381
|
+
import tempfile
|
|
382
|
+
|
|
383
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".svg", delete=False) as f:
|
|
384
|
+
temp_path = Path(f.name)
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
export_svg(patcher, temp_path, show_ports=show_ports, title=title)
|
|
388
|
+
return temp_path.read_text(encoding="utf-8")
|
|
389
|
+
finally:
|
|
390
|
+
temp_path.unlink()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
__all__ = ["export_svg", "export_svg_string"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Layout management for py2max patches.
|
|
2
|
+
|
|
3
|
+
This subpackage provides various layout managers for automatic positioning of
|
|
4
|
+
Max objects in patches:
|
|
5
|
+
|
|
6
|
+
- LayoutManager: Basic horizontal layout (deprecated)
|
|
7
|
+
- GridLayoutManager: Grid-based layout with clustering
|
|
8
|
+
- FlowLayoutManager: Signal flow-based hierarchical layout
|
|
9
|
+
- MatrixLayoutManager: Matrix/columnar layout for signal chains
|
|
10
|
+
- HorizontalLayoutManager: Legacy alias for GridLayoutManager (horizontal)
|
|
11
|
+
- VerticalLayoutManager: Legacy alias for GridLayoutManager (vertical)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .base import LayoutManager
|
|
15
|
+
from .flow import FlowLayoutManager
|
|
16
|
+
from .grid import GridLayoutManager, HorizontalLayoutManager, VerticalLayoutManager
|
|
17
|
+
from .matrix import MatrixLayoutManager
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"LayoutManager",
|
|
21
|
+
"GridLayoutManager",
|
|
22
|
+
"HorizontalLayoutManager",
|
|
23
|
+
"VerticalLayoutManager",
|
|
24
|
+
"FlowLayoutManager",
|
|
25
|
+
"MatrixLayoutManager",
|
|
26
|
+
]
|