pict-section-flow 0.0.17 → 0.0.19

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.
Files changed (90) hide show
  1. package/README.md +18 -18
  2. package/docs/Architecture.md +1 -1
  3. package/docs/Data_Model.md +2 -2
  4. package/docs/Getting_Started.md +5 -5
  5. package/docs/Implementation_Reference.md +6 -6
  6. package/docs/Layout_Persistence.md +3 -3
  7. package/docs/README.md +12 -12
  8. package/docs/Theme_Integration.md +150 -0
  9. package/docs/_cover.md +1 -1
  10. package/docs/_sidebar.md +7 -6
  11. package/docs/_version.json +7 -0
  12. package/docs/api/PictFlowCard.md +6 -6
  13. package/docs/api/PictFlowCardPropertiesPanel.md +2 -2
  14. package/docs/api/addConnection.md +4 -4
  15. package/docs/api/addNode.md +6 -6
  16. package/docs/api/autoLayout.md +2 -2
  17. package/docs/api/getFlowData.md +5 -5
  18. package/docs/api/marshalToView.md +3 -3
  19. package/docs/api/openPanel.md +2 -2
  20. package/docs/api/registerHandler.md +3 -3
  21. package/docs/api/registerNodeType.md +3 -3
  22. package/docs/api/removeConnection.md +5 -5
  23. package/docs/api/removeNode.md +6 -6
  24. package/docs/api/saveLayout.md +2 -2
  25. package/docs/api/screenToSVGCoords.md +2 -2
  26. package/docs/api/selectNode.md +3 -3
  27. package/docs/api/setTheme.md +2 -2
  28. package/docs/api/setZoom.md +3 -3
  29. package/docs/api/toggleFullscreen.md +2 -2
  30. package/docs/card-help/EACH.md +3 -3
  31. package/docs/card-help/FREAD.md +5 -5
  32. package/docs/card-help/FWRITE.md +5 -5
  33. package/docs/card-help/GET.md +2 -2
  34. package/docs/card-help/ITE.md +3 -3
  35. package/docs/card-help/LOG.md +4 -4
  36. package/docs/card-help/NOTE.md +1 -1
  37. package/docs/card-help/PREV.md +2 -2
  38. package/docs/card-help/SET.md +5 -5
  39. package/docs/card-help/SPKL.md +2 -2
  40. package/docs/card-help/STAT.md +3 -3
  41. package/docs/card-help/SW.md +4 -4
  42. package/docs/css/docuserve.css +277 -23
  43. package/docs/index.html +2 -2
  44. package/docs/retold-catalog.json +1 -1
  45. package/docs/retold-keyword-index.json +1 -1
  46. package/example_applications/simple_cards/css/flowexample.css +2 -2
  47. package/example_applications/simple_cards/source/card-help-content.js +12 -12
  48. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +1 -1
  49. package/example_applications/simple_cards/source/sample-flows.js +410 -0
  50. package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +5 -5
  51. package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +5 -5
  52. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +4 -4
  53. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +141 -8
  54. package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +2 -2
  55. package/package.json +8 -7
  56. package/source/Pict-Section-Flow.js +26 -0
  57. package/source/providers/PictProvider-Flow-CSS.js +435 -61
  58. package/source/providers/PictProvider-Flow-ConnectorShapes.js +9 -5
  59. package/source/providers/PictProvider-Flow-NodeTypes.js +10 -0
  60. package/source/providers/PictProvider-Flow-PanelChrome.js +7 -17
  61. package/source/providers/PictProvider-Flow-Theme.js +7 -7
  62. package/source/providers/edges/Edge-Bezier.js +41 -0
  63. package/source/providers/edges/Edge-Orthogonal.js +37 -0
  64. package/source/providers/edges/Edge-OrthogonalSnap.js +72 -0
  65. package/source/providers/edges/Edge-Perimeter-Linear.js +31 -0
  66. package/source/providers/edges/Edge-Perimeter-Orthogonal.js +39 -0
  67. package/source/providers/edges/Edge-Perimeter.js +48 -0
  68. package/source/providers/edges/Edge-PerimeterMath.js +92 -0
  69. package/source/providers/edges/Edge-Straight.js +24 -0
  70. package/source/providers/layouts/Layout-Circular.js +203 -0
  71. package/source/providers/layouts/Layout-Coerce.js +40 -0
  72. package/source/providers/layouts/Layout-Columnar.js +134 -0
  73. package/source/providers/layouts/Layout-Custom.js +27 -0
  74. package/source/providers/layouts/Layout-ForcedFromCenter.js +256 -0
  75. package/source/providers/layouts/Layout-Grid.js +134 -0
  76. package/source/providers/layouts/Layout-Layered.js +209 -0
  77. package/source/providers/layouts/Layout-Tabular.js +94 -0
  78. package/source/services/PictService-Flow-ConnectionRenderer.js +532 -28
  79. package/source/services/PictService-Flow-DataManager.js +12 -1
  80. package/source/services/PictService-Flow-InteractionManager.js +39 -42
  81. package/source/services/PictService-Flow-Layout.js +305 -121
  82. package/source/services/PictService-Flow-PortRenderer.js +108 -26
  83. package/source/services/PictService-Flow-RenderManager.js +41 -11
  84. package/source/views/PictView-Flow-FloatingToolbar.js +69 -61
  85. package/source/views/PictView-Flow-Node.js +47 -6
  86. package/source/views/PictView-Flow-PropertiesPanel.js +46 -53
  87. package/source/views/PictView-Flow-Toolbar.js +1065 -485
  88. package/source/views/PictView-Flow.js +455 -7
  89. package/test/Layout_tests.js +1400 -0
  90. package/test/PortRenderer_tests.js +11 -2
