Skip to content

Commit e81243a

Browse files
committed
feat: replace action groups with UI groups
1 parent 674d5d4 commit e81243a

89 files changed

Lines changed: 355 additions & 173 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

+labkit/+ui/+app/create.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
function ui = create(spec, varargin)
2-
%CREATE Build a LabKit UI 3.0 workbench from declarative specs.
2+
%CREATE Build a LabKit UI 4.0 workbench from declarative specs.
33
%
44
% App-facing contract:
55
% ui = labkit.ui.app.create(spec, "debug", debugContext)

+labkit/+ui/+app/private/applyCommonValueProps.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% Private UI app helper. Expected caller: UI 3.0 control builders. Inputs are
1+
% Private UI app helper. Expected caller: UI 4.0 control builders. Inputs are
22
% a MATLAB UI handle and validated spec props. Output is none. Side effects
33
% assign common value, limit, item, and display-format properties when the
44
% target handle supports them.

+labkit/+ui/+app/private/applyTextFit.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% Private UI app helper. Expected caller: UI 3.0 control builders. Inputs are
1+
% Private UI app helper. Expected caller: UI 4.0 control builders. Inputs are
22
% a MATLAB UI text-bearing handle and optional fitting limits. Output is none.
33
% Side effects: enables wrapping when supported, reduces font size for long
44
% text, and installs a tooltip with the full text when supported.

+labkit/+ui/+app/private/buildControl.m

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
parentGrid, row);
1414
case 'action'
1515
ui = buildAction(ui, controlSpec, parentGrid, row, [1 2]);
16-
case 'actionGroup'
17-
ui = buildActionGroup(ui, controlSpec, parentGrid, row);
16+
case 'group'
17+
ui = buildGroup(ui, controlSpec, parentGrid, row, debug);
1818
case 'filePanel'
1919
ui = buildFilePanel(ui, controlSpec, parentGrid, row);
2020
case 'resultTable'
@@ -29,7 +29,7 @@
2929
ui = buildToolPanelControl(ui, controlSpec, parentGrid, row);
3030
otherwise
3131
error('labkit:ui:app:UnsupportedControl', ...
32-
'Unsupported UI 3.0 control kind "%s".', controlSpec.kind);
32+
'Unsupported UI 4.0 control kind "%s".', controlSpec.kind);
3333
end
3434
end
3535

@@ -100,7 +100,7 @@
100100
applyTextFit(control);
101101
otherwise
102102
error('labkit:ui:app:UnsupportedFieldKind', ...
103-
'Unsupported UI 3.0 field kind "%s".', kind);
103+
'Unsupported UI 4.0 field kind "%s".', kind);
104104
end
105105
applyCommonValueProps(control, props);
106106
applySliderTicks(control, props);
@@ -173,10 +173,26 @@ function setRangeValue(newValue)
173173
end
174174
end
175175

176-
function ui = buildActionGroup(ui, groupSpec, parentGrid, row)
176+
function ui = buildGroup(ui, groupSpec, parentGrid, row, debug)
177+
if usesActionLayout(groupSpec)
178+
ui = buildActionLayout(ui, groupSpec, parentGrid, row);
179+
return;
180+
end
181+
ui = buildFormGroup(ui, groupSpec, parentGrid, row, debug);
182+
end
183+
184+
function tf = usesActionLayout(groupSpec)
185+
layout = lower(char(string(optionValue(groupSpec.props, 'layout', 'auto'))));
186+
childKinds = string(cellfun(@(child) child.kind, groupSpec.children, ...
187+
'UniformOutput', false));
188+
tf = strcmp(layout, 'actions') || ...
189+
(strcmp(layout, 'auto') && all(childKinds == "action"));
190+
end
191+
192+
function ui = buildActionLayout(ui, groupSpec, parentGrid, row)
177193
actions = groupSpec.children;
178194
count = max(1, numel(actions));
179-
maxColumns = actionGroupMaxColumns(groupSpec);
195+
maxColumns = actionLayoutMaxColumns(groupSpec);
180196
columnCount = min(count, maxColumns);
181197
rowCount = max(1, ceil(count / columnCount));
182198
grid = uigridlayout(parentGrid, [rowCount columnCount]);
@@ -187,7 +203,7 @@ function setRangeValue(newValue)
187203
grid.ColumnWidth = repmat({'1x'}, 1, columnCount);
188204
grid.Layout.Row = row;
189205
grid.Layout.Column = [1 2];
190-
adapter = baseAdapter(groupSpec, 'actionGroup');
206+
adapter = baseAdapter(groupSpec, 'group');
191207
adapter.grid = grid;
192208
adapter.actions = struct();
193209
ui.controls.(groupSpec.id) = adapter;
@@ -204,7 +220,42 @@ function setRangeValue(newValue)
204220
end
205221
end
206222