@@ -21,67 +21,152 @@ const _DefaultConfiguration =
21
21
  [
22
22
  {
23
23
  Hash: 'Flow-Toolbar-Template',
24
+ // Inline onclick handlers route to the toolbar via the FlowView
25
+ // (the flow toolbar is reachable as ._ToolbarView from the
26
+ // registered FlowView). Each button is responsible for its own
27
+ // action — no event delegation.
24
28
  Template: /*html*/`
25
29
  <div class="pict-flow-toolbar" id="Flow-Toolbar-Bar-{~D:Record.FlowViewIdentifier~}">
26
30
  <div class="pict-flow-toolbar-group">
27
- <button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node">
31
+ <button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node"
32
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('add-node')">
28
33
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-plus-{~D:Record.FlowViewIdentifier~}"></span>
29
34
  <span class="pict-flow-toolbar-btn-text">Node</span>
30
35
  </button>
31
- <button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette">
36
+ <button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette"
37
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('cards-popup')">
32
38
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-cards-{~D:Record.FlowViewIdentifier~}"></span>
33
39
  <span class="pict-flow-toolbar-btn-text">Cards</span>
34
40
  <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-CardsChevron-{~D:Record.FlowViewIdentifier~}"></span>
35
41
  </button>
36
- <button class="pict-flow-toolbar-btn" data-flow-action="delete-selected" title="Delete Node">
42
+ <button class="pict-flow-toolbar-btn" data-flow-action="delete-selected" title="Delete Node"
43
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('delete-selected')">
37
44
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-trash-{~D:Record.FlowViewIdentifier~}"></span>
38
45
  </button>
39
46
  </div>
40
47
  <div class="pict-flow-toolbar-group">
41
- <button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage Layouts">
48
+ <button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage saved layouts"
49
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('layout-popup')">
42
50
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-layout-{~D:Record.FlowViewIdentifier~}"></span>
43
- <span class="pict-flow-toolbar-btn-text">Layout</span>
51
+ <span class="pict-flow-toolbar-btn-text">Layouts</span>
44
52
  <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-LayoutChevron-{~D:Record.FlowViewIdentifier~}"></span>
45
53
  </button>
46
- <button class="pict-flow-toolbar-btn" data-flow-action="auto-layout" title="Auto Layout">
47
- <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-auto-layout-{~D:Record.FlowViewIdentifier~}"></span>
48
- <span class="pict-flow-toolbar-btn-text">Auto Layout</span>
49
- </button>
54
+ <div class="pict-flow-toolbar-btn-split" id="Flow-Toolbar-Auto-{~D:Record.FlowViewIdentifier~}">
55
+ <button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-main" data-flow-action="apply-current-layout" title="Apply current layout algorithm"
56
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('apply-current-layout')">
57
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-auto-{~D:Record.FlowViewIdentifier~}"></span>
58
+ <span class="pict-flow-toolbar-btn-text">Auto</span>
59
+ </button>
60
+ <button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-chevron" data-flow-action="layout-algorithm-popup" title="Choose layout algorithm"
61
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('layout-algorithm-popup')">
62
+ <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-AutoChevron-{~D:Record.FlowViewIdentifier~}"></span>
63
+ </button>
64
+ </div>
50
65
  </div>
51
66
  <div class="pict-flow-toolbar-group">
52
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In">
67
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In"
68
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-in')">
53
69
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-in-{~D:Record.FlowViewIdentifier~}"></span>
54
70
  </button>
55
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out">
71
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out"
72
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-out')">
56
73
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-out-{~D:Record.FlowViewIdentifier~}"></span>
57
74
  </button>
58
- <button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View">
75
+ <button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View"
76
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-fit')">
59
77
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-fit-{~D:Record.FlowViewIdentifier~}"></span>
60
78
  </button>
61
79
  </div>
62
80
  <div class="pict-flow-toolbar-group pict-flow-toolbar-right">
63
- <button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings">
81
+ <button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings"
82
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('settings-popup')">
64
83
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-settings-{~D:Record.FlowViewIdentifier~}"></span>
65
84
  </button>
66
- <button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen">
85
+ <button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen"
86
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('fullscreen')">
67
87
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Fullscreen-Icon-{~D:Record.FlowViewIdentifier~}"></span>
68
88
  </button>
69
- <button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float">
89
+ <button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float"
90
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('toggle-floating')">
70
91
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-grip-{~D:Record.FlowViewIdentifier~}"></span>
71
92
  </button>
72
- <button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar">
93
+ <button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar"
94
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('collapse-toolbar')">
73
95
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-collapse-{~D:Record.FlowViewIdentifier~}"></span>
74
96
  </button>
75
97
  </div>
76
98
  </div>
77
99
  <div class="pict-flow-toolbar-collapsed" id="Flow-Toolbar-Collapsed-{~D:Record.FlowViewIdentifier~}">
78
- <button class="pict-flow-toolbar-expand-btn" data-flow-action="expand-toolbar" title="Expand Toolbar" id="Flow-Toolbar-ExpandBtn-{~D:Record.FlowViewIdentifier~}">
100
+ <button class="pict-flow-toolbar-expand-btn" data-flow-action="expand-toolbar" title="Expand Toolbar" id="Flow-Toolbar-ExpandBtn-{~D:Record.FlowViewIdentifier~}"
101
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('expand-toolbar')">
79
102
  <span id="Flow-Toolbar-Icon-expand-{~D:Record.FlowViewIdentifier~}"></span>
80
103
  </button>
81
104
  </div>
82
105
  <div class="pict-flow-toolbar-popup-anchor" id="Flow-Toolbar-PopupAnchor-{~D:Record.FlowViewIdentifier~}">
83
106
  </div>
84
107
  `
108
+ },
109
+ {
110
+ Hash: 'Flow-AddNode-List',
111
+ // Iteration source is `Record.Rows` — the outer call sets the
112
+ // FlowViewIdentifier on each row so inline handlers resolve their
113
+ // owning view independently. Nested {~D:~} inside {~TS:~}
114
+ // addresses isn't supported by the template engine, so we keep
115
+ // addresses static.
116
+ Template: '{~TS:Flow-AddNode-Row:Record.Rows~}'
117
+ },
118
+ {
119
+ Hash: 'Flow-AddNode-Row',
120
+ Template: '<div class="pict-flow-popup-list-item" data-node-type="{~D:Record.NodeType~}"'
121
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addNodeFromPopup(this.getAttribute(\'data-node-type\'))">'
122
+ + '<span class="pict-flow-popup-list-item-icon">{~D:Record.IconHTML~}</span>'
123
+ + '<span class="pict-flow-popup-list-item-label">{~D:Record.Label~}</span>'
124
+ + '{~NE:Record.Code^<span class="pict-flow-popup-list-item-code">{~D:Record.Code~}</span>~}'
125
+ + '</div>'
126
+ },
127
+ {
128
+ Hash: 'Flow-Cards-List',
129
+ Template: '{~TS:Flow-Cards-Category:Record.Categories~}'
130
+ },
131
+ {
132
+ Hash: 'Flow-Cards-Category',
133
+ Template: '<div class="pict-flow-palette-category" style="padding:0.35em 0.5em;">'
134
+ + '<div class="pict-flow-palette-category-label">{~D:Record.Name~}</div>'
135
+ + '<div class="pict-flow-palette-cards">{~TS:Flow-Cards-Card:Record.Cards~}</div>'
136
+ + '</div>'
137
+ },
138
+ {
139
+ Hash: 'Flow-Cards-Card',
140
+ Template: '<div class="pict-flow-palette-card{~D:Record.DisabledClass~}" data-card-type="{~D:Record.CardType~}" title="{~D:Record.Tooltip~}"'
141
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addCardFromPopup(this.getAttribute(\'data-card-type\'))">'
142
+ + '{~NE:Record.IconHTML^<span class="pict-flow-palette-card-icon">{~D:Record.IconHTML~}</span>~}'
143
+ + '{~NE:Record.IconEmoji^<span class="pict-flow-palette-card-icon">{~D:Record.IconEmoji~}</span>~}'
144
+ + '{~NE:Record.SwatchColor^<span class="pict-flow-palette-card-swatch" style="background-color: {~D:Record.SwatchColor~};"></span>~}'
145
+ + '<span class="pict-flow-palette-card-title">{~D:Record.Label~}</span>'
146
+ + '{~NE:Record.Code^<span class="pict-flow-palette-card-code">{~D:Record.Code~}</span>~}'
147
+ + '</div>'
148
+ },
149
+ {
150
+ Hash: 'Flow-Layout-List',
151
+ Template: '{~TS:Flow-Layout-Row:Record.Rows~}'
152
+ },
153
+ {
154
+ Hash: 'Flow-Layout-Row',
155
+ Template: '<div class="pict-flow-popup-layout-row" data-layout-hash="{~D:Record.LayoutHash~}"'
156
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._restoreLayoutFromPopup(\'{~D:Record.LayoutHash~}\', event)">'
157
+ + '<span class="pict-flow-popup-layout-name">{~D:Record.Name~}</span>'
158
+ + '<button class="pict-flow-popup-layout-delete" title="Delete layout"'
159
+ + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._deleteLayoutFromPopup(\'{~D:Record.LayoutHash~}\', event)">'
160
+ + '{~D:Record.DeleteIconHTML~}</button>'
161
+ + '</div>'
162
+ },
163
+ {
164
+ Hash: 'Flow-Layout-OptionList',
165
+ Template: '{~TS:Flow-Layout-Option:Record.Options~}'
166
+ },
167
+ {
168
+ Hash: 'Flow-Layout-Option',
169
+ Template: '<option value="{~D:Record.Value~}"{~D:Record.SelectedAttr~}>{~D:Record.Label~}</option>'
85
170
  }
86
171
  ],
87
172
 
@@ -113,6 +198,16 @@ class PictViewFlowToolbar extends libPictView
113
198
  this._FloatingPosition = { X: 80, Y: 80 };
114
199
  this._DocumentClickHandler = null;
115
200
  this._FloatingToolbarView = null;
201
+
202
+ // Layout-algorithm parameter form (pict-section-form metacontroller).
203
+ // Lazily created on first popup open; null until then. Falls back to
204
+ // hand-rolled inputs when PictFormMetacontroller is not registered.
205
+ this._LayoutFormMetacontroller = null;
206
+ this._LayoutFormHostID = null;
207
+ // Whether the parameter form is expanded inside the popup. Persists
208
+ // across popup opens so a user who collapsed it doesn't have to
209
+ // re-collapse on every reopen. Defaults to expanded.
210
+ this._LayoutFormExpanded = true;
116
211
  }
117
212
 
118
213
  render(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress)
@@ -126,33 +221,8 @@ class PictViewFlowToolbar extends libPictView
126
221
  {
127
222
  let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
128
223
 
129
- // Bind toolbar button events via event delegation
130
- let tmpToolbarBar = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Bar-${tmpFlowViewIdentifier}`);
131
- if (tmpToolbarBar.length > 0)
132
- {
133
- tmpToolbarBar[0].addEventListener('click', (pEvent) =>
134
- {
135
- let tmpTarget = pEvent.target;
136
- if (!tmpTarget) return;
137
-
138
- // Walk up to find the button with the action
139
- let tmpButton = tmpTarget.closest('[data-flow-action]');
140
- if (!tmpButton) return;
141
-
142
- let tmpAction = tmpButton.getAttribute('data-flow-action');
143
- this._handleToolbarAction(tmpAction);
144
- });
145
- }
146
-
147
- // Bind expand button click (it's outside the main toolbar bar)
148
- let tmpExpandBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-ExpandBtn-${tmpFlowViewIdentifier}`);
149
- if (tmpExpandBtn.length > 0)
150
- {
151
- tmpExpandBtn[0].addEventListener('click', () =>
152
- {
153
- this._setToolbarMode('docked');
154
- });
155
- }
224
+ // Click handlers live on each button as inline `onclick` attributes
225
+ // in Flow-Toolbar-Template — they call _handleToolbarAction directly.
156
226
 
157
227
  // Populate SVG icons for toolbar buttons
158
228
  this._populateToolbarIcons();
@@ -174,6 +244,14 @@ class PictViewFlowToolbar extends libPictView
174
244
  tmpCardsBtn[0].remove();
175
245
  }
176
246
  }
247
+ if (this.options.EnableLayoutMenu === false)
248
+ {
249
+ let tmpAutoBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Auto-${tmpFlowViewIdentifier}`);
250
+ if (tmpAutoBtn.length > 0)
251
+ {
252
+ tmpAutoBtn[0].remove();
253
+ }
254
+ }
177
255
 
178
256
  return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
179
257
  }
@@ -198,7 +276,7 @@ class PictViewFlowToolbar extends libPictView
198
276
  'zoom-in': 'zoom-in',
199
277
  'zoom-out': 'zoom-out',
200
278
  'zoom-fit': 'zoom-fit',
201
- 'auto-layout': 'auto-layout',
279
+ 'auto': 'auto-layout',
202
280
  'cards': 'cards',
203
281
  'layout': 'layout',
204
282
  'settings': 'settings',
@@ -237,6 +315,12 @@ class PictViewFlowToolbar extends libPictView
237
315
  {
238
316
  tmpLayoutChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
239
317
  }
318
+
319
+ let tmpAutoChevron = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-AutoChevron-${tmpFlowViewIdentifier}`);
320
+ if (tmpAutoChevron.length > 0)
321
+ {
322
+ tmpAutoChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
323
+ }
240
324
  }
241
325
 
242
326
  // ── Popup Management ──────────────────────────────────────────────────
@@ -278,6 +362,9 @@ class PictViewFlowToolbar extends libPictView
278
362
  case 'layout':
279
363
  this._buildLayoutPopup(tmpPopup);
280
364
  break;
365
+ case 'layout-algorithm':
366
+ this._buildLayoutAlgorithmPopup(tmpPopup);
367
+ break;
281
368
  case 'settings':
282
369
  this._buildSettingsPopup(tmpPopup);
283
370
  break;
@@ -366,6 +453,9 @@ class PictViewFlowToolbar extends libPictView
366
453
  case 'layout':
367
454
  tmpTriggerSelector = `#Flow-Toolbar-Layout-${tmpFlowViewIdentifier}`;
368
455
  break;
456
+ case 'layout-algorithm':
457
+ tmpTriggerSelector = `#Flow-Toolbar-Auto-${tmpFlowViewIdentifier}`;
458
+ break;
369
459
  case 'settings':
370
460
  tmpTriggerSelector = `#Flow-Toolbar-Settings-${tmpFlowViewIdentifier}`;
371
461
  break;
@@ -390,212 +480,180 @@ class PictViewFlowToolbar extends libPictView
390
480
  // ── Add Node Popup ────────────────────────────────────────────────────
391
481
 
392
482
  /**
393
- * Build the searchable Add Node popup content.
483
+ * Build the searchable Add Node popup content via templates with inline
484
+ * handlers. Search input fires `_filterNodeList` which re-renders the
485
+ * list section in place.
486
+ *
394
487
  * @param {HTMLElement} pContainer
395
488
  */
396
489
  _buildAddNodePopup(pContainer)
397
490
  {
398
- // Search wrapper
399
- let tmpSearchWrapper = document.createElement('div');
400
- tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
401
-
402
- let tmpSearchIcon = document.createElement('span');
403
- tmpSearchIcon.className = 'pict-flow-popup-search-icon';
491
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
404
492
  let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
405
- if (tmpIconProvider)
406
- {
407
- tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
408
- }
409
- tmpSearchWrapper.appendChild(tmpSearchIcon);
410
-
411
- let tmpSearchInput = document.createElement('input');
412
- tmpSearchInput.className = 'pict-flow-popup-search';
413
- tmpSearchInput.setAttribute('type', 'text');
414
- tmpSearchInput.setAttribute('placeholder', 'Search node types...');
415
- tmpSearchWrapper.appendChild(tmpSearchInput);
416
- pContainer.appendChild(tmpSearchWrapper);
417
-
418
- // Node list
419
- let tmpListDiv = document.createElement('div');
420
- tmpListDiv.className = 'pict-flow-popup-node-list';
421
- pContainer.appendChild(tmpListDiv);
422
-
423
- // Initial population
424
- this._populateNodeList(tmpListDiv, '');
425
-
426
- // Filter on input
427
- tmpSearchInput.addEventListener('input', () =>
428
- {
429
- this._populateNodeList(tmpListDiv, tmpSearchInput.value);
430
- });
493
+ let tmpSearchIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('search', 12) : '';
494
+
495
+ let tmpListID = `Flow-Toolbar-AddNodeList-${tmpFlowViewIdentifier}`;
496
+ let tmpSearchID = `Flow-Toolbar-AddNodeSearch-${tmpFlowViewIdentifier}`;
497
+
498
+ // Stage AppData and render the initial list HTML inline — the popup
499
+ // element isn't in the DOM yet, so we can't look it up via
500
+ // getElementById here. _filterNodeList handles updates after open.
501
+ let tmpInitialListHTML = this._renderNodeListHTML('');
502
+
503
+ this.pict.ContentAssignment.assignContent(pContainer,
504
+ '<div class="pict-flow-popup-search-wrapper">'
505
+ + '<span class="pict-flow-popup-search-icon">' + tmpSearchIconHTML + '</span>'
506
+ + '<input id="' + tmpSearchID + '" class="pict-flow-popup-search" type="text" placeholder="Search node types..." '
507
+ + 'oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._filterNodeList(\'' + tmpListID + '\', this.value)" />'
508
+ + '</div>'
509
+ + '<div id="' + tmpListID + '" class="pict-flow-popup-node-list">' + tmpInitialListHTML + '</div>');
431
510
  }
432
511
 
433
512
  /**
434
- * Populate the node list in the Add Node popup, filtered by search text.
435
- * @param {HTMLElement} pListDiv
513
+ * Build the HTML string for the Add Node list filtered by the given
514
+ * text. Stages AppData and renders the {~TS:~} template into a string;
515
+ * returns "no matches" markup when nothing matches.
516
+ *
436
517
  * @param {string} pFilter
518
+ * @returns {string}
437
519
  */
438
- _populateNodeList(pListDiv, pFilter)
520
+ _renderNodeListHTML(pFilter)
439
521
  {
440
- if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
441
-
442
- // Clear
443
- while (pListDiv.firstChild)
444
- {
445
- pListDiv.removeChild(pListDiv.firstChild);
446
- }
522
+ if (!this._FlowView || !this._FlowView._NodeTypeProvider) return '';
447
523
 
524
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
448
525
  let tmpTypes = this._FlowView._NodeTypeProvider.getNodeTypes();
449
526
  let tmpTypeKeys = Object.keys(tmpTypes);
450
527
  let tmpFilter = (pFilter || '').toLowerCase().trim();
451
528
  let tmpIconProvider = this._FlowView._IconProvider;
452
- let tmpMatchCount = 0;
453
529
 
530
+ let tmpRows = [];
454
531
  for (let i = 0; i < tmpTypeKeys.length; i++)
455
532
  {
456
533
  let tmpTypeConfig = tmpTypes[tmpTypeKeys[i]];
457
534
  let tmpMeta = tmpTypeConfig.CardMetadata || {};
458
-
459
- // Skip disabled cards
460
535
  if (tmpMeta.Enabled === false) continue;
461
536
 
462
- // Filter match: label, code, or category
463
537
  if (tmpFilter)
464
538
  {
465
539
  let tmpLabel = (tmpTypeConfig.Label || '').toLowerCase();
466
540
  let tmpCode = (tmpMeta.Code || '').toLowerCase();
467
541
  let tmpCategory = (tmpMeta.Category || '').toLowerCase();
468
- if (tmpLabel.indexOf(tmpFilter) < 0 &&
469
- tmpCode.indexOf(tmpFilter) < 0 &&
470
- tmpCategory.indexOf(tmpFilter) < 0)
471
- {
472
- continue;
473
- }
542
+ if (tmpLabel.indexOf(tmpFilter) < 0 && tmpCode.indexOf(tmpFilter) < 0 && tmpCategory.indexOf(tmpFilter) < 0) continue;
474
543
  }
475
544
 
476
- tmpMatchCount++;
477
-
478
- let tmpRow = document.createElement('div');
479
- tmpRow.className = 'pict-flow-popup-list-item';
480
- tmpRow.setAttribute('data-node-type', tmpTypeKeys[i]);
481
-
482
- // Icon
483
- let tmpIconSpan = document.createElement('span');
484
- tmpIconSpan.className = 'pict-flow-popup-list-item-icon';
545
+ let tmpIconHTML = '';
485
546
  if (tmpIconProvider)
486
547
  {
487
548
  let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
488
- tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
489
- }
490
- tmpRow.appendChild(tmpIconSpan);
491
-
492
- // Label
493
- let tmpLabelSpan = document.createElement('span');
494
- tmpLabelSpan.className = 'pict-flow-popup-list-item-label';
495
- tmpLabelSpan.textContent = tmpTypeConfig.Label;
496
- tmpRow.appendChild(tmpLabelSpan);
497
-
498
- // Code badge
499
- if (tmpMeta.Code)
500
- {
501
- let tmpCodeSpan = document.createElement('span');
502
- tmpCodeSpan.className = 'pict-flow-popup-list-item-code';
503
- tmpCodeSpan.textContent = tmpMeta.Code;
504
- tmpRow.appendChild(tmpCodeSpan);
549
+ tmpIconHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
505
550
  }
506
-
507
- // Click handler
508
- tmpRow.addEventListener('click', () =>
551
+ tmpRows.push(
509
552
  {
510
- this._addNodeAtCenter(tmpTypeKeys[i]);
511
- this._closePopup();
553
+ NodeType: tmpTypeKeys[i],
554
+ Label: tmpTypeConfig.Label || '',
555
+ IconHTML: tmpIconHTML,
556
+ Code: tmpMeta.Code || '',
557
+ FlowViewIdentifier: tmpFlowViewIdentifier
512
558
  });
513
-
514
- pListDiv.appendChild(tmpRow);
515
559
  }
516
560
 
517
- if (tmpMatchCount === 0)
561
+ if (tmpRows.length === 0)
518
562
  {
519
- let tmpEmpty = document.createElement('div');
520
- tmpEmpty.className = 'pict-flow-popup-list-empty';
521
- tmpEmpty.textContent = 'No matching node types';
522
- pListDiv.appendChild(tmpEmpty);
563
+ return '<div class="pict-flow-popup-list-empty">No matching node types</div>';
523
564
  }
565
+ return this.pict.parseTemplateByHash('Flow-AddNode-List', { Rows: tmpRows });
566
+ }
567
+
568
+ /**
569
+ * Update the Add Node list contents in response to a search input
570
+ * change. Called from the inline `oninput` handler. The list element
571
+ * is already in the DOM by the time this fires.
572
+ *
573
+ * @param {string} pListID
574
+ * @param {string} pFilter
575
+ */
576
+ _filterNodeList(pListID, pFilter)
577
+ {
578
+ let tmpListEl = document.getElementById(pListID);
579
+ if (!tmpListEl) return;
580
+ this.pict.ContentAssignment.assignContent(tmpListEl, this._renderNodeListHTML(pFilter));
581
+ }
582
+
583
+ /**
584
+ * Inline handler for an Add Node row click. Adds the node at viewport
585
+ * center and closes the popup.
586
+ *
587
+ * @param {string} pNodeType
588
+ */
589
+ _addNodeFromPopup(pNodeType)
590
+ {
591
+ this._addNodeAtCenter(pNodeType);
592
+ this._closePopup();
524
593
  }
525
594
 
526
595
  // ── Cards Popup ───────────────────────────────────────────────────────
527
596
 
528
597
  /**
529
- * Build the Cards popup content with search and categorized palette.
598
+ * Build the Cards popup using templates with inline handlers. Search
599
+ * input fires `_filterCardsList` which re-renders the categorized list.
600
+ *
530
601
  * @param {HTMLElement} pContainer
531
602
  */
532
603
  _buildCardsPopup(pContainer)
533
604
  {
534
- // Search wrapper
535
- let tmpSearchWrapper = document.createElement('div');
536
- tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
537
-
538
- let tmpSearchIcon = document.createElement('span');
539
- tmpSearchIcon.className = 'pict-flow-popup-search-icon';
605
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
540
606
  let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
541
- if (tmpIconProvider)
542
- {
543
- tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
544
- }
545
- tmpSearchWrapper.appendChild(tmpSearchIcon);
607
+ let tmpSearchIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('search', 12) : '';
546
608
 
547
- let tmpSearchInput = document.createElement('input');
548
- tmpSearchInput.className = 'pict-flow-popup-search';
549
- tmpSearchInput.setAttribute('type', 'text');
550
- tmpSearchInput.setAttribute('placeholder', 'Search cards...');
551
- tmpSearchWrapper.appendChild(tmpSearchInput);
552
- pContainer.appendChild(tmpSearchWrapper);
609
+ let tmpListID = `Flow-Toolbar-CardsList-${tmpFlowViewIdentifier}`;
610
+ let tmpSearchID = `Flow-Toolbar-CardsSearch-${tmpFlowViewIdentifier}`;
553
611
 
554
- // Palette list container
555
- let tmpListDiv = document.createElement('div');
556
- tmpListDiv.className = 'pict-flow-popup-node-list';
557
- pContainer.appendChild(tmpListDiv);
612
+ // Initial render inline — popup not yet in DOM.
613
+ let tmpInitialListHTML = this._renderCardsListHTML('');
558
614
 
559
- // Initial population
560
- this._renderPalette(tmpListDiv, '');
615
+ this.pict.ContentAssignment.assignContent(pContainer,
616
+ '<div class="pict-flow-popup-search-wrapper">'
617
+ + '<span class="pict-flow-popup-search-icon">' + tmpSearchIconHTML + '</span>'
618
+ + '<input id="' + tmpSearchID + '" class="pict-flow-popup-search" type="text" placeholder="Search cards..." '
619
+ + 'oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._filterCardsList(\'' + tmpListID + '\', this.value)" />'
620
+ + '</div>'
621
+ + '<div id="' + tmpListID + '" class="pict-flow-popup-node-list">' + tmpInitialListHTML + '</div>');
561
622
 
562
- // Filter on input
563
- tmpSearchInput.addEventListener('input', () =>
623
+ // Focus search input — defer past the popup append.
624
+ setTimeout(() =>
564
625
  {
565
- this._renderPalette(tmpListDiv, tmpSearchInput.value);
566
- });
567
-
568
- // Focus search input
569
- setTimeout(() => { tmpSearchInput.focus(); }, 50);
626
+ let tmpSearch = document.getElementById(tmpSearchID);
627
+ if (tmpSearch) tmpSearch.focus();
628
+ }, 50);
570
629
  }
571
630
 
572
631
  /**
573
- * Render the card palette with categories and card chips into a container.
574
- * @param {HTMLElement} pContainer - The target container element
575
- * @param {string} [pFilter] - Optional search filter text
632
+ * Build the cards-list HTML string for the given filter. Stages
633
+ * AppData and renders via the {~TS:~} templates; returns "no matches"
634
+ * markup when nothing matches.
635
+ *
636
+ * @param {string} pFilter
637
+ * @returns {string}
576
638
  */
577
- _renderPalette(pContainer, pFilter)
639
+ _renderCardsListHTML(pFilter)
578
640
  {
579
- if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
580
-
581
- // Clear existing content
582
- while (pContainer.firstChild)
583
- {
584
- pContainer.removeChild(pContainer.firstChild);
585
- }
641
+ if (!this._FlowView || !this._FlowView._NodeTypeProvider) return '';
586
642
 
643
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
587
644
  let tmpCategories = this._FlowView._NodeTypeProvider.getCardsByCategory();
588
645
  let tmpCategoryKeys = Object.keys(tmpCategories);
589
646
  let tmpFilter = (pFilter || '').toLowerCase().trim();
647
+ let tmpIconProvider = this._FlowView._IconProvider;
590
648
  let tmpTotalMatchCount = 0;
591
649
 
650
+ let tmpCategoryRecords = [];
592
651
  for (let i = 0; i < tmpCategoryKeys.length; i++)
593
652
  {
594
653
  let tmpCategoryName = tmpCategoryKeys[i];
595
654
  let tmpCards = tmpCategories[tmpCategoryName];
596
- let tmpMatchingCards = [];
655
+ let tmpMatching = [];
597
656
 
598
- // Filter cards within this category
599
657
  for (let j = 0; j < tmpCards.length; j++)
600
658
  {
601
659
  let tmpCardConfig = tmpCards[j];
@@ -606,413 +664,916 @@ class PictViewFlowToolbar extends libPictView
606
664
  let tmpLabel = (tmpCardConfig.Label || '').toLowerCase();
607
665
  let tmpCode = (tmpMeta.Code || '').toLowerCase();
608
666
  let tmpCategory = tmpCategoryName.toLowerCase();
609
- if (tmpLabel.indexOf(tmpFilter) < 0 &&
610
- tmpCode.indexOf(tmpFilter) < 0 &&
611
- tmpCategory.indexOf(tmpFilter) < 0)
612
- {
613
- continue;
614
- }
667
+ if (tmpLabel.indexOf(tmpFilter) < 0 && tmpCode.indexOf(tmpFilter) < 0 && tmpCategory.indexOf(tmpFilter) < 0) continue;
615
668
  }
616
669
 
617
- tmpMatchingCards.push(tmpCardConfig);
618
- }
619
-
620
- if (tmpMatchingCards.length === 0) continue;
621
-
622
- tmpTotalMatchCount += tmpMatchingCards.length;
623
-
624
- let tmpCategoryDiv = document.createElement('div');
625
- tmpCategoryDiv.className = 'pict-flow-palette-category';
626
- tmpCategoryDiv.style.padding = '0.35em 0.5em';
627
-
628
- let tmpCategoryLabel = document.createElement('div');
629
- tmpCategoryLabel.className = 'pict-flow-palette-category-label';
630
- tmpCategoryLabel.textContent = tmpCategoryName;
631
- tmpCategoryDiv.appendChild(tmpCategoryLabel);
632
-
633
- let tmpCardsDiv = document.createElement('div');
634
- tmpCardsDiv.className = 'pict-flow-palette-cards';
635
-
636
- for (let j = 0; j < tmpMatchingCards.length; j++)
637
- {
638
- let tmpCardConfig = tmpMatchingCards[j];
639
- let tmpMeta = tmpCardConfig.CardMetadata || {};
640
-
641
- let tmpCardEl = document.createElement('div');
642
- tmpCardEl.className = 'pict-flow-palette-card';
643
- if (tmpMeta.Enabled === false)
644
- {
645
- tmpCardEl.classList.add('disabled');
646
- }
647
- tmpCardEl.setAttribute('data-card-type', tmpCardConfig.Hash);
648
-
649
- if (tmpMeta.Tooltip)
650
- {
651
- tmpCardEl.setAttribute('title', tmpMeta.Tooltip);
652
- }
653
- else if (tmpMeta.Description)
654
- {
655
- tmpCardEl.setAttribute('title', tmpMeta.Description);
656
- }
657
-
658
- // Icon or color swatch
659
- if (tmpMeta.Icon)
660
- {
661
- let tmpIconSpan = document.createElement('span');
662
- tmpIconSpan.className = 'pict-flow-palette-card-icon';
663
- let tmpIconProvider = this._FlowView._IconProvider;
664
- if (tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
665
- {
666
- let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
667
- tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 14);
668
- }
669
- else
670
- {
671
- tmpIconSpan.textContent = tmpMeta.Icon;
672
- }
673
- tmpCardEl.appendChild(tmpIconSpan);
674
- }
675
- else if (this._FlowView._IconProvider)
670
+ let tmpIconHTML = '';
671
+ let tmpIsEmoji = false;
672
+ if (tmpMeta.Icon && tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
676
673
  {
677
- let tmpIconSpan = document.createElement('span');
678
- tmpIconSpan.className = 'pict-flow-palette-card-icon';
679
- tmpIconSpan.innerHTML = this._FlowView._IconProvider.getIconSVGMarkup('default', 14);
680
- tmpCardEl.appendChild(tmpIconSpan);
674
+ tmpIconHTML = tmpIconProvider.getIconSVGMarkup(tmpIconProvider.resolveIconKey(tmpMeta), 14);
681
675
  }
682
- else if (tmpCardConfig.TitleBarColor)
676
+ else if (tmpMeta.Icon)
683
677
  {
684
- let tmpSwatch = document.createElement('span');
685
- tmpSwatch.className = 'pict-flow-palette-card-swatch';
686
- tmpSwatch.style.backgroundColor = tmpCardConfig.TitleBarColor;
687
- tmpCardEl.appendChild(tmpSwatch);
678
+ tmpIsEmoji = true;
688
679
  }
689
-
690
- // Title
691
- let tmpTitleSpan = document.createElement('span');
692
- tmpTitleSpan.className = 'pict-flow-palette-card-title';
693
- tmpTitleSpan.textContent = tmpCardConfig.Label;
694
- tmpCardEl.appendChild(tmpTitleSpan);
695
-
696
- // Code badge
697
- if (tmpMeta.Code)
680
+ else if (tmpIconProvider)
698
681
  {
699
- let tmpCodeSpan = document.createElement('span');
700
- tmpCodeSpan.className = 'pict-flow-palette-card-code';
701
- tmpCodeSpan.textContent = tmpMeta.Code;
702
- tmpCardEl.appendChild(tmpCodeSpan);
682
+ tmpIconHTML = tmpIconProvider.getIconSVGMarkup('default', 14);
703
683
  }
704
684
 
705
- // Click handler
706
- tmpCardEl.addEventListener('click', () =>
685
+ tmpMatching.push(
707
686
  {
708
- this._addCardFromPalette(tmpCardConfig.Hash);
709
- this._closePopup();
687
+ CardType: tmpCardConfig.Hash,
688
+ Label: tmpCardConfig.Label || '',
689
+ Code: tmpMeta.Code || '',
690
+ IconHTML: tmpIconHTML,
691
+ IconEmoji: tmpIsEmoji ? tmpMeta.Icon : '',
692
+ DisabledClass: (tmpMeta.Enabled === false) ? ' disabled' : '',
693
+ Tooltip: tmpMeta.Tooltip || tmpMeta.Description || '',
694
+ SwatchColor: (!tmpIconHTML && !tmpIsEmoji && tmpCardConfig.TitleBarColor) ? tmpCardConfig.TitleBarColor : '',
695
+ FlowViewIdentifier: tmpFlowViewIdentifier
710
696
  });
711
-
712
- tmpCardsDiv.appendChild(tmpCardEl);
713
697
  }
714
698
 
715
- tmpCategoryDiv.appendChild(tmpCardsDiv);
716
- pContainer.appendChild(tmpCategoryDiv);
699
+ if (tmpMatching.length === 0) continue;
700
+ tmpTotalMatchCount += tmpMatching.length;
701
+ tmpCategoryRecords.push(
702
+ {
703
+ Name: tmpCategoryName,
704
+ Cards: tmpMatching,
705
+ FlowViewIdentifier: tmpFlowViewIdentifier
706
+ });
717
707
  }
718
708
 
719
709
  if (tmpTotalMatchCount === 0)
720
710
  {
721
- let tmpEmpty = document.createElement('div');
722
- tmpEmpty.className = 'pict-flow-popup-list-empty';
723
- tmpEmpty.textContent = tmpFilter ? 'No matching cards' : 'No card types available';
724
- pContainer.appendChild(tmpEmpty);
711
+ return '<div class="pict-flow-popup-list-empty">' + (tmpFilter ? 'No matching cards' : 'No card types available') + '</div>';
725
712
  }
713
+ return this.pict.parseTemplateByHash('Flow-Cards-List', { Categories: tmpCategoryRecords });
714
+ }
715
+
716
+ /**
717
+ * Inline `oninput` handler — refresh card list with the new filter.
718
+ */
719
+ _filterCardsList(pListID, pFilter)
720
+ {
721
+ let tmpListEl = document.getElementById(pListID);
722
+ if (!tmpListEl) return;
723
+ this.pict.ContentAssignment.assignContent(tmpListEl, this._renderCardsListHTML(pFilter));
724
+ }
725
+
726
+ /**
727
+ * Inline handler for a card click. Adds the node and closes the popup.
728
+ *
729
+ * @param {string} pCardType
730
+ */
731
+ _addCardFromPopup(pCardType)
732
+ {
733
+ this._addCardFromPalette(pCardType);
734
+ this._closePopup();
726
735
  }
727
736
 
728
737
  // ── Layout Popup ──────────────────────────────────────────────────────
729
738
 
730
739
  /**
731
- * Build the Layout popup content.
740
+ * Build the Layout popup using templates with inline handlers.
741
+ *
742
+ * Layout records (name + hash) are pushed into AppData and iterated via
743
+ * a {~TS:~} template. The save section is a static fragment with inline
744
+ * handlers that call the toolbar's helper methods.
745
+ *
732
746
  * @param {HTMLElement} pContainer
733
747
  */
734
748
  _buildLayoutPopup(pContainer)
735
749
  {
750
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
736
751
  let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
752
+ let tmpSaveIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('save', 14) : '✓';
753
+ let tmpDeleteIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('trash', 12) : '×';
754
+
755
+ let tmpSaveRowID = `Flow-Toolbar-LayoutSaveRow-${tmpFlowViewIdentifier}`;
756
+ let tmpSaveInputRowID = `Flow-Toolbar-LayoutSaveInputRow-${tmpFlowViewIdentifier}`;
757
+ let tmpSaveInputID = `Flow-Toolbar-LayoutSaveInput-${tmpFlowViewIdentifier}`;
758
+ let tmpListID = `Flow-Toolbar-LayoutList-${tmpFlowViewIdentifier}`;
759
+
760
+ // Build layout records for the {~TS:~} iteration
761
+ let tmpLayouts = (this._FlowView && this._FlowView._LayoutProvider)
762
+ ? this._FlowView._LayoutProvider.getLayouts() : [];
763
+ let tmpLayoutRecords = [];
764
+ for (let i = 0; i < tmpLayouts.length; i++)
765
+ {
766
+ tmpLayoutRecords.push(
767
+ {
768
+ LayoutHash: tmpLayouts[i].Hash,
769
+ Name: tmpLayouts[i].Name || '',
770
+ DeleteIconHTML: tmpDeleteIconHTML,
771
+ FlowViewIdentifier: tmpFlowViewIdentifier
772
+ });
773
+ }
737
774
 
738
- // Save Layout section at top
739
- let tmpSaveSection = document.createElement('div');
740
- tmpSaveSection.className = 'pict-flow-popup-layout-save-section';
741
-
742
- // Save input row (hidden initially)
743
- let tmpSaveInputRow = document.createElement('div');
744
- tmpSaveInputRow.className = 'pict-flow-popup-layout-save-input-row';
745
- tmpSaveInputRow.style.display = 'none';
775
+ let tmpListHTML = (tmpLayoutRecords.length === 0)
776
+ ? '<div class="pict-flow-popup-list-empty">No saved layouts</div>'
777
+ : this.pict.parseTemplateByHash('Flow-Layout-List', { Rows: tmpLayoutRecords });
778
+
779
+ this.pict.ContentAssignment.assignContent(pContainer,
780
+ '<div class="pict-flow-popup-layout-save-section">'
781
+ + '<div id="' + tmpSaveRowID + '" class="pict-flow-popup-layout-save"'
782
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._showLayoutSaveInput(\'' + tmpSaveRowID + '\', \'' + tmpSaveInputRowID + '\', \'' + tmpSaveInputID + '\')">'
783
+ + '<span class="pict-flow-popup-layout-save-icon">' + tmpSaveIconHTML + '</span>'
784
+ + '<span>Save Current Layout</span>'
785
+ + '</div>'
786
+ + '<div id="' + tmpSaveInputRowID + '" class="pict-flow-popup-layout-save-input-row" style="display:none;">'
787
+ + '<input id="' + tmpSaveInputID + '" class="pict-flow-popup-layout-save-input" type="text" placeholder="Layout name..."'
788
+ + ' onclick="event.stopPropagation()"'
789
+ + ' onkeydown="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleLayoutSaveKey(event, \'' + tmpSaveInputID + '\', \'' + tmpSaveRowID + '\', \'' + tmpSaveInputRowID + '\')" />'
790
+ + '<button class="pict-flow-popup-layout-save-confirm" title="Save"'
791
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._confirmLayoutSave(\'' + tmpSaveInputID + '\')">'
792
+ + tmpSaveIconHTML + '</button>'
793
+ + '</div>'
794
+ + '</div>'
795
+ + '<div class="pict-flow-popup-divider"></div>'
796
+ + '<div id="' + tmpListID + '">' + tmpListHTML + '</div>');
797
+ }
746
798
 
747
- let tmpSaveInput = document.createElement('input');
748
- tmpSaveInput.className = 'pict-flow-popup-layout-save-input';
749
- tmpSaveInput.setAttribute('type', 'text');
750
- tmpSaveInput.setAttribute('placeholder', 'Layout name...');
751
- tmpSaveInputRow.appendChild(tmpSaveInput);
799
+ /**
800
+ * Inline handler — reveal the layout-name input and focus it.
801
+ */
802
+ _showLayoutSaveInput(pSaveRowID, pInputRowID, pInputID)
803
+ {
804
+ let tmpRow = document.getElementById(pSaveRowID);
805
+ let tmpInputRow = document.getElementById(pInputRowID);
806
+ let tmpInput = document.getElementById(pInputID);
807
+ if (tmpRow) tmpRow.style.display = 'none';
808
+ if (tmpInputRow) tmpInputRow.style.display = '';
809
+ if (tmpInput)
810
+ {
811
+ tmpInput.value = '';
812
+ setTimeout(() => { tmpInput.focus(); }, 50);
813
+ }
814
+ }
752
815
 
753
- let tmpSaveConfirmBtn = document.createElement('button');
754
- tmpSaveConfirmBtn.className = 'pict-flow-popup-layout-save-confirm';
755
- tmpSaveConfirmBtn.title = 'Save';
756
- if (tmpIconProvider)
816
+ /**
817
+ * Inline handler — Enter saves, Escape cancels.
818
+ */
819
+ _handleLayoutSaveKey(pEvent, pInputID, pSaveRowID, pInputRowID)
820
+ {
821
+ if (pEvent.key === 'Enter')
757
822
  {
758
- tmpSaveConfirmBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
823
+ pEvent.preventDefault();
824
+ this._confirmLayoutSave(pInputID);
759
825
  }
760
- else
826
+ else if (pEvent.key === 'Escape')
761
827
  {
762
- tmpSaveConfirmBtn.textContent = '✓';
828
+ let tmpRow = document.getElementById(pSaveRowID);
829
+ let tmpInputRow = document.getElementById(pInputRowID);
830
+ if (tmpInputRow) tmpInputRow.style.display = 'none';
831
+ if (tmpRow) tmpRow.style.display = '';
763
832
  }
764
- tmpSaveInputRow.appendChild(tmpSaveConfirmBtn);
833
+ }
765
834
 
766
- // "Save Current Layout" clickable row
767
- let tmpSaveRow = document.createElement('div');
768
- tmpSaveRow.className = 'pict-flow-popup-layout-save';
835
+ /**
836
+ * Inline handler — save the layout under the entered name and rebuild
837
+ * the popup so the new entry appears.
838
+ */
839
+ _confirmLayoutSave(pInputID)
840
+ {
841
+ let tmpInput = document.getElementById(pInputID);
842
+ if (!tmpInput) return;
843
+ let tmpName = (tmpInput.value || '').trim();
844
+ if (tmpName === '') return;
845
+ this._FlowView._LayoutProvider.saveLayout(tmpName);
769
846
 
770
- let tmpSaveIcon = document.createElement('span');
771
- tmpSaveIcon.className = 'pict-flow-popup-layout-save-icon';
772
- if (tmpIconProvider)
773
- {
774
- tmpSaveIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
775
- }
776
- tmpSaveRow.appendChild(tmpSaveIcon);
847
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
848
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
849
+ if (tmpPopupEl) this._buildLayoutPopup(tmpPopupEl);
850
+ }
851
+
852
+ /**
853
+ * Inline handler — restore layout when the row (not its delete button)
854
+ * is clicked.
855
+ */
856
+ _restoreLayoutFromPopup(pLayoutHash, pEvent)
857
+ {
858
+ if (pEvent && pEvent.target && pEvent.target.closest('.pict-flow-popup-layout-delete')) return;
859
+ this._FlowView._LayoutProvider.restoreLayout(pLayoutHash);
860
+ this._closePopup();
861
+ }
862
+
863
+ /**
864
+ * Inline handler — delete a saved layout and refresh the popup.
865
+ */
866
+ _deleteLayoutFromPopup(pLayoutHash, pEvent)
867
+ {
868
+ if (pEvent && typeof pEvent.stopPropagation === 'function') pEvent.stopPropagation();
869
+ this._FlowView._LayoutProvider.deleteLayout(pLayoutHash);
870
+
871
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
872
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
873
+ if (tmpPopupEl) this._buildLayoutPopup(tmpPopupEl);
874
+ }
875
+
876
+ // ── Layout Algorithm Popup ────────────────────────────────────────────
877
+
878
+ /**
879
+ * Build the Layout Algorithm popup using templates with inline handlers.
880
+ * The popup has three logical sections (algorithm chooser, edge-theme
881
+ * chooser, auto-apply toggle + apply button). Each control's behavior
882
+ * lives on the toolbar as a helper method called from inline handlers.
883
+ *
884
+ * Iterations (algorithm options, edge themes) are template-driven via
885
+ * AppData arrays.
886
+ *
887
+ * @param {HTMLElement} pContainer
888
+ */
889
+ _buildLayoutAlgorithmPopup(pContainer)
890
+ {
891
+ if (!this._FlowView || !this._FlowView._LayoutService) return;
777
892
 
778
- let tmpSaveText = document.createElement('span');
779
- tmpSaveText.textContent = 'Save Current Layout';
780
- tmpSaveRow.appendChild(tmpSaveText);
893
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
894
+ let tmpLayoutService = this._FlowView._LayoutService;
895
+ let tmpCurrentSettings = this._FlowView.getLayoutAlgorithm();
896
+ let tmpAlgoDescriptor = tmpLayoutService.getAlgorithm(tmpCurrentSettings.Algorithm);
897
+ let tmpEdgeSettings = this._FlowView.getEdgeTheme();
898
+ let tmpResolvedEdge = tmpEdgeSettings.Theme;
899
+ let tmpExplicitEdgeOverride = tmpEdgeSettings.Override;
900
+ let tmpDefaultThemeName = (tmpAlgoDescriptor && tmpAlgoDescriptor.DefaultEdgeTheme) || 'Bezier';
901
+ let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
902
+ let tmpSettingsIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('settings', 13) : '⚙';
781
903
 
782
- // Click "Save Current Layout" to reveal the input row
783
- tmpSaveRow.addEventListener('click', () =>
904
+ // Build option records for the dropdowns
905
+ let tmpAlgoOptions = tmpLayoutService.listAlgorithms().map((pAlgo) => (
784
906
  {
785
- tmpSaveRow.style.display = 'none';
786
- tmpSaveInputRow.style.display = '';
787
- tmpSaveInput.value = '';
788
- setTimeout(() => { tmpSaveInput.focus(); }, 50);
789
- });
907
+ Value: pAlgo.Name,
908
+ Label: pAlgo.Label || pAlgo.Name,
909
+ SelectedAttr: (pAlgo.Name === tmpCurrentSettings.Algorithm) ? ' selected="selected"' : ''
910
+ }));
790
911
 
791
- // Confirm save via button click
792
- let tmpDoSave = () =>
912
+ let tmpEdgeOptionRecords = [];
913
+ tmpEdgeOptionRecords.push(
793
914
  {
794
- let tmpName = tmpSaveInput.value.trim();
795
- if (tmpName === '') return;
796
- this._FlowView._LayoutProvider.saveLayout(tmpName);
797
- // Refresh the popup content
798
- while (pContainer.firstChild)
915
+ Value: '__inherit__',
916
+ Label: `Inherit from layout (${tmpDefaultThemeName})`,
917
+ SelectedAttr: !tmpExplicitEdgeOverride ? ' selected="selected"' : ''
918
+ });
919
+ let tmpEdgeThemes = tmpLayoutService.listEdgeThemes();
920
+ for (let i = 0; i < tmpEdgeThemes.length; i++)
921
+ {
922
+ tmpEdgeOptionRecords.push(
799
923
  {
800
- pContainer.removeChild(pContainer.firstChild);
801
- }
802
- this._buildLayoutPopup(pContainer);
803
- };
924
+ Value: tmpEdgeThemes[i].Name,
925
+ Label: tmpEdgeThemes[i].Label || tmpEdgeThemes[i].Name,
926
+ SelectedAttr: (tmpEdgeThemes[i].Name === tmpExplicitEdgeOverride) ? ' selected="selected"' : ''
927
+ });
928
+ }
929
+
930
+ let tmpAlgoOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpAlgoOptions });
931
+ let tmpEdgeOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpEdgeOptionRecords });
804
932
 