207-
function maxColumns = actionGroupMaxColumns(groupSpec)
223+
function ui = buildFormGroup(ui, groupSpec, parentGrid, row, debug)
224+
childCount = max(1, numel(groupSpec.children));
225+
titleText = char(string(optionValue(groupSpec.props, 'title', '')));
226+
if strlength(string(titleText)) > 0
227+
host = uipanel(parentGrid, 'Title', titleText);
228+
else
229+
host = uipanel(parentGrid, 'BorderType', 'none');
230+
end
231+
host.Layout.Row = row;
232+
host.Layout.Column = [1 2];
233+
234+
grid = uigridlayout(host, [childCount 2]);
235+
grid.RowHeight = groupRowHeights(groupSpec.children);
236+
grid.ColumnWidth = {120, '1x'};
237+
grid.RowSpacing = 6;
238+
grid.ColumnSpacing = 8;
239+
grid.Padding = [0 0 0 0];
240+
241+
adapter = baseAdapter(groupSpec, 'group');
242+
adapter.panel = host;
243+
adapter.grid = grid;
244+
ui.controls.(groupSpec.id) = adapter;
245+
for iChild = 1:numel(groupSpec.children)
246+
ui = buildControl(ui, groupSpec.children{iChild}, grid, iChild, debug);
247+
end
248+
end
249+
250+
function rowHeight = groupRowHeights(children)
251+
count = max(1, numel(children));
252+
rowHeight = repmat({'fit'}, 1, count);
253+
for k = 1:numel(children)
254+
rowHeight{k} = specRowHeight(children{k}, 'fit');
255+
end
256+
end
257+
258+
function maxColumns = actionLayoutMaxColumns(groupSpec)
208259
maxColumns = 2;
209260
labels = actionLabels(groupSpec.children);
210261
if any(strlength(labels) > 28)

+labkit/+ui/+app/private/createTabbedWorkbenchShell.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
% figPosition - uifigure Position vector.
1414
% leftWidth - initial fixed width of the left control panel.
1515
% labels - struct with controlsPanel and rightPanel text.
16-
% tabSpecs - internal tab specs derived from UI 3.0 app specs.
16+
% tabSpecs - internal tab specs derived from UI 4.0 app specs.
1717
% rightGridSize - right-side uigridlayout size.
1818
% rightRowHeight - right-side grid RowHeight cell array.
1919
% rightRowSpacing - right-side grid RowSpacing scalar.
@@ -24,7 +24,7 @@
2424
%
2525
% Notes:
2626
% Logical tab rows are expanded with physical resize-handle rows here.
27-
% App code should use UI 3.0 specs rather than depending on physical row
27+
% App code should use UI 4.0 specs rather than depending on physical row
2828
% indices.
2929

3030
if nargin < 9

+labkit/+ui/+app/private/runSemanticAppCallback.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% Private UI app helper. Expected caller: UI 3.0 semantic callback wrappers.
1+
% Private UI app helper. Expected caller: UI 4.0 semantic callback wrappers.
22
% Inputs are the current UI registry, control adapter, event payload, app
33
% callback, and control id. Side effects: runs the callback in app busy state.
44
function runSemanticAppCallback(ui, control, event, appCallback, id)

+labkit/+ui/+app/private/semanticEvent.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% Private UI app helper. Expected callers: UI 3.0 callback wrappers under
1+
% Private UI app helper. Expected callers: UI 4.0 callback wrappers under
22
% +labkit/+ui/+app/private. Inputs are a semantic control adapter, the
33
% originating MATLAB UI source/raw event, and a semantic source label. Output is
44
% the normalized event payload seen by app callbacks.

+labkit/+ui/+app/private/specRowHeight.m

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
% Private UI app layout helper. Expected caller: buildShellFromSpec and
2-
% buildSection. Inputs are one validated UI 3.0 spec and an optional default
2+
% buildSection. Inputs are one validated UI 4.0 spec and an optional default
33
% row height. Output is a MATLAB uigridlayout RowHeight value.
44
function value = specRowHeight(spec, defaultValue)
55
if nargin < 2
@@ -25,8 +25,8 @@
2525
value = actionHeight(spec);
2626
case 'resultTable'
2727
value = tablePanelHeight();
28-
case 'actionGroup'
29-
value = actionGroupHeight(spec);
28+
case 'group'
29+
value = groupHeight(spec);
3030
otherwise
3131
value = normalizeHeight(defaultValue);
3232
end
@@ -72,20 +72,54 @@
7272
value = max(185, 24 * max(1, double(rows)) + 58);
7373
end
7474

75-
function value = actionGroupHeight(groupSpec)
75+
function value = groupHeight(groupSpec)
76+
if ~usesActionLayout(groupSpec)
77+
value = formGroupHeight(groupSpec);
78+
return;
79+
end
80+
value = actionLayoutHeight(groupSpec);
81+
end
82+
83+
function tf = usesActionLayout(groupSpec)
84+
layout = lower(char(string(optionValue(groupSpec.props, 'layout', 'auto'))));
85+
childKinds = string(cellfun(@(child) child.kind, groupSpec.children, ...
86+
'UniformOutput', false));
87+
tf = strcmp(layout, 'actions') || ...
88+
(strcmp(layout, 'auto') && all(childKinds == "action"));
89+
end
90+
91+
function value = actionLayoutHeight(groupSpec)
7692
count = numel(groupSpec.children);
7793
if count == 0
7894
value = defaultControlHeight();
7995
return;
8096
end
81-
maxColumns = actionGroupMaxColumns(groupSpec);
97+
maxColumns = actionLayoutMaxColumns(groupSpec);
8298
columnCount = min(count, maxColumns);
8399
rowCount = max(1, ceil(count / columnCount));
84100
rowHeight = max(defaultControlHeight(), max(actionHeights(groupSpec.children)));
85101
value = rowCount * rowHeight + ...
86102
max(0, rowCount - 1) * 6;
87103
end
88104

105+
function value = formGroupHeight(groupSpec)
106+
if isempty(groupSpec.children)
107+
value = defaultControlHeight();
108+
return;
109+
end
110+
rowHeights = zeros(1, numel(groupSpec.children));
111+
for k = 1:numel(groupSpec.children)
112+
childHeight = specRowHeight(groupSpec.children{k}, 'fit');
113+
rowHeights(k) = numericRowHeight(childHeight, defaultControlHeight());
114+
end
115+
titleAllowance = 0;
116+
if strlength(string(optionValue(groupSpec.props, 'title', ''))) > 0
117+
titleAllowance = 24;
118+
end
119+
value = sum(rowHeights) + 6 * max(0, numel(rowHeights) - 1) + ...
120+
titleAllowance;
121+
end
122+
89123
function value = fieldHeight(fieldSpec)
90124
props = fieldSpec.props;
91125
kind = lower(char(string(optionValue(props, 'kind', 'text'))));
@@ -119,7 +153,7 @@
119153
value = max(defaultControlHeight(), 20 * lineCount + 6);
120154
end
121155