805
- tmpSaveConfirmBtn.addEventListener('click', tmpDoSave);
933
+ let tmpHasParameters = !!(tmpAlgoDescriptor && (
934
+ (tmpAlgoDescriptor.ParameterManifest && tmpAlgoDescriptor.ParameterManifest.Descriptors) ||
935
+ (tmpAlgoDescriptor.ParameterSchema && Object.keys(tmpAlgoDescriptor.ParameterSchema).length > 0)
936
+ ));
806
937
 
807
- // Confirm save via Enter key
808
- tmpSaveInput.addEventListener('keydown', (pEvent) =>
938
+ let tmpAlgoDescription = (tmpAlgoDescriptor && tmpAlgoDescriptor.Description)
939
+ ? '<div class="pict-flow-popup-control-description">' + tmpAlgoDescriptor.Description + '</div>'
940
+ : '';
941
+
942
+ let tmpFormToggleHTML = '';
943
+ if (tmpHasParameters)
809
944
  {
810
- if (pEvent.key === 'Enter')
811
- {
812
- pEvent.preventDefault();
813
- tmpDoSave();
814
- }
815
- else if (pEvent.key === 'Escape')
945
+ tmpFormToggleHTML = '<button type="button" class="pict-flow-popup-collapse-toggle"'
946
+ + ' aria-expanded="' + (this._LayoutFormExpanded ? 'true' : 'false') + '"'
947
+ + ' title="' + (this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters') + '"'
948
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._toggleLayoutFormCollapsed(this)">'
949
+ + tmpSettingsIconHTML
950
+ + '</button>';
951
+ }
952
+
953
+ let tmpAutoApplyID = `Flow-Toolbar-AutoApply-${tmpFlowViewIdentifier}`;
954
+ let tmpApplyDisabled = (tmpCurrentSettings.Algorithm === 'Custom') ? ' disabled' : '';
955
+ let tmpApplyTitle = (tmpCurrentSettings.Algorithm === 'Custom') ? ' title="Custom does not auto-position nodes"' : '';
956
+
957
+ let tmpEdgeDescriptionHTML = '';
958
+ if (tmpResolvedEdge)
959
+ {
960
+ let tmpResolvedDescriptor = tmpLayoutService.getEdgeTheme(tmpResolvedEdge);
961
+ if (tmpResolvedDescriptor && tmpResolvedDescriptor.Description)
816
962
  {
817
- // Cancel hide input, show the save row again
818
- tmpSaveInputRow.style.display = 'none';
819
- tmpSaveRow.style.display = '';
963
+ tmpEdgeDescriptionHTML = '<div class="pict-flow-popup-control-description">' + tmpResolvedDescriptor.Description + '</div>';
820
964
  }
821
- });
965
+ }
822
966
 
823
- // Prevent clicks inside the input from closing the popup
824
- tmpSaveInput.addEventListener('click', (pEvent) =>
967
+ // Place a host div for the parameter form; mount the form into it
968
+ // after the popup is in the DOM (the metacontroller resolves
969
+ // destinations via document.querySelector).
970
+ let tmpParamFormHostID = `Flow-Toolbar-LayoutParamFormHost-${tmpFlowViewIdentifier}`;
971
+
972
+ this.pict.ContentAssignment.assignContent(pContainer,
973
+ '<div class="pict-flow-popup-settings-section pict-flow-popup-layout-algorithm-row">'
974
+ + '<label class="pict-flow-popup-settings-label">Algorithm</label>'
975
+ + '<div class="pict-flow-popup-layout-algorithm-controls">'
976
+ + '<select class="pict-flow-popup-settings-select pict-flow-popup-layout-algorithm-select" data-layout-control="algorithm"'
977
+ + ' onclick="event.stopPropagation()"'
978
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleLayoutAlgoChange(this.value)">'
979
+ + tmpAlgoOptionsHTML
980
+ + '</select>'
981
+ + tmpFormToggleHTML
982
+ + '</div>'
983
+ + '</div>'
984
+ + tmpAlgoDescription
985
+ + '<div id="' + tmpParamFormHostID + '"></div>'
986
+ + '<div class="pict-flow-popup-divider"></div>'
987
+ + '<div class="pict-flow-popup-settings-section">'
988
+ + '<label class="pict-flow-popup-settings-label">Edge theme</label>'
989
+ + '<select class="pict-flow-popup-settings-select" data-layout-control="edge-theme"'
990
+ + ' onclick="event.stopPropagation()"'
991
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleEdgeThemeChange(this.value)">'
992
+ + tmpEdgeOptionsHTML
993
+ + '</select>'
994
+ + '</div>'
995
+ + tmpEdgeDescriptionHTML
996
+ + '<div class="pict-flow-popup-divider"></div>'
997
+ + '<div class="pict-flow-popup-settings-section" style="display:flex;align-items:center;gap:8px;">'
998
+ + '<input type="checkbox" id="' + tmpAutoApplyID + '"' + (tmpCurrentSettings.AutoApply ? ' checked="checked"' : '') + ''
999
+ + ' onclick="event.stopPropagation()"'
1000
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleAutoApplyChange(this.checked)" />'
1001
+ + '<label class="pict-flow-popup-settings-label" for="' + tmpAutoApplyID + '" style="cursor:pointer;">Auto-apply on changes</label>'
1002
+ + '</div>'
1003
+ + '<div class="pict-flow-popup-divider"></div>'
1004
+ + '<div class="pict-flow-popup-settings-section" style="padding:4px 8px;">'
1005
+ + '<button class="pict-flow-popup-layout-save-confirm" style="width:100%;padding:6px;"' + tmpApplyDisabled + tmpApplyTitle
1006
+ + ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleApplyLayoutNow()">Apply Now</button>'
1007
+ + '</div>');
1008
+
1009
+ // Mount the parameter form (handles its own DOM building /
1010
+ // metacontroller). Use pContainer.querySelector — the popup may
1011
+ // not be in the document yet, so document.getElementById would miss.
1012
+ let tmpFormHostEl = pContainer.querySelector('#' + tmpParamFormHostID);
1013
+ if (tmpFormHostEl)
825
1014
  {
826
- pEvent.stopPropagation();
827
- });
1015
+ this._buildLayoutParameterFormSection(tmpFormHostEl, tmpAlgoDescriptor, tmpCurrentSettings);
1016
+ }
1017
+ }
828
1018
 