122-
function maxColumns = actionGroupMaxColumns(groupSpec)
156+
function maxColumns = actionLayoutMaxColumns(groupSpec)
123157
maxColumns = 2;
124158
labels = actionLabels(groupSpec.children);
125159
if any(strlength(labels) > 28)

+labkit/+ui/+app/private/validateAppSpec.m

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function validateAppSpec(spec)
2020
duplicate = firstDuplicate(ids);
2121
if strlength(duplicate) > 0
2222
error('labkit:ui:app:DuplicateId', ...
23-
'Duplicate UI 3.0 spec id "%s".', char(duplicate));
23+
'Duplicate UI 4.0 spec id "%s".', char(duplicate));
2424
end
2525
validateTreeShape(spec);
2626
end
@@ -69,10 +69,13 @@ function validateTreeShape(spec)
6969
case 'section'
7070
validateNonEmptySection(spec);
7171
validateChildKinds(spec, {'field', 'rangeField', 'panner', 'action', ...
72-
'actionGroup', 'filePanel', 'resultTable', 'statusPanel', ...
72+
'group', 'filePanel', 'resultTable', 'statusPanel', ...
7373
'usagePanel', 'logPanel', 'toolPanel'});
74-
case 'actionGroup'
75-
validateChildKinds(spec, {'action'});
74+
case 'group'
75+
validateNonEmptyGroup(spec);
76+
validateGroupLayout(spec);
77+
validateChildKinds(spec, {'field', 'rangeField', 'panner', ...
78+
'action', 'group'});
7679
otherwise
7780
validateChildKinds(spec, {});
7881
end
@@ -87,6 +90,36 @@ function validateNonEmptySection(spec)
8790
end
8891
end
8992

93+
function validateNonEmptyGroup(spec)
94+
if isempty(spec.children)
95+
error('labkit:ui:app:EmptyGroup', ...
96+
'Spec "%s" declares an empty group. Add semantic child controls.', ...
97+
spec.id);
98+
end
99+
end
100+
101+
function validateGroupLayout(spec)
102+
layout = string(optionValue(spec.props, 'layout', 'auto'));
103+
allowed = ["auto", "actions", "form", "inline", "grid"];
104+
if ~isscalar(layout) || ~any(layout == allowed)
105+
error('labkit:ui:app:InvalidGroupLayout', ...
106+
'Spec "%s" uses unsupported group layout "%s".', ...
107+
spec.id, char(layout));
108+
end
109+
if layout == "actions" && ~allGroupChildrenAre(spec, 'action')
110+
error('labkit:ui:app:InvalidGroupLayout', ...
111+
'Spec "%s" uses action layout but contains non-action children.', ...
112+
spec.id);
113+
end
114+
end
115+
116+
function tf = allGroupChildrenAre(spec, kind)
117+
tf = true;
118+
for k = 1:numel(spec.children)
119+
tf = tf && strcmp(spec.children{k}.kind, kind);
120+
end
121+
end
122+
90123
function validateNoAppLayoutProps(spec)
91124
layoutProps = {'height', 'minRows', 'minHeight', 'maxColumns', ...
92125
'rowSpacing', 'columnSpacing', 'padding', 'chrome', ...
@@ -127,6 +160,13 @@ function assertCommonSpec(spec)
127160
~iscell(spec.children) || ...
128161
~(isempty(spec.children) || isrow(spec.children))
129162
error('labkit:ui:app:InvalidSpec', ...
130-
'UI 3.0 specs must be scalar structs with cell row children.');
163+
'UI 4.0 specs must be scalar structs with cell row children.');
164+
end
165+
end
166+
167+
function value = optionValue(opts, name, defaultValue)
168+
value = defaultValue;
169+
if isstruct(opts) && isfield(opts, name)
170+
value = opts.(name);
131171
end
132172
end

+labkit/+ui/+spec/actionGroup.m

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)