829
- tmpSaveSection.appendChild(tmpSaveRow);
830
- tmpSaveSection.appendChild(tmpSaveInputRow);
831
- pContainer.appendChild(tmpSaveSection);
1019
+ /**
1020
+ * Inline handler — switch layout algorithm, refresh popup.
1021
+ */
1022
+ _handleLayoutAlgoChange(pName)
1023
+ {
1024
+ if (!this._FlowView || !this._FlowView._LayoutService) return;
1025
+ let tmpAlgo = this._FlowView._LayoutService.getAlgorithm(pName);
1026
+ let tmpDefaults = (tmpAlgo && tmpAlgo.DefaultParameters)
1027
+ ? JSON.parse(JSON.stringify(tmpAlgo.DefaultParameters)) : {};
1028
+ this._FlowView.setLayoutAlgorithm(pName, tmpDefaults);
832
1029
 
833
- // Divider
834
- let tmpDivider = document.createElement('div');
835
- tmpDivider.className = 'pict-flow-popup-divider';
836
- pContainer.appendChild(tmpDivider);
1030
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1031
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
1032
+ if (tmpPopupEl) this._buildLayoutAlgorithmPopup(tmpPopupEl);
1033
+ }
837
1034
 
838
- // Layout rows
839
- if (!this._FlowView || !this._FlowView._LayoutProvider)
1035
+ /**
1036
+ * Inline handler — switch edge theme, refresh popup so descriptions
1037
+ * track the new theme.
1038
+ */
1039
+ _handleEdgeThemeChange(pValue)
1040
+ {
1041
+ if (!this._FlowView) return;
1042
+ if (pValue === '__inherit__')
840
1043
  {
841
- let tmpEmpty = document.createElement('div');
842
- tmpEmpty.className = 'pict-flow-popup-list-empty';
843
- tmpEmpty.textContent = 'No saved layouts';
844
- pContainer.appendChild(tmpEmpty);
845
- return;
1044
+ this._FlowView.setEdgeTheme(null);
1045
+ }
1046
+ else
1047
+ {
1048
+ this._FlowView.setEdgeTheme(pValue);
846
1049
  }
847
1050
 
848
- let tmpLayouts = this._FlowView._LayoutProvider.getLayouts();
1051
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1052
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
1053
+ if (tmpPopupEl) this._buildLayoutAlgorithmPopup(tmpPopupEl);
1054
+ }
1055
+
1056
+ /**
1057
+ * Inline handler — toggle auto-apply.
1058
+ */
1059
+ _handleAutoApplyChange(pChecked)
1060
+ {
1061
+ if (!this._FlowView) return;
1062
+ this._FlowView.setLayoutAutoApply(pChecked);
1063
+ }
849
1064
 
850
- if (tmpLayouts.length === 0)
1065
+ /**
1066
+ * Inline handler — fire the layout (when not Custom).
1067
+ */
1068
+ _handleApplyLayoutNow()
1069
+ {
1070
+ if (!this._FlowView) return;
1071
+ let tmpSettings = this._FlowView.getLayoutAlgorithm();
1072
+ if (tmpSettings.Algorithm === 'Custom') return;
1073
+ this._FlowView.applyCurrentLayout();
1074
+ }
1075
+
1076
+ /**
1077
+ * Inline handler — toggle the parameter-form collapse state.
1078
+ */
1079
+ _toggleLayoutFormCollapsed(pToggleButton)
1080
+ {
1081
+ this._LayoutFormExpanded = !this._LayoutFormExpanded;
1082
+ pToggleButton.setAttribute('aria-expanded', this._LayoutFormExpanded ? 'true' : 'false');
1083
+ pToggleButton.title = this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters';
1084
+ let tmpHost = this._LayoutFormHostID ? document.getElementById(this._LayoutFormHostID) : null;
1085
+ if (tmpHost)
851
1086
  {
852
- let tmpEmpty = document.createElement('div');
853
- tmpEmpty.className = 'pict-flow-popup-list-empty';
854
- tmpEmpty.textContent = 'No saved layouts';
855
- pContainer.appendChild(tmpEmpty);
1087
+ tmpHost.setAttribute('data-collapsed', this._LayoutFormExpanded ? 'false' : 'true');
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * Render the parameter form section. When the host app has registered
1093
+ * `PictFormMetacontroller` and the algorithm provides a
1094
+ * `ParameterManifest`, we inject that manifest into a section-form so
1095
+ * the user gets the same Manyfest-driven UX as the rest of the Pict
1096
+ * ecosystem. Otherwise we fall back to schema-driven hand-rolled inputs.
1097
+ *
1098
+ * Data binding: `pict.AppData.PictFlowLayoutEditor.Parameters` is a
1099
+ * mirror of `_FlowData.LayoutParameters`. The form's Informary writes
1100
+ * directly into AppData; a `change`/`input` listener on the popup
1101
+ * pushes those edits back into `_FlowData.LayoutParameters` and (when
1102
+ * the active algorithm is non-Custom) re-applies the layout.
1103
+ *
1104
+ * @param {HTMLElement} pContainer
1105
+ * @param {Object} pAlgoDescriptor - the descriptor for the active algorithm
1106
+ * @param {{ Algorithm: string, Parameters: Object }} pCurrentSettings
1107
+ */
1108
+ _buildLayoutParameterFormSection(pContainer, pAlgoDescriptor, pCurrentSettings)
1109
+ {
1110
+ if (!pAlgoDescriptor) return;
1111
+
1112
+ let tmpHasManifest = !!(pAlgoDescriptor.ParameterManifest && pAlgoDescriptor.ParameterManifest.Descriptors);
1113
+ let tmpMetacontrollerType = this._resolveMetacontrollerServiceType();
1114
+
1115
+ if (tmpHasManifest && tmpMetacontrollerType)
1116
+ {
1117
+ this._mountLayoutParameterMetacontroller(pContainer, pAlgoDescriptor, pCurrentSettings, tmpMetacontrollerType);
856
1118
  return;
857
1119
  }
858
1120
 
859
- for (let i = 0; i < tmpLayouts.length; i++)
1121
+ // Fallback path schema-driven hand-rolled inputs.
1122
+ let tmpSchema = (pAlgoDescriptor.ParameterSchema) ? pAlgoDescriptor.ParameterSchema : {};
1123
+ let tmpParamKeys = Object.keys(tmpSchema);
1124
+ if (tmpParamKeys.length === 0) return;
1125
+
1126
+ let tmpParamDivider = document.createElement('div');
1127
+ tmpParamDivider.className = 'pict-flow-popup-divider';
1128
+ pContainer.appendChild(tmpParamDivider);
1129
+
1130
+ let tmpParamHeader = document.createElement('div');
1131
+ tmpParamHeader.className = 'pict-flow-popup-settings-label';
1132
+ tmpParamHeader.style.fontWeight = 'bold';
1133
+ tmpParamHeader.style.padding = '4px 8px';
1134
+ tmpParamHeader.textContent = 'Parameters';
1135
+ pContainer.appendChild(tmpParamHeader);
1136
+
1137
+ for (let i = 0; i < tmpParamKeys.length; i++)
860
1138
  {
861
- let tmpLayout = tmpLayouts[i];
1139
+ let tmpKey = tmpParamKeys[i];
1140
+ let tmpFieldSchema = tmpSchema[tmpKey];
1141
+ let tmpCurrentValue = (pCurrentSettings.Parameters && pCurrentSettings.Parameters.hasOwnProperty(tmpKey))
1142
+ ? pCurrentSettings.Parameters[tmpKey]
1143
+ : tmpFieldSchema.Default;
862
1144
 
863
1145
  let tmpRow = document.createElement('div');
864
- tmpRow.className = 'pict-flow-popup-layout-row';
865
-
866
- let tmpNameSpan = document.createElement('span');
867
- tmpNameSpan.className = 'pict-flow-popup-layout-name';
868
- tmpNameSpan.textContent = tmpLayout.Name;
869
- tmpRow.appendChild(tmpNameSpan);
1146
+ tmpRow.className = 'pict-flow-popup-settings-section';
1147
+ tmpRow.style.display = 'flex';
1148
+ tmpRow.style.alignItems = 'center';
1149
+ tmpRow.style.gap = '8px';
1150
+
1151
+ let tmpRowLabel = document.createElement('label');
1152
+ tmpRowLabel.className = 'pict-flow-popup-settings-label';
1153
+ tmpRowLabel.textContent = tmpFieldSchema.Label || tmpKey;
1154
+ tmpRowLabel.style.flex = '1';
1155
+ tmpRow.appendChild(tmpRowLabel);
1156
+
1157
+ let tmpInput = this._buildLayoutParamInput(tmpKey, tmpFieldSchema, tmpCurrentValue);
1158
+ tmpRow.appendChild(tmpInput);
1159
+ pContainer.appendChild(tmpRow);
1160
+ }
1161
+ }
870
1162
 
871
- // Delete button (visible on hover via CSS)
872
- let tmpDeleteBtn = document.createElement('button');
873
- tmpDeleteBtn.className = 'pict-flow-popup-layout-delete';
874
- tmpDeleteBtn.title = 'Delete layout';
875
- if (tmpIconProvider)
1163
+ /**
1164
+ * Remove `PictSectionForm-*` views the layout-algorithm popup has
1165
+ * previously registered on the host Pict. Without this, switching
1166
+ * algorithms accumulates dead section views — the metatemplate
1167
+ * generated by the next inject would still reference the old ones.
1168
+ * Idempotent.
1169
+ */
1170
+ _evictLayoutFormViews()
1171
+ {
1172
+ if (!this.pict || !this.pict.views) return;
1173
+ let tmpKeys = Object.keys(this.pict.views);
1174
+ for (let i = 0; i < tmpKeys.length; i++)
1175
+ {
1176
+ let tmpKey = tmpKeys[i];
1177
+ if (tmpKey.indexOf('PictSectionForm-') === 0 && tmpKey.indexOf('PictFlowLayout') > 0)
876
1178
  {
877
- tmpDeleteBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('trash', 12);
1179
+ delete this.pict.views[tmpKey];
1180
+ continue;
878
1181
  }
879
- else
1182
+ // Match any previously-injected layout-algorithm section by hash
1183
+ // suffix (manifests use unique algorithm-suffixed section names).
1184
+ if (tmpKey.indexOf('PictSectionForm-PFL') === 0)
880
1185
  {
881
- tmpDeleteBtn.textContent = '×';
1186
+ delete this.pict.views[tmpKey];
882
1187
  }
883
- tmpRow.appendChild(tmpDeleteBtn);
884
-
885
- // Click row → restore layout
886
- tmpRow.addEventListener('click', (pEvent) =>
887
- {
888
- // Don't restore if they clicked the delete button
889
- if (pEvent.target.closest('.pict-flow-popup-layout-delete'))
890
- {
891
- return;
892
- }
893
- this._FlowView._LayoutProvider.restoreLayout(tmpLayout.Hash);
894
- this._closePopup();
895
- });
896
-
897
- // Click delete → delete layout and refresh popup
898
- tmpDeleteBtn.addEventListener('click', (pEvent) =>
899
- {
900
- pEvent.stopPropagation();
901
- this._FlowView._LayoutProvider.deleteLayout(tmpLayout.Hash);
902
- // Refresh the popup content
903
- while (pContainer.firstChild)
904
- {
905
- pContainer.removeChild(pContainer.firstChild);
906
- }
907
- this._buildLayoutPopup(pContainer);
908
- });
909
-
910
- pContainer.appendChild(tmpRow);
911
1188
  }
1189
+ this._LayoutFormMetacontroller = null;
912
1190
  }
913
1191
 
914
- // ── Settings Popup ───────────────────────────────────────────────────
1192
+ /**
1193
+ * Look up which form-metacontroller service is registered on the host
1194
+ * Pict instance. pict-section-form has been published under both
1195
+ * 'PictFormMetacontroller' and (older) 'PictViewFormMetacontroller'
1196
+ * names; check both, in that order.
1197
+ * @returns {string|null}
1198
+ */
1199
+ _resolveMetacontrollerServiceType()
1200
+ {
1201
+ if (!this.fable || !this.fable.servicesMap) return null;
1202
+ if (this.fable.servicesMap.hasOwnProperty('PictFormMetacontroller')) return 'PictFormMetacontroller';
1203
+ if (this.fable.servicesMap.hasOwnProperty('PictViewFormMetacontroller')) return 'PictViewFormMetacontroller';
1204
+ return null;
1205
+ }
915
1206
 
916
1207
  /**
917
- * Build the Settings popup content (theme dropdown + noise slider).
1208
+ * Build the parameter form section using a pict-section-form
1209
+ * metacontroller. Creates a host div, binds the active layout
1210
+ * parameters to `pict.AppData.PictFlowLayoutEditor.Parameters`, and
1211
+ * injects the algorithm's `ParameterManifest`. Form-input changes
1212
+ * propagate back to `_FlowData.LayoutParameters` via a single
1213
+ * `change`/`input` listener on the popup container.
1214
+ *
918
1215
  * @param {HTMLElement} pContainer
1216
+ * @param {Object} pAlgoDescriptor
1217
+ * @param {{ Algorithm: string, Parameters: Object }} pCurrentSettings
1218
+ * @param {string} pMetacontrollerType - 'PictFormMetacontroller' or 'PictViewFormMetacontroller'
919
1219
  */
920
- _buildSettingsPopup(pContainer)
1220
+ _mountLayoutParameterMetacontroller(pContainer, pAlgoDescriptor, pCurrentSettings, pMetacontrollerType)
921
1221
  {
922
- if (!this._FlowView || !this._FlowView._ThemeProvider) return;
923
-
924
- let tmpThemeProvider = this._FlowView._ThemeProvider;
925
-
926
- // Theme selector section
927
- let tmpThemeSection = document.createElement('div');
928
- tmpThemeSection.className = 'pict-flow-popup-settings-section';
929
-
930
- let tmpThemeLabel = document.createElement('label');
931
- tmpThemeLabel.className = 'pict-flow-popup-settings-label';
932
- tmpThemeLabel.textContent = 'Theme';
933
- tmpThemeSection.appendChild(tmpThemeLabel);
1222
+ let tmpManifest = JSON.parse(JSON.stringify(pAlgoDescriptor.ParameterManifest));
1223
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
934
1224
 
935
- let tmpThemeSelect = document.createElement('select');
936
- tmpThemeSelect.className = 'pict-flow-popup-settings-select';
1225
+ // Section header + divider
1226
+ let tmpDivider = document.createElement('div');
1227
+ tmpDivider.className = 'pict-flow-popup-divider';
1228
+ pContainer.appendChild(tmpDivider);
937
1229
 
938
- let tmpThemeKeys = tmpThemeProvider.getThemeKeys();
939
- let tmpActiveKey = tmpThemeProvider.getActiveThemeKey();
1230
+ // Form host div — the metacontroller renders into here. Inline
1231
+ // onclick stops popup-close propagation; onchange/oninput drive the
1232
+ // _FlowData.LayoutParameters writeback (see _pushFormBack).
1233
+ let tmpHostID = `Flow-Toolbar-LayoutForm-${tmpFlowViewIdentifier}`;
1234
+ this._LayoutFormHostID = tmpHostID;
1235
+
1236
+ let tmpHostDiv = document.createElement('div');
1237
+ tmpHostDiv.id = tmpHostID;
1238
+ tmpHostDiv.className = 'pict-flow-popup-layout-form-host';
1239
+ tmpHostDiv.setAttribute('data-collapsed', this._LayoutFormExpanded ? 'false' : 'true');
1240
+ tmpHostDiv.setAttribute('onclick', 'event.stopPropagation()');
1241
+ tmpHostDiv.setAttribute('onchange',
1242
+ "_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView._pushLayoutFormBack('" + pCurrentSettings.Algorithm + "')");
1243
+ tmpHostDiv.setAttribute('oninput',
1244
+ "_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView._pushLayoutFormBack('" + pCurrentSettings.Algorithm + "')");
1245
+ pContainer.appendChild(tmpHostDiv);
1246
+
1247
+ // Bind the layout parameters as the data source for the form.
1248
+ // `injectManifestAndRender(manifest, _, UUID)` calls
1249
+ // `createDistinctManifest` which prepends the UUID to every descriptor
1250
+ // address (so re-injecting the same manifest doesn't collide). We use
1251
+ // the algorithm name as the UUID, so the form's expected address is
1252
+ // `AppData.<Algorithm>.PictFlowLayoutEditor.Parameters.<Key>` —
1253
+ // bind there, not at the unprefixed root.
1254
+ let tmpScope = pCurrentSettings.Algorithm;
1255
+ this.pict.AppData[tmpScope] = this.pict.AppData[tmpScope] || {};
1256
+ this.pict.AppData[tmpScope].PictFlowLayoutEditor = this.pict.AppData[tmpScope].PictFlowLayoutEditor || {};
1257
+ this.pict.AppData[tmpScope].PictFlowLayoutEditor.Parameters = JSON.parse(JSON.stringify(pCurrentSettings.Parameters || {}));
1258
+
1259
+ // Always recreate the metacontroller per mount. Reusing the same
1260
+ // metacontroller across algorithm switches accumulates section views
1261
+ // (each switch's manifest gets injected onto the existing roster) and
1262
+ // the metatemplate ends up wedging multiple sections into a single
1263
+ // shared destination — only the last one survives. Fresh instance per
1264
+ // mount keeps the DOM consistent; we also evict the prior
1265
+ // `PictSectionForm-*` views from `pict.views` so they GC cleanly.
1266
+ this._evictLayoutFormViews();
1267
+ try
1268
+ {
1269
+ this._LayoutFormMetacontroller = this.fable.instantiateServiceProviderWithoutRegistration(
1270
+ pMetacontrollerType,
1271
+ {
1272
+ ViewIdentifier: `Flow-Toolbar-LayoutForm-MC-${tmpFlowViewIdentifier}-${this.fable.getUUID()}`,
1273
+ DefaultDestinationAddress: `#${tmpHostID}`,
1274
+ AutoRender: false,
1275
+ AutoPopulateAfterRender: true,
1276
+ AutoSolveBeforeRender: false
1277
+ });
1278
+ }
1279
+ catch (pError)
1280
+ {
1281
+ this.log.warn(`Failed to instantiate ${pMetacontrollerType}: ${pError.message}`);
1282
+ this._LayoutFormMetacontroller = null;
1283
+ }
940
1284
 
941
- for (let i = 0; i < tmpThemeKeys.length; i++)
1285
+ if (!this._LayoutFormMetacontroller)
942
1286
  {
943
- let tmpOption = document.createElement('option');
944
- tmpOption.value = tmpThemeKeys[i];
1287
+ tmpHostDiv.innerHTML = '<em style="padding:8px;display:block;opacity:0.7;">pict-section-form not available; parameter form skipped.</em>';
1288
+ return;
1289
+ }
945
1290
 
946
- let tmpTheme = tmpThemeProvider._Themes[tmpThemeKeys[i]];
947
- tmpOption.textContent = tmpTheme.Label || tmpThemeKeys[i];
1291
+ // Establish the form-container div the metacontroller expects.
1292
+ let tmpFormContainerID = `Pict-${this._LayoutFormMetacontroller.UUID}-FormContainer`;
1293
+ tmpHostDiv.innerHTML = `<div id="${tmpFormContainerID}" class="pict-form pict-flow-popup-layout-form"></div>`;
948
1294
 
949
- if (tmpThemeKeys[i] === tmpActiveKey)
1295
+ try
1296
+ {
1297
+ // Use `injectManifest` + explicit per-section destination divs +
1298
+ // per-section render. Don't use `injectManifestAndRender` —
1299
+ // its metatemplate flow ends up wedging multi-section manifests
1300
+ // into a single shared destination (each section's render call
1301
+ // uses RenderMethod=replace, blowing the others away). The
1302
+ // explicit path gives each section its own destination div and
1303
+ // renders them independently.
1304
+ let tmpInjectFn = (typeof this._LayoutFormMetacontroller.injectManifest === 'function')
1305
+ ? this._LayoutFormMetacontroller.injectManifest.bind(this._LayoutFormMetacontroller)
1306
+ : null;
1307
+ if (!tmpInjectFn) throw new Error('Metacontroller exposes neither injectManifest nor injectManifestAndRender');
1308
+
1309
+ // Pass the algorithm name as the section-hash discriminator so
1310
+ // re-injecting the same algorithm gets unique view registrations.
1311
+ // (createDistinctManifest does the address-prefixing too — see
1312
+ // the per-algorithm AppData binding above.)
1313
+ let tmpDistinct = (typeof this._LayoutFormMetacontroller.createDistinctManifest === 'function')
1314
+ ? this._LayoutFormMetacontroller.createDistinctManifest(tmpManifest, pCurrentSettings.Algorithm)
1315
+ : tmpManifest;
1316
+
1317
+ let tmpViews = tmpInjectFn(tmpDistinct);
1318
+
1319
+ // Build a destination div per section view, in order, and render each.
1320
+ let tmpFormContainerEl = tmpHostDiv.querySelector(`#${tmpFormContainerID}`);
1321
+ if (tmpFormContainerEl)
950
1322
  {
951
- tmpOption.selected = true;
1323
+ let tmpInner = '';
1324
+ for (let i = 0; i < tmpViews.length; i++)
1325
+ {
1326
+ let tmpDest = tmpViews[i].options.DefaultDestinationAddress;
1327
+ if (tmpDest && tmpDest.charAt(0) === '#') tmpDest = tmpDest.substring(1);
1328
+ tmpInner += `<div id="${tmpDest}" class="pict-form-view"></div>`;
1329
+ }
1330
+ tmpFormContainerEl.innerHTML = tmpInner;
952
1331
  }
953
- tmpThemeSelect.appendChild(tmpOption);
1332
+ // Defer render() to the next microtask so the popup has been
1333
+ // appended to the DOM by `_openPopup` — pict-section-form
1334
+ // resolves destinations via `document.querySelector`, which
1335
+ // can't see detached subtrees. (Same workaround the
1336
+ // metacontroller's own `injectManifestAndRender` uses.)
1337
+ setTimeout(() =>
1338
+ {
1339
+ for (let i = 0; i < tmpViews.length; i++)
1340
+ {
1341
+ tmpViews[i].render();
1342
+ if (typeof tmpViews[i].marshalToView === 'function')
1343
+ {
1344
+ tmpViews[i].marshalToView();
1345
+ }
1346
+ }
1347
+ }, 0);
1348
+ }
1349
+ catch (pError)
1350
+ {
1351
+ this.log.warn(`PictViewFlowToolbar: layout-form injection failed: ${pError.message}`);
1352
+ tmpHostDiv.innerHTML = `<em style="padding:8px;display:block;opacity:0.7;">Form render error: ${pError.message}</em>`;
1353
+ return;
954
1354
  }
955
1355
 
956
- tmpThemeSelect.addEventListener('change', () =>
1356
+ // Form-change writeback runs through the host div's inline
1357
+ // onchange/oninput attributes (set above), which call
1358
+ // `_pushLayoutFormBack(algorithm)`.
1359
+ }
1360
+
1361
+ /**
1362
+ * Inline handler for layout-parameter form changes. Pushes Informary's
1363
+ * write into AppData back into `_FlowData.LayoutParameters` and
1364
+ * re-applies the layout (when non-Custom). De-bounces via microtask so
1365
+ * multi-key edits collapse to a single layout pass.
1366
+ *
1367
+ * @param {string} pAlgorithm
1368
+ */
1369
+ _pushLayoutFormBack(pAlgorithm)
1370
+ {
1371
+ if (this._LayoutFormPushScheduled) return;
1372
+ this._LayoutFormPushScheduled = true;
1373
+ Promise.resolve().then(() =>
957
1374
  {
958
- this._FlowView.setTheme(tmpThemeSelect.value);
959
- // Refresh the noise slider visibility
960
- this._refreshNoiseSlider(pContainer);
1375
+ this._LayoutFormPushScheduled = false;
1376
+ if (!this._FlowView) return;
1377
+ let tmpScopedRoot = this.pict.AppData[pAlgorithm] || {};
1378
+ let tmpEditorParams = (tmpScopedRoot.PictFlowLayoutEditor || {}).Parameters || {};
1379
+ let tmpMerged = Object.assign({}, this._FlowView.getLayoutAlgorithm().Parameters || {}, tmpEditorParams);
1380
+ this._FlowView.setLayoutAlgorithm(pAlgorithm, tmpMerged);
961
1381
  });
1382
+ }
962
1383
 
963
- // Prevent popup close on select interaction
964
- tmpThemeSelect.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1384
+ /**
1385
+ * Build a single parameter input row for the layout-algorithm popup.
1386
+ * Wires the input's change handler to update the flow's
1387
+ * `LayoutParameters` and re-apply the layout if non-Custom.
1388
+ *
1389
+ * @param {string} pKey
1390
+ * @param {Object} pSchema - { Type: 'number' | 'string' | 'boolean' | 'enum', Default, Options? }
1391
+ * @param {*} pCurrentValue
1392
+ * @returns {HTMLElement}
1393
+ */
1394
+ _buildLayoutParamInput(pKey, pSchema, pCurrentValue)
1395
+ {
1396
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1397
+ let tmpViewPath = "_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView";
1398
+ let tmpKeyEsc = pKey.replace(/'/g, "\\'");
1399
+ let tmpInput;
965
1400
 
966
- tmpThemeSection.appendChild(tmpThemeSelect);
967
- pContainer.appendChild(tmpThemeSection);
1401
+ if (pSchema.Type === 'boolean')
1402
+ {
1403
+ tmpInput = document.createElement('input');
1404
+ tmpInput.type = 'checkbox';
1405
+ tmpInput.checked = !!pCurrentValue;
1406
+ tmpInput.setAttribute('onchange',
1407
+ tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.checked)");
1408
+ }
1409
+ else if (pSchema.Type === 'enum' && Array.isArray(pSchema.Options))
1410
+ {
1411
+ tmpInput = document.createElement('select');
1412
+ tmpInput.className = 'pict-flow-popup-settings-select';
1413
+ for (let i = 0; i < pSchema.Options.length; i++)
1414
+ {
1415
+ let tmpOpt = document.createElement('option');
1416
+ tmpOpt.value = pSchema.Options[i];
1417
+ tmpOpt.textContent = pSchema.Options[i];
1418
+ if (pSchema.Options[i] === pCurrentValue) tmpOpt.selected = true;
1419
+ tmpInput.appendChild(tmpOpt);
1420
+ }
1421
+ tmpInput.setAttribute('onchange',
1422
+ tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.value)");
1423
+ }
1424
+ else if (pSchema.Type === 'number' || pSchema.Type === 'Number' || pSchema.Type === 'PreciseNumber')
1425
+ {
1426
+ tmpInput = document.createElement('input');
1427
+ tmpInput.type = 'number';
1428
+ tmpInput.className = 'pict-flow-popup-settings-input';
1429
+ let tmpIsPrecise = (pSchema.Type === 'PreciseNumber');
1430
+ if (typeof pSchema.Min === 'number') tmpInput.min = String(pSchema.Min);
1431
+ if (typeof pSchema.Max === 'number') tmpInput.max = String(pSchema.Max);
1432
+ tmpInput.step = tmpIsPrecise ? 'any' : '1';
1433
+ tmpInput.value = (pCurrentValue == null) ? '' : String(pCurrentValue);
1434
+ tmpInput.style.width = '90px';
1435
+ let tmpPreciseFlag = tmpIsPrecise ? 'true' : 'false';
1436
+ tmpInput.setAttribute('onchange',
1437
+ tmpViewPath + "._updateLayoutNumberParameter('" + tmpKeyEsc + "', this.value, " + tmpPreciseFlag + ")");
1438
+ }
1439
+ else
1440
+ {
1441
+ tmpInput = document.createElement('input');
1442
+ tmpInput.type = 'text';
1443
+ tmpInput.className = 'pict-flow-popup-settings-input';
1444
+ tmpInput.value = (pCurrentValue == null) ? '' : String(pCurrentValue);
1445
+ tmpInput.style.width = '90px';
1446
+ tmpInput.setAttribute('onchange',
1447
+ tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.value)");
1448
+ }
968
1449
 
969
- // Divider
970
- let tmpDivider = document.createElement('div');
971
- tmpDivider.className = 'pict-flow-popup-divider';
972
- pContainer.appendChild(tmpDivider);
1450
+ tmpInput.setAttribute('onclick', 'event.stopPropagation()');
1451
+ return tmpInput;
1452
+ }
973
1453
 
974
- // Noise level section
975
- let tmpNoiseSection = document.createElement('div');
976
- tmpNoiseSection.className = 'pict-flow-popup-settings-section pict-flow-popup-settings-noise';
977
- tmpNoiseSection.setAttribute('data-settings-type', 'noise');
1454
+ /**
1455
+ * Update a single layout parameter on the flow and re-apply the layout
1456
+ * if the configured algorithm is not 'Custom'.
1457
+ * @param {string} pKey
1458
+ * @param {*} pValue
1459
+ */
1460
+ _updateLayoutParameter(pKey, pValue)
1461
+ {
1462
+ if (!this._FlowView) return;
1463
+ let tmpSettings = this._FlowView.getLayoutAlgorithm();
1464
+ let tmpParams = Object.assign({}, tmpSettings.Parameters || {});
1465
+ tmpParams[pKey] = pValue;
1466
+ this._FlowView.setLayoutAlgorithm(tmpSettings.Algorithm, tmpParams);
1467
+ }
978
1468
 
979
- let tmpNoiseLabel = document.createElement('label');
980
- tmpNoiseLabel.className = 'pict-flow-popup-settings-label';
981
- tmpNoiseLabel.textContent = 'Noise';
982
- tmpNoiseSection.appendChild(tmpNoiseLabel);
1469
+ /**
1470
+ * Inline-handler helper for numeric parameter inputs. Preserves the
1471
+ * raw string when PreciseNumber so big.js / ExpressionParser can use
1472
+ * it without float round-trip drift.
1473
+ *
1474
+ * @param {string} pKey
1475
+ * @param {string} pRawValue
1476
+ * @param {boolean} pIsPrecise
1477
+ */
1478
+ _updateLayoutNumberParameter(pKey, pRawValue, pIsPrecise)
1479
+ {
1480
+ if (pRawValue === '') return;
1481
+ if (pIsPrecise)
1482
+ {
1483
+ this._updateLayoutParameter(pKey, pRawValue);
1484
+ return;
1485
+ }
1486
+ let tmpNum = parseFloat(pRawValue);
1487
+ if (isNaN(tmpNum)) return;
1488
+ this._updateLayoutParameter(pKey, tmpNum);
1489
+ }
983
1490
 
984
- let tmpNoiseRow = document.createElement('div');
985
- tmpNoiseRow.className = 'pict-flow-popup-settings-slider-row';
1491
+ // ── Settings Popup ───────────────────────────────────────────────────
986
1492
 
987
- let tmpNoiseSlider = document.createElement('input');
988
- tmpNoiseSlider.type = 'range';
989
- tmpNoiseSlider.className = 'pict-flow-popup-settings-slider';
990
- tmpNoiseSlider.min = '0';
991
- tmpNoiseSlider.max = '100';
992
- tmpNoiseSlider.value = String(Math.round(tmpThemeProvider.getNoiseLevel() * 100));
1493
+ /**
1494
+ * Build the Settings popup using a template with inline handlers.
1495
+ *
1496
+ * Theme options are iterated via `{~TS:~}` from AppData. Sliders and
1497
+ * selects fire inline handlers that update the FlowView's theme state.
1498
+ *
1499
+ * @param {HTMLElement} pContainer
1500
+ */
1501
+ _buildSettingsPopup(pContainer)
1502
+ {
1503
+ if (!this._FlowView || !this._FlowView._ThemeProvider) return;
993
1504
 
994
- let tmpNoiseValue = document.createElement('span');
995
- tmpNoiseValue.className = 'pict-flow-popup-settings-slider-value';
996
- tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
1505
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1506
+ let tmpThemeProvider = this._FlowView._ThemeProvider;
1507
+ let tmpThemeKeys = tmpThemeProvider.getThemeKeys();
1508
+ let tmpActiveKey = tmpThemeProvider.getActiveThemeKey();
997
1509
 
998
- tmpNoiseSlider.addEventListener('input', () =>
1510
+ let tmpThemeOptions = [];
1511
+ for (let i = 0; i < tmpThemeKeys.length; i++)
999
1512
  {
1000
- let tmpLevel = parseInt(tmpNoiseSlider.value, 10) / 100;
1001
- tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
1002
- this._FlowView.setNoiseLevel(tmpLevel);
1003
- });
1513
+ let tmpTheme = tmpThemeProvider._Themes[tmpThemeKeys[i]];
1514
+ tmpThemeOptions.push(
1515
+ {
1516
+ Value: tmpThemeKeys[i],
1517
+ Label: tmpTheme.Label || tmpThemeKeys[i],
1518
+ SelectedAttr: (tmpThemeKeys[i] === tmpActiveKey) ? ' selected="selected"' : ''
1519
+ });
1520
+ }
1004
1521
 
1005
- // Prevent popup close on slider interaction
1006
- tmpNoiseSlider.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
1007
- tmpNoiseSlider.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
1522
+ let tmpThemeOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpThemeOptions });
1523
+
1524
+ let tmpNoiseLevel = Math.round(tmpThemeProvider.getNoiseLevel() * 100);
1525
+ let tmpActiveTheme = tmpThemeProvider.getActiveTheme();
1526
+ let tmpNoiseEnabled = !!(tmpActiveTheme && tmpActiveTheme.NoiseConfig && tmpActiveTheme.NoiseConfig.Enabled);
1527
+ let tmpNoiseDisplay = tmpNoiseEnabled ? '' : 'display:none;';
1528
+
1529
+ this.pict.ContentAssignment.assignContent(pContainer,
1530
+ '<div class="pict-flow-popup-settings-section">'
1531
+ + '<label class="pict-flow-popup-settings-label">Theme</label>'
1532
+ + '<select class="pict-flow-popup-settings-select"'
1533
+ + ' onclick="event.stopPropagation()"'
1534
+ + ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleThemeSelectChange(this.value)">'
1535
+ + tmpThemeOptionsHTML
1536
+ + '</select>'
1537
+ + '</div>'
1538
+ + '<div class="pict-flow-popup-divider"></div>'
1539
+ + '<div class="pict-flow-popup-settings-section pict-flow-popup-settings-noise" data-settings-type="noise" style="' + tmpNoiseDisplay + '">'
1540
+ + '<label class="pict-flow-popup-settings-label">Noise</label>'
1541
+ + '<div class="pict-flow-popup-settings-slider-row">'
1542
+ + '<input type="range" class="pict-flow-popup-settings-slider" min="0" max="100" value="' + tmpNoiseLevel + '"'
1543
+ + ' onclick="event.stopPropagation()"'
1544
+ + ' onpointerdown="event.stopPropagation()"'
1545
+ + ' oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleNoiseSliderInput(this)" />'
1546
+ + '<span class="pict-flow-popup-settings-slider-value">' + tmpNoiseLevel + '%</span>'
1547
+ + '</div>'
1548
+ + '</div>');
1549
+ }
1008
1550
 
1009
- tmpNoiseRow.appendChild(tmpNoiseSlider);
1010
- tmpNoiseRow.appendChild(tmpNoiseValue);
1011
- tmpNoiseSection.appendChild(tmpNoiseRow);
1012
- pContainer.appendChild(tmpNoiseSection);
1551
+ /**
1552
+ * Inline handler — apply selected flow theme and refresh the noise
1553
+ * slider visibility (some themes don't expose noise).
1554
+ */
1555
+ _handleThemeSelectChange(pThemeKey)
1556
+ {
1557
+ if (!this._FlowView) return;
1558
+ this._FlowView.setTheme(pThemeKey);
1013
1559
 
1014
- // Show/hide noise slider based on active theme
1015
- this._refreshNoiseSlider(pContainer);
1560
+ let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
1561
+ let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
1562
+ if (tmpPopupEl) this._refreshNoiseSlider(tmpPopupEl);
1563
+ }
1564
+
1565
+ /**
1566
+ * Inline handler — push slider position into the noise level and
1567
+ * update the percentage label adjacent to it.
1568
+ */
1569
+ _handleNoiseSliderInput(pSlider)
1570
+ {
1571
+ let tmpRaw = parseInt(pSlider.value, 10);
1572
+ if (isNaN(tmpRaw)) return;
1573
+ let tmpLevel = tmpRaw / 100;
1574
+ let tmpValueLabel = pSlider.parentNode ? pSlider.parentNode.querySelector('.pict-flow-popup-settings-slider-value') : null;
1575
+ if (tmpValueLabel) tmpValueLabel.textContent = tmpRaw + '%';
1576
+ if (this._FlowView) this._FlowView.setNoiseLevel(tmpLevel);
1016
1577
  }
1017
1578
 
1018
1579
  /**
@@ -1202,6 +1763,17 @@ class PictViewFlowToolbar extends libPictView
1202
1763
  break;
1203
1764
 
1204
1765
  case 'auto-layout':
1766
+ // Legacy alias kept for backward compatibility with any
1767
+ // caller still firing 'auto-layout'. The toolbar's "Auto"
1768
+ // button now uses 'apply-current-layout' (below).
1769
+ this._FlowView.autoLayout();
1770
+ break;
1771
+
1772
+ case 'apply-current-layout':
1773
+ // Respects whichever algorithm is configured in the
1774
+ // Algorithm popup. Falls back to Layered when the
1775
+ // configured value is 'Custom' or unset (autoLayout's
1776
+ // existing behavior — "do something useful").
1205
1777
  this._FlowView.autoLayout();
1206
1778
  break;
1207
1779
 
@@ -1213,6 +1785,14 @@ class PictViewFlowToolbar extends libPictView
1213
1785
  this._openPopup('layout');
1214
1786
  break;
1215
1787
 
1788
+ case 'layout-algorithm-popup':
1789
+ this._openPopup('layout-algorithm');
1790
+ break;
1791
+
1792
+ case 'apply-layout':
1793
+ this._FlowView.applyCurrentLayout();
1794
+ break;
1795
+
1216
1796
  case 'settings-popup':
1217
1797
  this._openPopup('settings');
1218
1798
  break;