diff --git a/+labkit/+ui/+app/createShell.m b/+labkit/+ui/+app/createShell.m index 2d0fc483..457d55d9 100644 --- a/+labkit/+ui/+app/createShell.m +++ b/+labkit/+ui/+app/createShell.m @@ -6,7 +6,7 @@ % 'title', "Example", ... % 'position', [90 70 1200 800], ... % 'leftWidth', 360, ... -% 'options', struct('rightKind', 'dualPlot'))); +% 'options', struct('rightGridSize', [1 1]))); % % Inputs: % spec - scalar struct with fields: @@ -14,12 +14,10 @@ % position - MATLAB figure position [x y width height]. % leftWidth - initial left controls width in pixels. % options - optional shell options: -% rightKind - "custom" or "dualPlot", default "custom". % rightGridSize - custom right grid size, default [1 1]. % rightRowHeight - custom right grid rows, default {'1x'}. -% rightRowSpacing - scalar spacing, default 8 or 10 for dualPlot. -% showPlotControls - dualPlot only, default true. -% controlsTitle, rightTitle, topPlotTitle, bottomPlotTitle - labels. +% rightRowSpacing - scalar right-grid spacing, default 8. +% rightTitle - right panel label. % tabs - labkit.ui.app.tab struct array. % % Output: @@ -42,38 +40,19 @@ end opts = optionValue(spec, 'options', struct()); - if isfield(spec, 'shellOptions') - opts = spec.shellOptions; - end - rightKind = char(string(optionValue(opts, 'rightKind', 'custom'))); rightGridSize = optionValue(opts, 'rightGridSize', [1 1]); rightRowHeight = optionValue(opts, 'rightRowHeight', {'1x'}); rightRowSpacing = optionValue(opts, 'rightRowSpacing', 8); - if strcmp(rightKind, 'dualPlot') - showPlotControls = optionValue(opts, 'showPlotControls', true); - if showPlotControls - rightGridSize = [4 1]; - rightRowHeight = {'fit', '1x', 'fit', '1x'}; - else - rightGridSize = [2 1]; - rightRowHeight = {'1x', '1x'}; - end - rightRowSpacing = optionValue(opts, 'rightRowSpacing', 10); - end shellLabels = struct( ... - 'controlsPanel', optionValue(opts, 'controlsTitle', 'Controls'), ... + 'controlsPanel', 'Controls', ... 'rightPanel', optionValue(opts, 'rightTitle', 'Plots')); tabSpecs = optionValue(opts, 'tabs', standardTabs()); ui = createTabbedWorkbenchShell( ... spec.title, spec.position, spec.leftWidth, shellLabels, tabSpecs, ... rightGridSize, rightRowHeight, rightRowSpacing); - - if strcmp(rightKind, 'dualPlot') - ui = addDualPlotRegion(ui, opts); - end end function tabs = standardTabs() @@ -87,44 +66,6 @@ labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})]; end -function ui = addDualPlotRegion(ui, opts) - topTitle = optionValue(opts, 'topPlotTitle', 'Top Plot'); - bottomTitle = optionValue(opts, 'bottomPlotTitle', 'Bottom Plot'); - showPlotControls = optionValue(opts, 'showPlotControls', true); - - if showPlotControls - ui.topControlsPanel = uipanel(ui.rightGrid, 'Title', topTitle); - ui.topControlsPanel.Layout.Row = 1; - - ui.topAxes = uiaxes(ui.rightGrid); - ui.topAxes.Layout.Row = 2; - title(ui.topAxes, topTitle); - labkit.ui.view.draw(ui.topAxes, 'popout'); - disableAxesInteractivity(ui.topAxes); - - ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); - ui.bottomControlsPanel.Layout.Row = 3; - - ui.bottomAxes = uiaxes(ui.rightGrid); - ui.bottomAxes.Layout.Row = 4; - else - ui.topControlsPanel = []; - ui.bottomControlsPanel = []; - - ui.topAxes = uiaxes(ui.rightGrid); - ui.topAxes.Layout.Row = 1; - title(ui.topAxes, topTitle); - labkit.ui.view.draw(ui.topAxes, 'popout'); - disableAxesInteractivity(ui.topAxes); - - ui.bottomAxes = uiaxes(ui.rightGrid); - ui.bottomAxes.Layout.Row = 2; - end - title(ui.bottomAxes, bottomTitle); - labkit.ui.view.draw(ui.bottomAxes, 'popout'); - disableAxesInteractivity(ui.bottomAxes); -end - function value = optionValue(opts, name, defaultValue) value = defaultValue; if isstruct(opts) && isfield(opts, name) diff --git a/+labkit/+ui/+app/private/disableAxesInteractivity.m b/+labkit/+ui/+app/private/disableAxesInteractivity.m deleted file mode 100644 index 7ec2e886..00000000 --- a/+labkit/+ui/+app/private/disableAxesInteractivity.m +++ /dev/null @@ -1,31 +0,0 @@ -% Private UI app helper. Expected caller: labkit.ui.app shell construction -% code. Inputs and outputs are internal uifigure, grid, tab, or resize handle -% values. Side effects are limited to UI object creation or callback wiring on -% supplied parents; assumes the caller owns component lifecycle. -function disableAxesInteractivity(ax) -%DISABLEAXESINTERACTIVITY Disable optional axes interactions when available. -% -% Called by: -% UI axes/image helpers when an app wants static preview axes. -% -% Inputs: -% ax - MATLAB axes or UIAxes handle. -% -% Side effects: -% Best-effort removal of default interactivity, Interactions entries, and -% toolbar visibility. Unsupported properties are ignored for MATLAB-version -% compatibility. - - try - disableDefaultInteractivity(ax); - catch - end - try - ax.Interactions = []; - catch - end - try - ax.Toolbar.Visible = 'off'; - catch - end -end diff --git a/+labkit/+ui/+view/draw.m b/+labkit/+ui/+view/draw.m index e733bdef..91149be0 100644 --- a/+labkit/+ui/+view/draw.m +++ b/+labkit/+ui/+view/draw.m @@ -4,18 +4,17 @@ % App-facing contract: % labkit.ui.view.draw(ax, "reset", titleText, resetScaleAndTicks) % hImage = labkit.ui.view.draw(ax, "image", imageData, titleText, opts) -% info = labkit.ui.view.draw(ax, "xy", x, y, labels, opts) % labkit.ui.view.draw(ax, "clear") % labkit.ui.view.draw(ax, "popout") % % Inputs: % ax - target axes. -% action - "reset", "image", "xy", "clear", or "popout". +% action - "reset", "image", "clear", or "popout". % varargin - action-specific payload described above. % % Outputs: -% image returns the image graphics object. xy returns a status struct. -% reset, clear, and popout mutate axes in place and return [] when captured. +% image returns the image graphics object. reset, clear, and popout mutate +% axes in place and return [] when captured. switch normalizeAction(action) case 'reset' @@ -28,12 +27,6 @@ titleText = positional(varargin, 2, ''); opts = positional(varargin, 3, struct()); out = showImage(ax, imageData, titleText, opts); - case 'xy' - x = positional(varargin, 1, []); - y = positional(varargin, 2, []); - labels = positional(varargin, 3, struct()); - opts = positional(varargin, 4, struct()); - out = plotXY(ax, x, y, labels, opts); case 'clear' clearAxes(ax); out = []; @@ -52,18 +45,6 @@ function action = normalizeAction(action) action = lower(regexprep(char(string(action)), '[^a-zA-Z0-9]', '')); - switch action - case {'plot', 'plotxy'} - action = 'xy'; - case {'imageaxes', 'showimage'} - action = 'image'; - case {'resetaxes', 'hardreset'} - action = 'reset'; - case {'clearaxes'} - action = 'clear'; - case {'enablepopout', 'axespopout'} - action = 'popout'; - end end function value = positional(args, index, defaultValue) diff --git a/+labkit/+ui/+view/form.m b/+labkit/+ui/+view/form.m index 898efda8..4b825af8 100644 --- a/+labkit/+ui/+view/form.m +++ b/+labkit/+ui/+view/form.m @@ -1,4 +1,4 @@ -function varargout = form(parent, spec, varargin) +function varargout = form(parent, spec) %FORM Create LabKit form controls from a unified control spec. % % Usage: @@ -6,8 +6,6 @@ % 'kind', 'spinner', 'label', 'Samples:', ... % 'value', 10, 'limits', [1 Inf], 'step', 1, ... % 'callback', @onChanged)); -% [lbl, spinner] = labkit.ui.view.form(parent, 'spinner', 'Samples:', ... -% 'Value', 10, 'Limits', [1 Inf], 'ValueChangedFcn', @onChanged); % % ui = labkit.ui.view.form(parent, struct( ... % 'title', 'Settings', 'row', 2, 'layout', [2 2], ... @@ -15,19 +13,19 @@ % % Inputs: % parent - parent grid or shell tab grid. -% spec - scalar struct or control kind. Single-control specs use -% kind/label fields. Section specs use title, row, layout, and -% controls fields. String kind calls are normalized into a control -% spec and are the preferred app-facing replacement for one-off -% labeled control helper calls. +% spec - scalar struct. Single-control specs use kind/label fields. Section +% specs use title, row, layout, and controls fields. % % Control fields: % id - optional valid field name for returned ui.controls.(id). % kind - "spinner", "dropdown", "edit", "readonly", "info", "button", % or "checkbox". % label - label text for labeled controls. -% value, items, limits, step, enabled, callback - optional common values. -% args - optional cell array of raw MATLAB name/value pairs. +% style - edit-field style, default "text". +% value, items, limits, step, valueDisplayFormat, enabled, callback - +% optional common values. +% text - button or checkbox label text. +% row, column - optional layout location. % % Outputs: % Single-control call returns [labelHandle, controlHandle] for labeled @@ -40,16 +38,12 @@ if nargin < 2 error('labkit:ui:view:InvalidFormSpec', ... - 'form requires a scalar struct spec or control kind.'); - end - - if ~isstruct(spec) - spec = shorthandSpec(spec, varargin); + 'form requires a scalar struct spec.'); end if ~isstruct(spec) || ~isscalar(spec) error('labkit:ui:view:InvalidFormSpec', ... - 'form requires a scalar struct spec or control kind.'); + 'form requires a scalar struct spec.'); end if isfield(spec, 'controls') @@ -68,36 +62,6 @@ end end -function spec = shorthandSpec(kind, args) - kind = lower(char(string(kind))); - switch kind - case {'spinner', 'dropdown'} - if isempty(args) - error('labkit:ui:view:InvalidFormSpec', ... - '%s controls require label text.', kind); - end - spec = struct('kind', kind, 'label', args{1}, 'args', {args(2:end)}); - case 'edit' - if numel(args) < 2 - error('labkit:ui:view:InvalidFormSpec', ... - 'edit controls require label text and edit style.'); - end - spec = struct('kind', 'edit', 'label', args{1}, ... - 'style', args{2}, 'args', {args(3:end)}); - case 'readonly' - spec = struct('kind', 'readonly', 'args', {args}); - case 'info' - if numel(args) < 2 - error('labkit:ui:view:InvalidFormSpec', ... - 'info rows require row and label text.'); - end - spec = struct('kind', 'info', 'row', args{1}, 'label', args{2}); - otherwise - error('labkit:ui:view:UnknownControlKind', ... - 'Unsupported form control kind "%s".', kind); - end -end - function ui = createFormSection(parent, spec) layout = optionValue(spec, 'layout', [numel(spec.controls) 2]); ui = labkit.ui.view.section(parent, ... @@ -161,8 +125,7 @@ function setValue(id, value, reason) function [lbl, ctrl] = createOne(parent, spec) kind = lower(char(string(optionValue(spec, 'kind', 'edit')))); labelText = char(string(optionValue(spec, 'label', ''))); - args = optionValue(spec, 'args', {}); - args = [commonArgs(spec), args]; + args = commonArgs(spec); switch kind case 'spinner' @@ -223,6 +186,9 @@ function setValue(id, value, reason) if isfield(spec, 'step') args = [args, {'Step', spec.step}]; end + if isfield(spec, 'valueDisplayFormat') + args = [args, {'ValueDisplayFormat', spec.valueDisplayFormat}]; + end if isfield(spec, 'enabled') args = [args, {'Enable', onOff(spec.enabled)}]; end diff --git a/+labkit/+ui/+view/panel.m b/+labkit/+ui/+view/panel.m index aa542124..fbc1b1cd 100644 --- a/+labkit/+ui/+view/panel.m +++ b/+labkit/+ui/+view/panel.m @@ -6,10 +6,8 @@ % ui = labkit.ui.view.panel(parent, spec) % % Inputs: -% parent - parent container for the component group. For top/bottom plot -% controls this is the top controls panel. -% kind - component kind: "files", "log", "text", "table", -% "plotOptions", or "topBottomPlotControls". +% parent - parent container for the component group. +% kind - component kind: "files", "log", "text", or "table". % spec - optional struct alternative with kind plus fields matching the % positional arguments described below. % @@ -18,9 +16,6 @@ % panel(parent, "log", row, initialValue) % panel(parent, "text", title, row, lines, opts) % panel(parent, "table", title, row, columnNames, initialData) -% panel(parent, "plotOptions", rowCount, row) -% panel(topPanel, "topBottomPlotControls", bottomPanel, xItems, yItems, -% topDefaults, bottomDefaults, valueChangedFcn) % % Output: % ui - struct of MATLAB component handles owned by the created component. @@ -52,19 +47,6 @@ columnNames = positional(varargin, 3, {}); initialData = positional(varargin, 4, cell(0, numel(columnNames))); ui = resultTable(parent, titleText, row, columnNames, initialData); - case 'plotoptions' - rowCount = positional(varargin, 1, 1); - row = positional(varargin, 2, 3); - ui = plotOptionsPanel(parent, rowCount, row); - case 'topbottomplotcontrols' - bottomPanel = positional(varargin, 1, []); - xItems = positional(varargin, 2, {}); - yItems = positional(varargin, 3, {}); - topDefaults = positional(varargin, 4, struct()); - bottomDefaults = positional(varargin, 5, struct()); - valueChangedFcn = positional(varargin, 6, []); - ui = topBottomPlotControls(parent, bottomPanel, xItems, yItems, ... - topDefaults, bottomDefaults, valueChangedFcn); otherwise error('labkit_ui:panel:UnknownKind', ... 'Unknown LabKit view panel kind "%s".', char(kind)); @@ -91,15 +73,6 @@ ui = resultTable(parent, fieldOr(spec, 'title', ''), ... fieldOr(spec, 'row', 1), columnNames, ... fieldOr(spec, 'initialData', cell(0, numel(columnNames)))); - case 'plotoptions' - ui = plotOptionsPanel(parent, fieldOr(spec, 'rowCount', 1), ... - fieldOr(spec, 'row', 3)); - case 'topbottomplotcontrols' - ui = topBottomPlotControls(parent, requireField(spec, 'bottomPanel'), ... - fieldOr(spec, 'xItems', {}), fieldOr(spec, 'yItems', {}), ... - fieldOr(spec, 'topDefaults', struct()), ... - fieldOr(spec, 'bottomDefaults', struct()), ... - fieldOr(spec, 'callback', [])); otherwise error('labkit_ui:panel:UnknownKind', ... 'Unknown LabKit view panel kind "%s".', char(kind)); @@ -108,18 +81,6 @@ function key = normalizeKind(kind) key = lower(regexprep(char(string(kind)), '[^a-zA-Z0-9]', '')); - switch key - case {'file', 'fileselection', 'filepanel'} - key = 'files'; - case {'readonlytext', 'textpanel'} - key = 'text'; - case {'result', 'results', 'resulttable', 'tablepanel'} - key = 'table'; - case {'plotoption', 'plotoptionspanel'} - key = 'plotoptions'; - case {'topbottom', 'topbottomplots', 'topbottomplot'} - key = 'topbottomplotcontrols'; - end end function value = positional(args, index, defaultValue) diff --git a/+labkit/+ui/+view/private/plotOptionsPanel.m b/+labkit/+ui/+view/private/plotOptionsPanel.m deleted file mode 100644 index 4fa9ccd4..00000000 --- a/+labkit/+ui/+view/private/plotOptionsPanel.m +++ /dev/null @@ -1,21 +0,0 @@ -% Private UI view helper. Expected caller: labkit.ui.view panel, control, -% plot, or text facades. Inputs and outputs are internal UI handles, labels, -% selections, table data, or plot info. Side effects are limited to supplied UI -% parents or axes; assumes the caller owns callbacks and app state. -function ui = plotOptionsPanel(parent, rowCount, row) -%CREATEPLOTOPTIONSPANEL Create the shared plot-options panel grid. -% -% Inputs: -% parent - parent grid. -% rowCount - number of rows inside the options grid. -% row - optional logical parent row, default 3. -% -% Output: -% ui - section struct from labkit.ui.view.section. - - if nargin < 3 || isempty(row) - row = 3; - end - - ui = labkit.ui.view.section(parent, 'Plot Options', row, [rowCount 2]); -end diff --git a/+labkit/+ui/+view/private/setTopBottomPlotSelections.m b/+labkit/+ui/+view/private/setTopBottomPlotSelections.m deleted file mode 100644 index f4def0fe..00000000 --- a/+labkit/+ui/+view/private/setTopBottomPlotSelections.m +++ /dev/null @@ -1,19 +0,0 @@ -% Private UI view helper. Expected caller: labkit.ui.view panel, control, -% plot, or text facades. Inputs and outputs are internal UI handles, labels, -% selections, table data, or plot info. Side effects are limited to supplied UI -% parents or axes; assumes the caller owns callbacks and app state. -function setTopBottomPlotSelections(topX, topY, bottomX, bottomY, topSelection, bottomSelection) -%SETTOPBOTTOMPLOTSELECTIONS Apply top/bottom plot dropdown selections. -% -% Inputs: -% topX, topY, bottomX, bottomY - dropdown handles. -% topSelection, bottomSelection - structs with x and y fields. -% -% Output: -% Mutates dropdown Value properties in place. - - topX.Value = topSelection.x; - topY.Value = topSelection.y; - bottomX.Value = bottomSelection.x; - bottomY.Value = bottomSelection.y; -end diff --git a/+labkit/+ui/+view/private/swapTopBottomPlotSelections.m b/+labkit/+ui/+view/private/swapTopBottomPlotSelections.m deleted file mode 100644 index 5b577de2..00000000 --- a/+labkit/+ui/+view/private/swapTopBottomPlotSelections.m +++ /dev/null @@ -1,23 +0,0 @@ -% Private UI view helper. Expected caller: labkit.ui.view panel, control, -% plot, or text facades. Inputs and outputs are internal UI handles, labels, -% selections, table data, or plot info. Side effects are limited to supplied UI -% parents or axes; assumes the caller owns callbacks and app state. -function swapTopBottomPlotSelections(topX, topY, bottomX, bottomY) -%SWAPTOPBOTTOMPLOTSELECTIONS Swap top and bottom plot dropdown values. -% -% Inputs: -% topX, topY, bottomX, bottomY - dropdown handles. -% -% Output: -% Mutates dropdown Value properties in place. - - topXValue = topX.Value; - topYValue = topY.Value; - bottomXValue = bottomX.Value; - bottomYValue = bottomY.Value; - - topX.Value = bottomXValue; - topY.Value = bottomYValue; - bottomX.Value = topXValue; - bottomY.Value = topYValue; -end diff --git a/+labkit/+ui/+view/update.m b/+labkit/+ui/+view/update.m index 15f291a3..2cd43ffb 100644 --- a/+labkit/+ui/+view/update.m +++ b/+labkit/+ui/+view/update.m @@ -5,8 +5,6 @@ % labkit.ui.view.update(textArea, "appendLog", message) % labkit.ui.view.update(listbox, "listItems", names) % [value, idx] = labkit.ui.view.update(listbox, "listSelection", names, preferred, opts) -% labkit.ui.view.update(plotControls, "setPlotSelections", topSelection, bottomSelection) -% labkit.ui.view.update(plotControls, "swapPlotSelections") % % Inputs: % target - MATLAB handle or LabKit component struct returned by panel(). @@ -30,16 +28,6 @@ opts = positional(varargin, 3, struct()); [value, index] = refreshListboxSelection(target, names, preferredSelection, opts); out = {value, index}; - case 'setplotselections' - topSelection = positional(varargin, 1, struct()); - bottomSelection = positional(varargin, 2, struct()); - setTopBottomPlotSelections(target.topX, target.topY, ... - target.bottomX, target.bottomY, topSelection, bottomSelection); - out = {[]}; - case 'swapplotselections' - swapTopBottomPlotSelections(target.topX, target.topY, ... - target.bottomX, target.bottomY); - out = {[]}; otherwise error('labkit_ui:update:UnknownAction', ... 'Unknown LabKit view update action "%s".', char(action)); @@ -55,18 +43,6 @@ function action = normalizeAction(action) action = lower(regexprep(char(string(action)), '[^a-zA-Z0-9]', '')); - switch action - case {'log', 'logappend'} - action = 'appendlog'; - case {'items', 'listboxitems', 'refreshlistboxitems'} - action = 'listitems'; - case {'selection', 'listboxselection', 'refreshlistboxselection'} - action = 'listselection'; - case {'settopbottomplotselections', 'settopbottomselections'} - action = 'setplotselections'; - case {'swap', 'swaptopbottomplotselections', 'swaptopbottomselections'} - action = 'swapplotselections'; - end end function value = positional(args, index, defaultValue) diff --git a/apps/dic/dic_postprocess/+dic_postprocess/+ui/createRightAxesPair.m b/apps/dic/dic_postprocess/+dic_postprocess/+ui/createRightAxesPair.m new file mode 100644 index 00000000..5e676cdd --- /dev/null +++ b/apps/dic/dic_postprocess/+dic_postprocess/+ui/createRightAxesPair.m @@ -0,0 +1,46 @@ +% App-owned DIC postprocess overlay layout helper. Expected caller: +% labkit_DICPostprocess_app. Inputs are the shell UI struct, axes titles, and +% whether plot-control panels are needed. Output is the UI struct with +% top/bottom axes and panel fields. Side effects are limited to creating axes +% and optional panels on the shell right grid. +function ui = createRightAxesPair(ui, topTitle, bottomTitle, showControls) +%CREATERIGHTAXESPAIR Create DIC postprocess overlay axes. + + if showControls + ui.topControlsPanel = uipanel(ui.rightGrid, 'Title', topTitle); + ui.topControlsPanel.Layout.Row = 1; + ui.topAxes = createOneAxes(ui.rightGrid, 2, topTitle); + + ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); + ui.bottomControlsPanel.Layout.Row = 3; + ui.bottomAxes = createOneAxes(ui.rightGrid, 4, bottomTitle); + else + ui.topControlsPanel = []; + ui.bottomControlsPanel = []; + ui.topAxes = createOneAxes(ui.rightGrid, 1, topTitle); + ui.bottomAxes = createOneAxes(ui.rightGrid, 2, bottomTitle); + end +end + +function ax = createOneAxes(parent, row, titleText) + ax = uiaxes(parent); + ax.Layout.Row = row; + title(ax, titleText); + labkit.ui.view.draw(ax, 'popout'); + disableAxesInteractivity(ax); +end + +function disableAxesInteractivity(ax) + try + disableDefaultInteractivity(ax); + catch + end + try + ax.Interactions = []; + catch + end + try + ax.Toolbar.Visible = 'off'; + catch + end +end diff --git a/apps/dic/dic_postprocess/labkit_DICPostprocess_app.m b/apps/dic/dic_postprocess/labkit_DICPostprocess_app.m index aeaed965..08b70c7e 100644 --- a/apps/dic/dic_postprocess/labkit_DICPostprocess_app.m +++ b/apps/dic/dic_postprocess/labkit_DICPostprocess_app.m @@ -28,11 +28,11 @@ S.overlayEyy = []; S.summaryTable = table(); - workbenchOpts = struct('rightKind', 'dualPlot', ... + workbenchOpts = struct( ... 'rightTitle', 'Strain Overlays', ... - 'topPlotTitle', 'EXX Overlay', ... - 'bottomPlotTitle', 'EYY Overlay', ... - 'showPlotControls', false); + 'rightGridSize', [2 1], ... + 'rightRowHeight', {{'1x', '1x'}}, ... + 'rightRowSpacing', 10); workbenchOpts.tabs = [ ... labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ... {240, 230, 260, 120}, ... @@ -48,6 +48,8 @@ 'position', [90 70 1450 880], ... 'leftWidth', 390, ... 'options', workbenchOpts)); + ui = dic_postprocess.ui.createRightAxesPair(ui, ... + 'EXX Overlay', 'EYY Overlay', false); fig = ui.fig; layFA = ui.filesAnalysisGrid; @@ -69,13 +71,19 @@ btnMask.Layout.Row = 2; btnMask.Layout.Column = [1 2]; - txtMat = labkit.ui.view.form(fileGrid, 'readonly', 'Value', 'No MAT file loaded'); + txtMat = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No MAT file loaded')); txtMat.Layout.Row = 3; txtMat.Layout.Column = [1 2]; - txtReference = labkit.ui.view.form(fileGrid, 'readonly', 'Value', 'No reference image loaded'); + txtReference = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No reference image loaded')); txtReference.Layout.Row = 4; txtReference.Layout.Column = [1 2]; - txtMask = labkit.ui.view.form(fileGrid, 'readonly', 'Value', 'No mask image loaded'); + txtMask = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No mask image loaded')); txtMask.Layout.Row = 5; txtMask.Layout.Column = [1 2]; @@ -88,37 +96,24 @@ struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}})); optionGrid = optionPanel.grid; - [~, edAlpha] = labkit.ui.view.form(optionGrid, 'spinner', 'Alpha:', ... - 'Value', 0.60, 'Limits', [0 1], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edMin] = labkit.ui.view.form(optionGrid, 'spinner', 'Color min:', ... - 'Value', -0.15, 'Step', 0.01, 'ValueChangedFcn', @onOptionsChanged); - [~, edMax] = labkit.ui.view.form(optionGrid, 'spinner', 'Color max:', ... - 'Value', 0.15, 'Step', 0.01, 'ValueChangedFcn', @onOptionsChanged); - [~, edOversample] = labkit.ui.view.form(optionGrid, 'spinner', 'Oversample:', ... - 'Value', 6, 'Limits', [1 20], 'Step', 1, 'ValueChangedFcn', @onOptionsChanged); - [~, edSigma] = labkit.ui.view.form(optionGrid, 'spinner', 'Smooth sigma:', ... - 'Value', 0.8, 'Limits', [0 Inf], 'Step', 0.1, 'ValueChangedFcn', @onOptionsChanged); - [~, edResolution] = labkit.ui.view.form(optionGrid, 'spinner', 'Export DPI:', ... - 'Value', 1000, 'Limits', [72 2400], 'Step', 50); + [~, edAlpha] = labkit.ui.view.form(optionGrid, struct('kind', 'spinner', 'label', 'Alpha:', 'value', 0.60, 'limits', [0 1], 'step', 0.05, 'callback', @onOptionsChanged)); + [~, edMin] = labkit.ui.view.form(optionGrid, struct('kind', 'spinner', 'label', 'Color min:', 'value', -0.15, 'step', 0.01, 'callback', @onOptionsChanged)); + [~, edMax] = labkit.ui.view.form(optionGrid, struct('kind', 'spinner', 'label', 'Color max:', 'value', 0.15, 'step', 0.01, 'callback', @onOptionsChanged)); + [~, edOversample] = labkit.ui.view.form(optionGrid, struct('kind', 'spinner', 'label', 'Oversample:', 'value', 6, 'limits', [1 20], 'step', 1, 'callback', @onOptionsChanged)); + [~, edSigma] = labkit.ui.view.form(optionGrid, struct('kind', 'spinner', 'label', 'Smooth sigma:', 'value', 0.8, 'limits', [0 Inf], 'step', 0.1, 'callback', @onOptionsChanged)); + [~, edResolution] = labkit.ui.view.form(optionGrid, struct('kind', 'spinner', 'label', 'Export DPI:', 'value', 1000, 'limits', [72 2400], 'step', 50)); imagePanel = labkit.ui.view.section(layFA, 'Optical Image Enhancement', 3, [7 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}})); imageGrid = imagePanel.grid; - [~, edBrightness] = labkit.ui.view.form(imageGrid, 'spinner', 'Brightness:', ... - 'Value', 0, 'Limits', [-1 1], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edContrast] = labkit.ui.view.form(imageGrid, 'spinner', 'Contrast:', ... - 'Value', 1, 'Limits', [0.05 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edGamma] = labkit.ui.view.form(imageGrid, 'spinner', 'Gamma:', ... - 'Value', 1, 'Limits', [0.05 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edSaturation] = labkit.ui.view.form(imageGrid, 'spinner', 'Saturation:', ... - 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edRedGain] = labkit.ui.view.form(imageGrid, 'spinner', 'Red gain:', ... - 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edGreenGain] = labkit.ui.view.form(imageGrid, 'spinner', 'Green gain:', ... - 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edBlueGain] = labkit.ui.view.form(imageGrid, 'spinner', 'Blue gain:', ... - 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); + [~, edBrightness] = labkit.ui.view.form(imageGrid, struct('kind', 'spinner', 'label', 'Brightness:', 'value', 0, 'limits', [-1 1], 'step', 0.05, 'callback', @onOptionsChanged)); + [~, edContrast] = labkit.ui.view.form(imageGrid, struct('kind', 'spinner', 'label', 'Contrast:', 'value', 1, 'limits', [0.05 5], 'step', 0.05, 'callback', @onOptionsChanged)); + [~, edGamma] = labkit.ui.view.form(imageGrid, struct('kind', 'spinner', 'label', 'Gamma:', 'value', 1, 'limits', [0.05 5], 'step', 0.05, 'callback', @onOptionsChanged)); + [~, edSaturation] = labkit.ui.view.form(imageGrid, struct('kind', 'spinner', 'label', 'Saturation:', 'value', 1, 'limits', [0 5], 'step', 0.05, 'callback', @onOptionsChanged)); + [~, edRedGain] = labkit.ui.view.form(imageGrid, struct('kind', 'spinner', 'label', 'Red gain:', 'value', 1, 'limits', [0 5], 'step', 0.05, 'callback', @onOptionsChanged)); + [~, edGreenGain] = labkit.ui.view.form(imageGrid, struct('kind', 'spinner', 'label', 'Green gain:', 'value', 1, 'limits', [0 5], 'step', 0.05, 'callback', @onOptionsChanged)); + [~, edBlueGain] = labkit.ui.view.form(imageGrid, struct('kind', 'spinner', 'label', 'Blue gain:', 'value', 1, 'limits', [0 5], 'step', 0.05, 'callback', @onOptionsChanged)); exportPanel = labkit.ui.view.section(layFA, 'Exports', 4, [3 2], ... struct('rowHeight', {{'fit', 'fit', 'fit'}}, 'columnWidth', {{'1x', '1x'}})); diff --git a/apps/dic/dic_preprocess/+dic_preprocess/+ui/createLayout.m b/apps/dic/dic_preprocess/+dic_preprocess/+ui/createLayout.m index c599b62a..7b3fa140 100644 --- a/apps/dic/dic_preprocess/+dic_preprocess/+ui/createLayout.m +++ b/apps/dic/dic_preprocess/+dic_preprocess/+ui/createLayout.m @@ -5,11 +5,11 @@ function [ui, controls] = createLayout(callbacks) %CREATELAYOUT Build DIC preprocess shell, controls, and preview runtime. - workbenchOpts = struct('rightKind', 'dualPlot', ... + workbenchOpts = struct( ... 'rightTitle', 'Image Preview', ... - 'topPlotTitle', 'Reference', ... - 'bottomPlotTitle', 'Current Preview', ... - 'showPlotControls', false); + 'rightGridSize', [2 1], ... + 'rightRowHeight', {{'1x', '1x'}}, ... + 'rightRowSpacing', 10); workbenchOpts.tabs = [ ... labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ... {240, 210, 330, 170}, ... @@ -25,6 +25,8 @@ 'position', [80 60 1400 860], ... 'leftWidth', 370, ... 'options', workbenchOpts)); + ui = dic_preprocess.ui.createRightAxesPair(ui, ... + 'Reference', 'Current Preview', false); ui.imageRuntime = labkit.ui.tool.createRuntime(ui.topAxes, ... struct('figure', ui.fig, 'defaultScrollFcn', callbacks.scrollZoom)); @@ -43,21 +45,25 @@ controls.btnMoving.Layout.Row = 1; controls.btnMoving.Layout.Column = 2; - controls.txtReference = labkit.ui.view.form(fileGrid, 'readonly', ... - 'Value', 'No reference image loaded'); + controls.txtReference = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No reference image loaded')); controls.txtReference.Layout.Row = 2; controls.txtReference.Layout.Column = [1 2]; - controls.txtMoving = labkit.ui.view.form(fileGrid, 'readonly', ... - 'Value', 'No moving image loaded'); + controls.txtMoving = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No moving image loaded')); controls.txtMoving.Layout.Row = 3; controls.txtMoving.Layout.Column = [1 2]; [controls.lblPreview, controls.ddPreview] = labkit.ui.view.form( ... - fileGrid, 'dropdown', 'Preview:', ... - 'Items', {'Current pair', 'Current moving image', ... - 'False-color overlay', 'Original pair', 'ROI mask'}, ... - 'Value', 'Current pair', ... - 'ValueChangedFcn', callbacks.previewChanged); + fileGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Preview:', ... + 'items', {{'Current pair', 'Current moving image', ... + 'False-color overlay', 'Original pair', 'ROI mask'}}, ... + 'value', 'Current pair', ... + 'callback', callbacks.previewChanged)); controls.lblPreview.Layout.Row = 4; controls.lblPreview.Layout.Column = 1; controls.ddPreview.Layout.Row = 4; @@ -114,10 +120,12 @@ controls.btnStartMask.Layout.Row = 1; controls.btnStartMask.Layout.Column = [1 2]; [controls.lblBoundaryStyle, controls.ddBoundaryStyle] = labkit.ui.view.form( ... - maskGrid, 'dropdown', 'Boundary:', ... - 'Items', {'Curve', 'Straight lines'}, ... - 'Value', 'Curve', ... - 'ValueChangedFcn', callbacks.boundaryStyleChanged); + maskGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Boundary:', ... + 'items', {{'Curve', 'Straight lines'}}, ... + 'value', 'Curve', ... + 'callback', callbacks.boundaryStyleChanged)); controls.lblBoundaryStyle.Layout.Row = 2; controls.lblBoundaryStyle.Layout.Column = 1; controls.ddBoundaryStyle.Layout.Row = 2; diff --git a/apps/dic/dic_preprocess/+dic_preprocess/+ui/createRightAxesPair.m b/apps/dic/dic_preprocess/+dic_preprocess/+ui/createRightAxesPair.m new file mode 100644 index 00000000..7948db82 --- /dev/null +++ b/apps/dic/dic_preprocess/+dic_preprocess/+ui/createRightAxesPair.m @@ -0,0 +1,46 @@ +% App-owned DIC preprocess preview layout helper. Expected caller: +% dic_preprocess.ui.createLayout. Inputs are the shell UI struct, axes titles, +% and whether plot-control panels are needed. Output is the UI struct with +% top/bottom axes and panel fields. Side effects are limited to creating axes +% and optional panels on the shell right grid. +function ui = createRightAxesPair(ui, topTitle, bottomTitle, showControls) +%CREATERIGHTAXESPAIR Create DIC preprocess preview axes. + + if showControls + ui.topControlsPanel = uipanel(ui.rightGrid, 'Title', topTitle); + ui.topControlsPanel.Layout.Row = 1; + ui.topAxes = createOneAxes(ui.rightGrid, 2, topTitle); + + ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); + ui.bottomControlsPanel.Layout.Row = 3; + ui.bottomAxes = createOneAxes(ui.rightGrid, 4, bottomTitle); + else + ui.topControlsPanel = []; + ui.bottomControlsPanel = []; + ui.topAxes = createOneAxes(ui.rightGrid, 1, topTitle); + ui.bottomAxes = createOneAxes(ui.rightGrid, 2, bottomTitle); + end +end + +function ax = createOneAxes(parent, row, titleText) + ax = uiaxes(parent); + ax.Layout.Row = row; + title(ax, titleText); + labkit.ui.view.draw(ax, 'popout'); + disableAxesInteractivity(ax); +end + +function disableAxesInteractivity(ax) + try + disableDefaultInteractivity(ax); + catch + end + try + ax.Interactions = []; + catch + end + try + ax.Toolbar.Visible = 'off'; + catch + end +end diff --git a/apps/electrochem/chrono_overlay/+chrono_overlay/+ui/runApp.m b/apps/electrochem/chrono_overlay/+chrono_overlay/+ui/runApp.m index 66de6cfd..8fe27623 100644 --- a/apps/electrochem/chrono_overlay/+chrono_overlay/+ui/runApp.m +++ b/apps/electrochem/chrono_overlay/+chrono_overlay/+ui/runApp.m @@ -45,19 +45,23 @@ lbFiles = fileUi.listbox; txtLoaded = fileUi.loadedText; - plotOptionsUi = labkit.ui.view.panel(layFA, 'plotOptions', 4, 2); + plotOptionsUi = labkit.ui.view.section(layFA, 'Plot Options', 2, [4 2]); gp = plotOptionsUi.grid; - [~, ddXAxis] = labkit.ui.view.form(gp, 'dropdown', 'X axis:', ... - 'Items', {'Time (s)', 'Time (ms)', 'Sample #'}, ... - 'Value', 'Time (s)', ... - 'ValueChangedFcn', @(~,~) refreshPlots()); - - [~, edLineWidth] = labkit.ui.view.form(gp, 'spinner', 'Line width:', ... - 'Value', 1.3, ... - 'Limits', [0.1 10], ... - 'Step', 0.1, ... - 'ValueChangedFcn', @(~,~) refreshPlots()); + [~, ddXAxis] = labkit.ui.view.form(gp, struct( ... + 'kind', 'dropdown', ... + 'label', 'X axis:', ... + 'items', {{'Time (s)', 'Time (ms)', 'Sample #'}}, ... + 'value', 'Time (s)', ... + 'callback', @(~,~) refreshPlots())); + + [~, edLineWidth] = labkit.ui.view.form(gp, struct( ... + 'kind', 'spinner', ... + 'label', 'Line width:', ... + 'value', 1.3, ... + 'limits', [0.1 10], ... + 'step', 0.1, ... + 'callback', @(~,~) refreshPlots())); cbLegend = uicheckbox(gp, ... 'Text', 'Show file-name legend', ... diff --git a/apps/electrochem/cic/+cic/+ui/buildControls.m b/apps/electrochem/cic/+cic/+ui/buildControls.m index e42d4d21..4ad98af1 100644 --- a/apps/electrochem/cic/+cic/+ui/buildControls.m +++ b/apps/electrochem/cic/+cic/+ui/buildControls.m @@ -11,7 +11,12 @@ 'title', 'Gamry CIC GUI (Voltage Transient)', ... 'position', [40 30 1680 980], ... 'leftWidth', 430, ... - 'options', struct('rightKind', 'dualPlot'))); + 'options', struct( ... + 'rightTitle', 'Plots', ... + 'rightGridSize', [4 1], ... + 'rightRowHeight', {{'fit', '1x', 'fit', '1x'}}, ... + 'rightRowSpacing', 10))); + ui = cic.ui.createRightAxesPair(ui, 'Top Plot', 'Bottom Plot', true); C.fig = ui.fig; fileLabels = struct( ... @@ -37,24 +42,36 @@ 'ValueChangedFcn', callbacks.onPresetChanged); C.ddPreset.Layout.Row = 1; C.ddPreset.Layout.Column = 2; - [lblCathLim, C.edCathLim] = labkit.ui.view.form(gs, 'spinner', ... - 'Cathodic limit (V):', 'Value', -0.6, 'Limits', [-10 10], ... - 'Step', 0.01, 'ValueDisplayFormat', '%.6g', ... - 'ValueChangedFcn', callbacks.onAnalyzeCurrentFile); + [lblCathLim, C.edCathLim] = labkit.ui.view.form(gs, struct( ... + 'kind', 'spinner', ... + 'label', 'Cathodic limit (V):', ... + 'value', -0.6, ... + 'limits', [-10 10], ... + 'step', 0.01, ... + 'valueDisplayFormat', '%.6g', ... + 'callback', callbacks.onAnalyzeCurrentFile)); lblCathLim.Layout.Row = 2; lblCathLim.Layout.Column = 1; C.edCathLim.Layout.Row = 2; C.edCathLim.Layout.Column = 2; - [lblAnodLim, C.edAnodLim] = labkit.ui.view.form(gs, 'spinner', ... - 'Anodic limit (V):', 'Value', 0.8, 'Limits', [-10 10], ... - 'Step', 0.01, 'ValueDisplayFormat', '%.6g', ... - 'ValueChangedFcn', callbacks.onAnalyzeCurrentFile); + [lblAnodLim, C.edAnodLim] = labkit.ui.view.form(gs, struct( ... + 'kind', 'spinner', ... + 'label', 'Anodic limit (V):', ... + 'value', 0.8, ... + 'limits', [-10 10], ... + 'step', 0.01, ... + 'valueDisplayFormat', '%.6g', ... + 'callback', callbacks.onAnalyzeCurrentFile)); lblAnodLim.Layout.Row = 3; lblAnodLim.Layout.Column = 1; C.edAnodLim.Layout.Row = 3; C.edAnodLim.Layout.Column = 2; - [lblDelayUs, C.edDelayUs] = labkit.ui.view.form(gs, 'spinner', ... - 'Sample delay after pulse end:', 'Value', 10, 'Limits', [0 inf], ... - 'Step', 1, 'ValueDisplayFormat', '%.6g', ... - 'ValueChangedFcn', callbacks.onAnalyzeCurrentFile); + [lblDelayUs, C.edDelayUs] = labkit.ui.view.form(gs, struct( ... + 'kind', 'spinner', ... + 'label', 'Sample delay after pulse end:', ... + 'value', 10, ... + 'limits', [0 inf], ... + 'step', 1, ... + 'valueDisplayFormat', '%.6g', ... + 'callback', callbacks.onAnalyzeCurrentFile)); lblDelayUs.Layout.Row = 4; lblDelayUs.Layout.Column = 1; C.edDelayUs.Layout.Row = 4; C.edDelayUs.Layout.Column = 2; @@ -93,17 +110,17 @@ infoUi = labkit.ui.view.section(ui.summaryResultsGrid, ... 'Current File Summary', 1, [11 2]); gi = infoUi.grid; - C.txtControlMode = labkit.ui.view.form(gi, 'info', 1, 'Control mode:'); - C.txtDetect = labkit.ui.view.form(gi, 'info', 2, 'Detection:'); - C.txtDelay = labkit.ui.view.form(gi, 'info', 3, 'Delay used:'); - C.txtArea = labkit.ui.view.form(gi, 'info', 4, 'Area:'); - C.txtEmc = labkit.ui.view.form(gi, 'info', 5, 'Emc:'); - C.txtEma = labkit.ui.view.form(gi, 'info', 6, 'Ema:'); - C.txtQc = labkit.ui.view.form(gi, 'info', 7, 'Cathodic Q/CIC:'); - C.txtQa = labkit.ui.view.form(gi, 'info', 8, 'Anodic Q/CIC:'); - C.txtQt = labkit.ui.view.form(gi, 'info', 9, 'Total Q/CIC:'); - C.txtSafe = labkit.ui.view.form(gi, 'info', 10, 'Safety:'); - C.txtBest = labkit.ui.view.form(gi, 'info', 11, 'Best safe among loaded:'); + C.txtControlMode = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 1, 'label', 'Control mode:')); + C.txtDetect = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 2, 'label', 'Detection:')); + C.txtDelay = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 3, 'label', 'Delay used:')); + C.txtArea = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 4, 'label', 'Area:')); + C.txtEmc = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 5, 'label', 'Emc:')); + C.txtEma = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 6, 'label', 'Ema:')); + C.txtQc = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 7, 'label', 'Cathodic Q/CIC:')); + C.txtQa = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 8, 'label', 'Anodic Q/CIC:')); + C.txtQt = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 9, 'label', 'Total Q/CIC:')); + C.txtSafe = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 10, 'label', 'Safety:')); + C.txtBest = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 11, 'label', 'Best safe among loaded:')); actionUi = labkit.ui.view.section(ui.filesAnalysisGrid, 'Plot / Debug', 3, [2 3]); ga = actionUi.grid; @@ -138,9 +155,8 @@ C.topPlotDefaults = struct('x', 'Time (s)', 'y', 'VT: Vf vs time', 'grid', true); C.bottomPlotDefaults = struct('x', 'Time (s)', 'y', 'IT: Im vs time', 'grid', true); - C.plotControls = labkit.ui.view.panel( ... + C.plotControls = cic.ui.topBottomPlotControls( ... ui.topControlsPanel, ... - 'topBottomPlotControls', ... ui.bottomControlsPanel, ... {'Time (s)', 'Sample #'}, ... {'VT: Vf vs time', 'IT: Im vs time'}, ... diff --git a/apps/electrochem/cic/+cic/+ui/createRightAxesPair.m b/apps/electrochem/cic/+cic/+ui/createRightAxesPair.m new file mode 100644 index 00000000..ffafa93a --- /dev/null +++ b/apps/electrochem/cic/+cic/+ui/createRightAxesPair.m @@ -0,0 +1,45 @@ +% App-owned CIC right-side axes layout helper. Expected caller: cic.ui.buildControls. +% Inputs are the shell UI struct, axes titles, and whether plot-control panels +% are needed. Output is the UI struct with top/bottom axes and panel fields. +% Side effects are limited to creating controls on the shell right grid. +function ui = createRightAxesPair(ui, topTitle, bottomTitle, showControls) +%CREATERIGHTAXESPAIR Create CIC top/bottom plot axes. + + if showControls + ui.topControlsPanel = uipanel(ui.rightGrid, 'Title', topTitle); + ui.topControlsPanel.Layout.Row = 1; + ui.topAxes = createOneAxes(ui.rightGrid, 2, topTitle); + + ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); + ui.bottomControlsPanel.Layout.Row = 3; + ui.bottomAxes = createOneAxes(ui.rightGrid, 4, bottomTitle); + else + ui.topControlsPanel = []; + ui.bottomControlsPanel = []; + ui.topAxes = createOneAxes(ui.rightGrid, 1, topTitle); + ui.bottomAxes = createOneAxes(ui.rightGrid, 2, bottomTitle); + end +end + +function ax = createOneAxes(parent, row, titleText) + ax = uiaxes(parent); + ax.Layout.Row = row; + title(ax, titleText); + labkit.ui.view.draw(ax, 'popout'); + disableAxesInteractivity(ax); +end + +function disableAxesInteractivity(ax) + try + disableDefaultInteractivity(ax); + catch + end + try + ax.Interactions = []; + catch + end + try + ax.Toolbar.Visible = 'off'; + catch + end +end diff --git a/apps/electrochem/cic/+cic/+ui/runApp.m b/apps/electrochem/cic/+cic/+ui/runApp.m index 75474894..596533a5 100644 --- a/apps/electrochem/cic/+cic/+ui/runApp.m +++ b/apps/electrochem/cic/+cic/+ui/runApp.m @@ -366,7 +366,7 @@ function plotOneAxis(ax, A, xChoice, yChoice, showGrid) end function swapPlots() - labkit.ui.view.update(plotControls, 'swapPlotSelections'); + plotControls.swapSelections(); refreshPlots(); end @@ -376,8 +376,7 @@ function resetAxes() end function restoreDefaultPlotSelections() - labkit.ui.view.update(plotControls, 'setPlotSelections', ... - topPlotDefaults, bottomPlotDefaults); + plotControls.setSelections(topPlotDefaults, bottomPlotDefaults); end function resetAxesToDefaultState() diff --git a/+labkit/+ui/+view/private/topBottomPlotControls.m b/apps/electrochem/cic/+cic/+ui/topBottomPlotControls.m similarity index 52% rename from +labkit/+ui/+view/private/topBottomPlotControls.m rename to apps/electrochem/cic/+cic/+ui/topBottomPlotControls.m index 83955cd0..f80b7532 100644 --- a/+labkit/+ui/+view/private/topBottomPlotControls.m +++ b/apps/electrochem/cic/+cic/+ui/topBottomPlotControls.m @@ -1,18 +1,10 @@ -% Private UI view helper. Expected caller: labkit.ui.view panel, control, -% plot, or text facades. Inputs and outputs are internal UI handles, labels, -% selections, table data, or plot info. Side effects are limited to supplied UI -% parents or axes; assumes the caller owns callbacks and app state. +% App-owned CIC top/bottom plot controls helper. Expected caller: +% cic.ui.buildControls. Inputs are parent panels, axis items, default +% selections, and a value-change callback. Output is a controls struct with +% handles plus setSelections/swapSelections closures. Side effects are limited +% to creating controls on the supplied panels. function ui = topBottomPlotControls(topPanel, bottomPanel, xItems, yItems, topDefaults, bottomDefaults, valueChangedFcn) -%CREATETOPBOTTOMPLOTCONTROLS Create shared top/bottom plot controls. -% -% Inputs: -% topPanel, bottomPanel - parent panels for control rows. -% xItems, yItems - dropdown items for X and Y axes. -% topDefaults, bottomDefaults - structs with x and y default values. -% valueChangedFcn - optional callback for dropdowns/grid checkboxes. -% -% Output: -% ui - struct with top/bottom grids, X/Y dropdowns, and grid checkboxes. +%TOPBOTTOMPLOTCONTROLS Create CIC top/bottom plot controls. if nargin < 7 valueChangedFcn = []; @@ -23,6 +15,19 @@ topPanel, xItems, yItems, topDefaults, valueChangedFcn); [ui.bottomGrid, ui.bottomX, ui.bottomY, ui.bottomGridCheckbox] = createOneRow( ... bottomPanel, xItems, yItems, bottomDefaults, valueChangedFcn); + ui.setSelections = @setSelections; + ui.swapSelections = @swapSelections; + + function setSelections(topSelection, bottomSelection) + applySelection(ui.topX, ui.topY, topSelection); + applySelection(ui.bottomX, ui.bottomY, bottomSelection); + end + + function swapSelections() + topSelection = struct('x', ui.topX.Value, 'y', ui.topY.Value); + bottomSelection = struct('x', ui.bottomX.Value, 'y', ui.bottomY.Value); + setSelections(bottomSelection, topSelection); + end end function [grid, ddX, ddY, cbGrid] = createOneRow(parent, xItems, yItems, defaults, valueChangedFcn) @@ -48,3 +53,12 @@ 'Value', defaults.grid, ... 'ValueChangedFcn', valueChangedFcn); end + +function applySelection(ddX, ddY, selection) + if isfield(selection, 'x') && any(strcmp(ddX.Items, selection.x)) + ddX.Value = selection.x; + end + if isfield(selection, 'y') && any(strcmp(ddY.Items, selection.y)) + ddY.Value = selection.y; + end +end diff --git a/apps/electrochem/csc/+csc/+ui/buildControls.m b/apps/electrochem/csc/+csc/+ui/buildControls.m index 66e9e20b..08f523cf 100644 --- a/apps/electrochem/csc/+csc/+ui/buildControls.m +++ b/apps/electrochem/csc/+csc/+ui/buildControls.m @@ -11,7 +11,12 @@ 'title', 'Gamry DTA GUI (literature CSC)', ... 'position', [50 30 1580 950], ... 'leftWidth', 390, ... - 'options', struct('rightKind', 'dualPlot'))); + 'options', struct( ... + 'rightTitle', 'Plots', ... + 'rightGridSize', [4 1], ... + 'rightRowHeight', {{'fit', '1x', 'fit', '1x'}}, ... + 'rightRowSpacing', 10))); + ui = csc.ui.createRightAxesPair(ui, 'Top Plot', 'Bottom Plot', true); C.fig = ui.fig; fileLabels = struct( ... @@ -28,11 +33,11 @@ curveUi = labkit.ui.view.section(ui.filesAnalysisGrid, 'Curve', 2, [4 2]); gf = curveUi.grid; uilabel(gf, 'Text', 'File:', 'HorizontalAlignment', 'right'); - C.txtFile = labkit.ui.view.form(gf, 'readonly'); + C.txtFile = labkit.ui.view.form(gf, struct('kind', 'readonly')); C.txtFile.Layout.Row = 1; C.txtFile.Layout.Column = 2; uilabel(gf, 'Text', 'Scan rate:', 'HorizontalAlignment', 'right'); - C.txtScan = labkit.ui.view.form(gf, 'readonly'); + C.txtScan = labkit.ui.view.form(gf, struct('kind', 'readonly')); C.txtScan.Layout.Row = 2; C.txtScan.Layout.Column = 2; uilabel(gf, 'Text', 'Curve:', 'HorizontalAlignment', 'right'); @@ -77,23 +82,23 @@ C.edArea.Layout.Row = 2; C.edArea.Layout.Column = 2; uilabel(gc, 'Text', 'CT charge / CSC:', 'HorizontalAlignment', 'right'); - C.txtQct = labkit.ui.view.form(gc, 'readonly'); + C.txtQct = labkit.ui.view.form(gc, struct('kind', 'readonly')); C.txtQct.Layout.Row = 3; C.txtQct.Layout.Column = 2; uilabel(gc, 'Text', 'CV charge / CSC:', 'HorizontalAlignment', 'right'); - C.txtQcv = labkit.ui.view.form(gc, 'readonly'); + C.txtQcv = labkit.ui.view.form(gc, struct('kind', 'readonly')); C.txtQcv.Layout.Row = 4; C.txtQcv.Layout.Column = 2; uilabel(gc, 'Text', 'Difference:', 'HorizontalAlignment', 'right'); - C.txtDiff = labkit.ui.view.form(gc, 'readonly'); + C.txtDiff = labkit.ui.view.form(gc, struct('kind', 'readonly')); C.txtDiff.Layout.Row = 5; C.txtDiff.Layout.Column = 2; uilabel(gc, 'Text', 'Relative diff:', 'HorizontalAlignment', 'right'); - C.txtRel = labkit.ui.view.form(gc, 'readonly'); + C.txtRel = labkit.ui.view.form(gc, struct('kind', 'readonly')); C.txtRel.Layout.Row = 6; C.txtRel.Layout.Column = 2; uilabel(gc, 'Text', 'max|dt-|dV|/v|:', 'HorizontalAlignment', 'right'); - C.txtDtErr = labkit.ui.view.form(gc, 'readonly'); + C.txtDtErr = labkit.ui.view.form(gc, struct('kind', 'readonly')); C.txtDtErr.Layout.Row = 7; C.txtDtErr.Layout.Column = 2; C.lblStatus = uilabel(gc, 'Text', 'Ready'); @@ -106,9 +111,8 @@ topPlotDefaults = struct('x', '(none)', 'y', '(none)', 'grid', true); bottomPlotDefaults = struct('x', '(none)', 'y', '(none)', 'grid', true); - C.plotControls = labkit.ui.view.panel( ... + C.plotControls = csc.ui.topBottomPlotControls( ... ui.topControlsPanel, ... - 'topBottomPlotControls', ... ui.bottomControlsPanel, ... {'(none)'}, ... {'(none)'}, ... diff --git a/apps/electrochem/csc/+csc/+ui/createRightAxesPair.m b/apps/electrochem/csc/+csc/+ui/createRightAxesPair.m new file mode 100644 index 00000000..08d57578 --- /dev/null +++ b/apps/electrochem/csc/+csc/+ui/createRightAxesPair.m @@ -0,0 +1,45 @@ +% App-owned CSC right-side axes layout helper. Expected caller: csc.ui.buildControls. +% Inputs are the shell UI struct, axes titles, and whether plot-control panels +% are needed. Output is the UI struct with top/bottom axes and panel fields. +% Side effects are limited to creating controls on the shell right grid. +function ui = createRightAxesPair(ui, topTitle, bottomTitle, showControls) +%CREATERIGHTAXESPAIR Create CSC top/bottom plot axes. + + if showControls + ui.topControlsPanel = uipanel(ui.rightGrid, 'Title', topTitle); + ui.topControlsPanel.Layout.Row = 1; + ui.topAxes = createOneAxes(ui.rightGrid, 2, topTitle); + + ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); + ui.bottomControlsPanel.Layout.Row = 3; + ui.bottomAxes = createOneAxes(ui.rightGrid, 4, bottomTitle); + else + ui.topControlsPanel = []; + ui.bottomControlsPanel = []; + ui.topAxes = createOneAxes(ui.rightGrid, 1, topTitle); + ui.bottomAxes = createOneAxes(ui.rightGrid, 2, bottomTitle); + end +end + +function ax = createOneAxes(parent, row, titleText) + ax = uiaxes(parent); + ax.Layout.Row = row; + title(ax, titleText); + labkit.ui.view.draw(ax, 'popout'); + disableAxesInteractivity(ax); +end + +function disableAxesInteractivity(ax) + try + disableDefaultInteractivity(ax); + catch + end + try + ax.Interactions = []; + catch + end + try + ax.Toolbar.Visible = 'off'; + catch + end +end diff --git a/+labkit/+ui/+view/private/plotXY.m b/apps/electrochem/csc/+csc/+ui/plotXY.m similarity index 60% rename from +labkit/+ui/+view/private/plotXY.m rename to apps/electrochem/csc/+csc/+ui/plotXY.m index 6d15b2a4..0bb13175 100644 --- a/+labkit/+ui/+view/private/plotXY.m +++ b/apps/electrochem/csc/+csc/+ui/plotXY.m @@ -1,26 +1,10 @@ -% Private UI view helper. Expected caller: labkit.ui.view panel, control, -% plot, or text facades. Inputs and outputs are internal UI handles, labels, -% selections, table data, or plot info. Side effects are limited to supplied UI -% parents or axes; assumes the caller owns callbacks and app state. +% App-owned CSC plotting helper. Expected caller: csc.ui.runApp plot callbacks. +% Inputs are a target axes, prepared X/Y vectors, label struct, and display +% options. Output is a status struct for runner logging. Side effects are +% limited to drawing on the supplied axes; assumes csc.view.plotRequest prepared +% app-specific data and log text. function info = plotXY(ax, x, y, labels, opts) -%PLOTXY Plot one prepared X/Y numeric series. -% -% Usage: -% info = plotXY(ax, x, y, struct('x','Time','y','Voltage')); -% -% Inputs: -% ax - target axes. -% x, y - numeric vectors of equal length. -% labels - optional struct with title, x, and y fields; defaults blank. -% opts - optional struct. -% -% Options: -% holdPlot - logical, default false; false clears axes before plotting. -% showGrid - logical or MATLAB grid value, default true. -% lineWidth - positive scalar, default 1.2. -% -% Output: -% info - status struct with ok, message, x/y vectors, and x/y names. +%PLOTXY Plot one prepared CSC X/Y numeric series. if nargin < 4 labels = struct(); diff --git a/apps/electrochem/csc/+csc/+ui/runApp.m b/apps/electrochem/csc/+csc/+ui/runApp.m index 7188bd08..050bf21b 100644 --- a/apps/electrochem/csc/+csc/+ui/runApp.m +++ b/apps/electrochem/csc/+csc/+ui/runApp.m @@ -319,8 +319,7 @@ function plotTop() c = S.curves(S.currentCurve); opts = struct('holdPlot', cbTopHold.Value, 'showGrid', cbTopGrid.Value, 'lineWidth', 1.2); request = csc.view.plotRequest(c, ddTopX.Value, ddTopY.Value, 'Top'); - info = labkit.ui.view.draw(axTop, 'xy', request.x, request.y, ... - request.labels, opts); + info = csc.ui.plotXY(axTop, request.x, request.y, request.labels, opts); if ~info.ok addLog(request.skipLog); return; @@ -333,8 +332,7 @@ function plotBottom() c = S.curves(S.currentCurve); opts = struct('holdPlot', cbBotHold.Value, 'showGrid', cbBotGrid.Value, 'lineWidth', 1.2); request = csc.view.plotRequest(c, ddBotX.Value, ddBotY.Value, 'Bottom'); - info = labkit.ui.view.draw(axBottom, 'xy', request.x, request.y, ... - request.labels, opts); + info = csc.ui.plotXY(axBottom, request.x, request.y, request.labels, opts); if ~info.ok addLog(request.skipLog); return; diff --git a/apps/electrochem/csc/+csc/+ui/topBottomPlotControls.m b/apps/electrochem/csc/+csc/+ui/topBottomPlotControls.m new file mode 100644 index 00000000..9e3de88b --- /dev/null +++ b/apps/electrochem/csc/+csc/+ui/topBottomPlotControls.m @@ -0,0 +1,64 @@ +% App-owned CSC top/bottom plot controls helper. Expected caller: +% csc.ui.buildControls. Inputs are parent panels, axis items, default +% selections, and a value-change callback. Output is a controls struct with +% dropdowns, grid checkboxes, and selection closures. Side effects are limited +% to creating controls on the supplied panels. +function ui = topBottomPlotControls(topPanel, bottomPanel, xItems, yItems, topDefaults, bottomDefaults, valueChangedFcn) +%TOPBOTTOMPLOTCONTROLS Create CSC top/bottom plot controls. + + if nargin < 7 + valueChangedFcn = []; + end + + ui = struct(); + [ui.topGrid, ui.topX, ui.topY, ui.topGridCheckbox] = createOneRow( ... + topPanel, xItems, yItems, topDefaults, valueChangedFcn); + [ui.bottomGrid, ui.bottomX, ui.bottomY, ui.bottomGridCheckbox] = createOneRow( ... + bottomPanel, xItems, yItems, bottomDefaults, valueChangedFcn); + ui.setSelections = @setSelections; + ui.swapSelections = @swapSelections; + + function setSelections(topSelection, bottomSelection) + applySelection(ui.topX, ui.topY, topSelection); + applySelection(ui.bottomX, ui.bottomY, bottomSelection); + end + + function swapSelections() + topSelection = struct('x', ui.topX.Value, 'y', ui.topY.Value); + bottomSelection = struct('x', ui.bottomX.Value, 'y', ui.bottomY.Value); + setSelections(bottomSelection, topSelection); + end +end + +function [grid, ddX, ddY, cbGrid] = createOneRow(parent, xItems, yItems, defaults, valueChangedFcn) + grid = uigridlayout(parent, [1 5]); + grid.ColumnWidth = {'fit', '1x', 'fit', '1x', '1x'}; + grid.Padding = [8 6 8 6]; + grid.ColumnSpacing = 8; + + uilabel(grid, 'Text', 'X:', 'HorizontalAlignment', 'right'); + ddX = uidropdown(grid, ... + 'Items', xItems, ... + 'Value', defaults.x, ... + 'ValueChangedFcn', valueChangedFcn); + + uilabel(grid, 'Text', 'Y:', 'HorizontalAlignment', 'right'); + ddY = uidropdown(grid, ... + 'Items', yItems, ... + 'Value', defaults.y, ... + 'ValueChangedFcn', valueChangedFcn); + + cbGrid = uicheckbox(grid, ... + 'Text', 'Grid', ... + 'Value', defaults.grid, ... + 'ValueChangedFcn', valueChangedFcn); +end + +function applySelection(ddX, ddY, selection) + if isfield(selection, 'x') && any(strcmp(ddX.Items, selection.x)) + ddX.Value = selection.x; + end + if isfield(selection, 'y') && any(strcmp(ddY.Items, selection.y)) + ddY.Value = selection.y; + end +end diff --git a/apps/electrochem/eis/+eis/+ui/runApp.m b/apps/electrochem/eis/+eis/+ui/runApp.m index 5f672976..df7c7963 100644 --- a/apps/electrochem/eis/+eis/+ui/runApp.m +++ b/apps/electrochem/eis/+eis/+ui/runApp.m @@ -58,30 +58,38 @@ lbFiles = fileUi.listbox; txtLoaded = fileUi.loadedText; - plotOptionsUi = labkit.ui.view.panel(layFA, 'plotOptions', 8, 2); + plotOptionsUi = labkit.ui.view.section(layFA, 'Plot Options', 2, [8 2]); gp = plotOptionsUi.grid; - [~, ddX] = labkit.ui.view.form(gp, 'dropdown', 'X axis:', ... - 'Items', axisItems, ... - 'Value', 'Zreal (ohm)', ... - 'ValueChangedFcn', @(~,~) refreshPlot()); - - [~, ddY] = labkit.ui.view.form(gp, 'dropdown', 'Y axis:', ... - 'Items', axisItems, ... - 'Value', '-Zimag (ohm)', ... - 'ValueChangedFcn', @(~,~) refreshPlot()); - - [~, edLineWidth] = labkit.ui.view.form(gp, 'spinner', 'Line width:', ... - 'Value', 1.4, ... - 'Limits', [0.1 10], ... - 'Step', 0.1, ... - 'ValueChangedFcn', @(~,~) refreshPlot()); - - [~, edMarkerSize] = labkit.ui.view.form(gp, 'spinner', 'Marker size:', ... - 'Value', 6, ... - 'Limits', [1 20], ... - 'Step', 1, ... - 'ValueChangedFcn', @(~,~) refreshPlot()); + [~, ddX] = labkit.ui.view.form(gp, struct( ... + 'kind', 'dropdown', ... + 'label', 'X axis:', ... + 'items', {axisItems}, ... + 'value', 'Zreal (ohm)', ... + 'callback', @(~,~) refreshPlot())); + + [~, ddY] = labkit.ui.view.form(gp, struct( ... + 'kind', 'dropdown', ... + 'label', 'Y axis:', ... + 'items', {axisItems}, ... + 'value', '-Zimag (ohm)', ... + 'callback', @(~,~) refreshPlot())); + + [~, edLineWidth] = labkit.ui.view.form(gp, struct( ... + 'kind', 'spinner', ... + 'label', 'Line width:', ... + 'value', 1.4, ... + 'limits', [0.1 10], ... + 'step', 0.1, ... + 'callback', @(~,~) refreshPlot())); + + [~, edMarkerSize] = labkit.ui.view.form(gp, struct( ... + 'kind', 'spinner', ... + 'label', 'Marker size:', ... + 'value', 6, ... + 'limits', [1 20], ... + 'step', 1, ... + 'callback', @(~,~) refreshPlot())); cbMarkers = uicheckbox(gp, ... 'Text', 'Show markers', ... diff --git a/apps/electrochem/vt_resistance/+vt_resistance/+ui/createRightAxesPair.m b/apps/electrochem/vt_resistance/+vt_resistance/+ui/createRightAxesPair.m new file mode 100644 index 00000000..6c176aba --- /dev/null +++ b/apps/electrochem/vt_resistance/+vt_resistance/+ui/createRightAxesPair.m @@ -0,0 +1,46 @@ +% App-owned VT Resistance right-side axes layout helper. Expected caller: +% vt_resistance.ui.runApp. Inputs are the shell UI struct, axes titles, and +% whether plot-control panels are needed. Output is the UI struct with +% top/bottom axes and panel fields. Side effects are limited to creating +% controls on the shell right grid. +function ui = createRightAxesPair(ui, topTitle, bottomTitle, showControls) +%CREATERIGHTAXESPAIR Create VT Resistance top/bottom plot axes. + + if showControls + ui.topControlsPanel = uipanel(ui.rightGrid, 'Title', topTitle); + ui.topControlsPanel.Layout.Row = 1; + ui.topAxes = createOneAxes(ui.rightGrid, 2, topTitle); + + ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); + ui.bottomControlsPanel.Layout.Row = 3; + ui.bottomAxes = createOneAxes(ui.rightGrid, 4, bottomTitle); + else + ui.topControlsPanel = []; + ui.bottomControlsPanel = []; + ui.topAxes = createOneAxes(ui.rightGrid, 1, topTitle); + ui.bottomAxes = createOneAxes(ui.rightGrid, 2, bottomTitle); + end +end + +function ax = createOneAxes(parent, row, titleText) + ax = uiaxes(parent); + ax.Layout.Row = row; + title(ax, titleText); + labkit.ui.view.draw(ax, 'popout'); + disableAxesInteractivity(ax); +end + +function disableAxesInteractivity(ax) + try + disableDefaultInteractivity(ax); + catch + end + try + ax.Interactions = []; + catch + end + try + ax.Toolbar.Visible = 'off'; + catch + end +end diff --git a/apps/electrochem/vt_resistance/+vt_resistance/+ui/runApp.m b/apps/electrochem/vt_resistance/+vt_resistance/+ui/runApp.m index 610e073e..362357a9 100644 --- a/apps/electrochem/vt_resistance/+vt_resistance/+ui/runApp.m +++ b/apps/electrochem/vt_resistance/+vt_resistance/+ui/runApp.m @@ -14,7 +14,12 @@ 'title', 'Gamry VT Steady Resistance GUI', ... 'position', [40 30 1680 980], ... 'leftWidth', 430, ... - 'options', struct('rightKind', 'dualPlot'))); + 'options', struct( ... + 'rightTitle', 'Plots', ... + 'rightGridSize', [4 1], ... + 'rightRowHeight', {{'fit', '1x', 'fit', '1x'}}, ... + 'rightRowSpacing', 10))); + ui = vt_resistance.ui.createRightAxesPair(ui, 'Top Plot', 'Bottom Plot', true); fig = ui.fig; layFA = ui.filesAnalysisGrid; laySR = ui.summaryResultsGrid; @@ -84,19 +89,19 @@ infoUi = labkit.ui.view.section(laySR, 'Current File Summary', 1, [13 2]); gi = infoUi.grid; - S.txtControlMode = labkit.ui.view.form(gi, 'info', 1, 'Control mode:'); - S.txtDetect = labkit.ui.view.form(gi, 'info', 2, 'Detection:'); - S.txtWindow = labkit.ui.view.form(gi, 'info', 3, 'Window:'); - S.txtCathIV = labkit.ui.view.form(gi, 'info', 4, 'Cathodic I / Vss:'); - S.txtAnodIV = labkit.ui.view.form(gi, 'info', 5, 'Anodic I / Vss:'); - S.txtCathBase = labkit.ui.view.form(gi, 'info', 6, 'Cathodic baseline:'); - S.txtAnodBase = labkit.ui.view.form(gi, 'info', 7, 'Anodic baseline:'); - S.txtCathBaseWin = labkit.ui.view.form(gi, 'info', 8, 'Cath baseline window:'); - S.txtAnodBaseWin = labkit.ui.view.form(gi, 'info', 9, 'Anod baseline window:'); - S.txtCathR = labkit.ui.view.form(gi, 'info', 10, 'Cathodic R:'); - S.txtAnodR = labkit.ui.view.form(gi, 'info', 11, 'Anodic R:'); - S.txtAvgR = labkit.ui.view.form(gi, 'info', 12, 'Average R:'); - S.txtStatus = labkit.ui.view.form(gi, 'info', 13, 'Status:'); + S.txtControlMode = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 1, 'label', 'Control mode:')); + S.txtDetect = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 2, 'label', 'Detection:')); + S.txtWindow = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 3, 'label', 'Window:')); + S.txtCathIV = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 4, 'label', 'Cathodic I / Vss:')); + S.txtAnodIV = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 5, 'label', 'Anodic I / Vss:')); + S.txtCathBase = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 6, 'label', 'Cathodic baseline:')); + S.txtAnodBase = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 7, 'label', 'Anodic baseline:')); + S.txtCathBaseWin = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 8, 'label', 'Cath baseline window:')); + S.txtAnodBaseWin = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 9, 'label', 'Anod baseline window:')); + S.txtCathR = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 10, 'label', 'Cathodic R:')); + S.txtAnodR = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 11, 'label', 'Anodic R:')); + S.txtAvgR = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 12, 'label', 'Average R:')); + S.txtStatus = labkit.ui.view.form(gi, struct('kind', 'info', 'row', 13, 'label', 'Status:')); tableUi = labkit.ui.view.panel(laySR, 'table', 'Batch Results', 2, ... {'File','Ic(A)','Ia(A)','Vc_ss(V)','Va_ss(V)','R_cath(ohm)','R_anod(ohm)','R_avg(ohm)','Detection'}, ... @@ -108,9 +113,8 @@ topPlotDefaults = struct('x', 'Time (s)', 'y', 'VT: Vf vs time', 'grid', true); bottomPlotDefaults = struct('x', 'Time (s)', 'y', 'IT: Im vs time', 'grid', true); - plotControls = labkit.ui.view.panel( ... + plotControls = vt_resistance.ui.topBottomPlotControls( ... ui.topControlsPanel, ... - 'topBottomPlotControls', ... ui.bottomControlsPanel, ... {'Time (s)', 'Sample #'}, ... {'VT: Vf vs time', 'IT: Im vs time'}, ... @@ -442,7 +446,7 @@ function plotOneAxis(ax, A, xChoice, yChoice, showGrid) end function swapPlots() - labkit.ui.view.update(plotControls, 'swapPlotSelections'); + plotControls.swapSelections(); refreshPlots(); end @@ -452,8 +456,7 @@ function resetAxes() end function restoreDefaultPlotSelections() - labkit.ui.view.update(plotControls, 'setPlotSelections', ... - topPlotDefaults, bottomPlotDefaults); + plotControls.setSelections(topPlotDefaults, bottomPlotDefaults); end function resetAxesToDefaultState() diff --git a/apps/electrochem/vt_resistance/+vt_resistance/+ui/topBottomPlotControls.m b/apps/electrochem/vt_resistance/+vt_resistance/+ui/topBottomPlotControls.m new file mode 100644 index 00000000..e539784e --- /dev/null +++ b/apps/electrochem/vt_resistance/+vt_resistance/+ui/topBottomPlotControls.m @@ -0,0 +1,64 @@ +% App-owned VT Resistance top/bottom plot controls helper. Expected caller: +% vt_resistance.ui.runApp. Inputs are parent panels, axis items, default +% selections, and a value-change callback. Output is a controls struct with +% handles plus setSelections/swapSelections closures. Side effects are limited +% to creating controls on the supplied panels. +function ui = topBottomPlotControls(topPanel, bottomPanel, xItems, yItems, topDefaults, bottomDefaults, valueChangedFcn) +%TOPBOTTOMPLOTCONTROLS Create VT Resistance top/bottom plot controls. + + if nargin < 7 + valueChangedFcn = []; + end + + ui = struct(); + [ui.topGrid, ui.topX, ui.topY, ui.topGridCheckbox] = createOneRow( ... + topPanel, xItems, yItems, topDefaults, valueChangedFcn); + [ui.bottomGrid, ui.bottomX, ui.bottomY, ui.bottomGridCheckbox] = createOneRow( ... + bottomPanel, xItems, yItems, bottomDefaults, valueChangedFcn); + ui.setSelections = @setSelections; + ui.swapSelections = @swapSelections; + + function setSelections(topSelection, bottomSelection) + applySelection(ui.topX, ui.topY, topSelection); + applySelection(ui.bottomX, ui.bottomY, bottomSelection); + end + + function swapSelections() + topSelection = struct('x', ui.topX.Value, 'y', ui.topY.Value); + bottomSelection = struct('x', ui.bottomX.Value, 'y', ui.bottomY.Value); + setSelections(bottomSelection, topSelection); + end +end + +function [grid, ddX, ddY, cbGrid] = createOneRow(parent, xItems, yItems, defaults, valueChangedFcn) + grid = uigridlayout(parent, [1 5]); + grid.ColumnWidth = {'fit', '1x', 'fit', '1x', '1x'}; + grid.Padding = [8 6 8 6]; + grid.ColumnSpacing = 8; + + uilabel(grid, 'Text', 'X:', 'HorizontalAlignment', 'right'); + ddX = uidropdown(grid, ... + 'Items', xItems, ... + 'Value', defaults.x, ... + 'ValueChangedFcn', valueChangedFcn); + + uilabel(grid, 'Text', 'Y:', 'HorizontalAlignment', 'right'); + ddY = uidropdown(grid, ... + 'Items', yItems, ... + 'Value', defaults.y, ... + 'ValueChangedFcn', valueChangedFcn); + + cbGrid = uicheckbox(grid, ... + 'Text', 'Grid', ... + 'Value', defaults.grid, ... + 'ValueChangedFcn', valueChangedFcn); +end + +function applySelection(ddX, ddY, selection) + if isfield(selection, 'x') && any(strcmp(ddX.Items, selection.x)) + ddX.Value = selection.x; + end + if isfield(selection, 'y') && any(strcmp(ddY.Items, selection.y)) + ddY.Value = selection.y; + end +end diff --git a/apps/image_measurement/batch_crop/+batch_crop/+ui/createControls.m b/apps/image_measurement/batch_crop/+batch_crop/+ui/createControls.m index 6deca155..f33fb788 100644 --- a/apps/image_measurement/batch_crop/+batch_crop/+ui/createControls.m +++ b/apps/image_measurement/batch_crop/+batch_crop/+ui/createControls.m @@ -21,8 +21,9 @@ controls.btnClearImages.Layout.Row = 1; controls.btnClearImages.Layout.Column = 2; - controls.txtImageSource = labkit.ui.view.form(fileGrid, 'readonly', ... - 'Value', 'No images loaded'); + controls.txtImageSource = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No images loaded')); controls.txtImageSource.Layout.Row = 2; controls.txtImageSource.Layout.Column = [1 2]; @@ -44,8 +45,9 @@ controls.btnNext.Layout.Row = 4; controls.btnNext.Layout.Column = 2; - controls.txtImageStatus = labkit.ui.view.form(fileGrid, 'readonly', ... - 'Value', 'Images: 0'); + controls.txtImageStatus = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'Images: 0')); controls.txtImageStatus.Layout.Row = 5; controls.txtImageStatus.Layout.Column = [1 2]; @@ -54,38 +56,59 @@ struct('rowHeight', {cropRows}, 'columnWidth', {{145, '1x'}})); cropGrid = cropPanel.grid; - [lblWidth, controls.edtCropWidth] = labkit.ui.view.form(cropGrid, 'spinner', ... - 'Width (px):', 'Value', 1024, 'Limits', [1 Inf], 'Step', 1, ... - 'ValueChangedFcn', callbacks.onCropGeometryChanged); + [lblWidth, controls.edtCropWidth] = labkit.ui.view.form(cropGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Width (px):', ... + 'value', 1024, ... + 'limits', [1 Inf], ... + 'step', 1, ... + 'callback', callbacks.onCropGeometryChanged)); placeLabeled(lblWidth, controls.edtCropWidth, 1); - [lblHeight, controls.edtCropHeight] = labkit.ui.view.form(cropGrid, 'spinner', ... - 'Height (px):', 'Value', 1024, 'Limits', [1 Inf], 'Step', 1, ... - 'ValueChangedFcn', callbacks.onCropGeometryChanged); + [lblHeight, controls.edtCropHeight] = labkit.ui.view.form(cropGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Height (px):', ... + 'value', 1024, ... + 'limits', [1 Inf], ... + 'step', 1, ... + 'callback', callbacks.onCropGeometryChanged)); placeLabeled(lblHeight, controls.edtCropHeight, 2); - [lblRotation, controls.edtRotation] = labkit.ui.view.form(cropGrid, 'spinner', ... - 'Rotation (deg):', 'Value', 0, 'Limits', [-180 180], 'Step', 0.5, ... - 'ValueChangedFcn', callbacks.onRotationChanged); + [lblRotation, controls.edtRotation] = labkit.ui.view.form(cropGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Rotation (deg):', ... + 'value', 0, ... + 'limits', [-180 180], ... + 'step', 0.5, ... + 'callback', callbacks.onRotationChanged)); placeLabeled(lblRotation, controls.edtRotation, 3); - [lblFill, controls.ddFillMode] = labkit.ui.view.form(cropGrid, 'dropdown', ... - 'Fill:', ... - 'Items', {'Black', 'White'}, ... - 'Value', 'Black', ... - 'ValueChangedFcn', callbacks.onFillModeChanged); + [lblFill, controls.ddFillMode] = labkit.ui.view.form(cropGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Fill:', ... + 'items', {{'Black', 'White'}}, ... + 'value', 'Black', ... + 'callback', callbacks.onFillModeChanged)); placeLabeled(lblFill, controls.ddFillMode, 4); - [lblCenterX, controls.edtCenterX] = labkit.ui.view.form(cropGrid, 'spinner', ... - 'Center X:', 'Value', 1, 'Limits', [1 Inf], 'Step', 1, ... - 'Enable', 'off', ... - 'ValueChangedFcn', callbacks.onCenterChanged); + [lblCenterX, controls.edtCenterX] = labkit.ui.view.form(cropGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Center X:', ... + 'value', 1, ... + 'limits', [1 Inf], ... + 'step', 1, ... + 'enabled', false, ... + 'callback', callbacks.onCenterChanged)); placeLabeled(lblCenterX, controls.edtCenterX, 5); - [lblCenterY, controls.edtCenterY] = labkit.ui.view.form(cropGrid, 'spinner', ... - 'Center Y:', 'Value', 1, 'Limits', [1 Inf], 'Step', 1, ... - 'Enable', 'off', ... - 'ValueChangedFcn', callbacks.onCenterChanged); + [lblCenterY, controls.edtCenterY] = labkit.ui.view.form(cropGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Center Y:', ... + 'value', 1, ... + 'limits', [1 Inf], ... + 'step', 1, ... + 'enabled', false, ... + 'callback', callbacks.onCenterChanged)); placeLabeled(lblCenterY, controls.edtCenterY, 6); controls.btnUseCanvasCenter = uibutton(cropGrid, 'Text', 'Use canvas center', ... @@ -99,18 +122,20 @@ struct('rowHeight', {exportRows}, 'columnWidth', {{145, '1x'}})); exportGrid = exportPanel.grid; - [lblFormat, controls.ddFormat] = labkit.ui.view.form(exportGrid, 'dropdown', ... - 'Format:', ... - 'Items', {'PNG', 'TIFF', 'JPEG'}, ... - 'Value', 'PNG', ... - 'ValueChangedFcn', callbacks.onExportSettingChanged); + [lblFormat, controls.ddFormat] = labkit.ui.view.form(exportGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Format:', ... + 'items', {{'PNG', 'TIFF', 'JPEG'}}, ... + 'value', 'PNG', ... + 'callback', callbacks.onExportSettingChanged)); lblFormat.Layout.Row = 1; lblFormat.Layout.Column = 1; controls.ddFormat.Layout.Row = 1; controls.ddFormat.Layout.Column = 2; - controls.txtOutputFolder = labkit.ui.view.form(exportGrid, 'readonly', ... - 'Value', char(initialOutputFolder)); + controls.txtOutputFolder = labkit.ui.view.form(exportGrid, struct( ... + 'kind', 'readonly', ... + 'value', char(initialOutputFolder))); controls.txtOutputFolder.Layout.Row = 2; controls.txtOutputFolder.Layout.Column = [1 2]; diff --git a/apps/image_measurement/batch_crop/labkit_BatchImageCrop_app.m b/apps/image_measurement/batch_crop/labkit_BatchImageCrop_app.m index 0ea98609..7efd3f25 100644 --- a/apps/image_measurement/batch_crop/labkit_BatchImageCrop_app.m +++ b/apps/image_measurement/batch_crop/labkit_BatchImageCrop_app.m @@ -24,11 +24,9 @@ S.lastExport = []; workbenchOpts = struct( ... - 'rightKind', 'custom', ... 'rightTitle', 'Crop Preview', ... 'rightGridSize', [1 1], ... - 'rightRowHeight', {{'1x'}}, ... - 'showPlotControls', false); + 'rightRowHeight', {{'1x'}}); workbenchOpts.tabs = [ ... labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [3 1], ... {250, 260, 145}, ... diff --git a/apps/image_measurement/curvature/+curvature/+ui/shellOptions.m b/apps/image_measurement/curvature/+curvature/+ui/appShellOptions.m similarity index 89% rename from apps/image_measurement/curvature/+curvature/+ui/shellOptions.m rename to apps/image_measurement/curvature/+curvature/+ui/appShellOptions.m index b5f5fa87..14ec261d 100644 --- a/apps/image_measurement/curvature/+curvature/+ui/shellOptions.m +++ b/apps/image_measurement/curvature/+curvature/+ui/appShellOptions.m @@ -1,8 +1,8 @@ % App-owned curvature shell options helper. Expected caller: % labkit_CurvatureMeasurement_app. Output is the createShell options struct. % Encodes only layout constants and has no side effects. -function opts = shellOptions() -%SHELLOPTIONS Return shell options for the curvature app. +function opts = appShellOptions() +%APPSHELLOPTIONS Return createShell options for the curvature app. opts = struct( ... 'rightTitle', 'Measurement Preview', ... diff --git a/apps/image_measurement/curvature/+curvature/+ui/createControls.m b/apps/image_measurement/curvature/+curvature/+ui/createControls.m index 1fd18e6a..31717939 100644 --- a/apps/image_measurement/curvature/+curvature/+ui/createControls.m +++ b/apps/image_measurement/curvature/+curvature/+ui/createControls.m @@ -15,13 +15,15 @@ btnOpenImage.Layout.Row = 1; btnOpenImage.Layout.Column = [1 2]; - controls.txtImage = labkit.ui.view.form(imageGrid, 'readonly', ... - 'Value', 'No image loaded'); + controls.txtImage = labkit.ui.view.form(imageGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No image loaded')); controls.txtImage.Layout.Row = 2; controls.txtImage.Layout.Column = [1 2]; - controls.txtPointCount = labkit.ui.view.form(imageGrid, 'readonly', ... - 'Value', 'Points: 0'); + controls.txtPointCount = labkit.ui.view.form(imageGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'Points: 0')); controls.txtPointCount.Layout.Row = 3; controls.txtPointCount.Layout.Column = [1 2]; @@ -65,8 +67,12 @@ controls.chkDensify.Layout.Row = 1; controls.chkDensify.Layout.Column = [1 2]; - [lblDenseN, controls.edtDenseN] = labkit.ui.view.form(fitGrid, 'spinner', ... - 'Dense point count:', 'Value', 300, 'Limits', [3 Inf], 'Step', 25); + [lblDenseN, controls.edtDenseN] = labkit.ui.view.form(fitGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Dense point count:', ... + 'value', 300, ... + 'limits', [3 Inf], ... + 'step', 25)); lblDenseN.Layout.Row = 2; lblDenseN.Layout.Column = 1; controls.edtDenseN.Layout.Row = 2; diff --git a/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m b/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m index 37d8caed..18be3052 100644 --- a/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m +++ b/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m @@ -31,7 +31,7 @@ 'title', 'Image Curvature Measurement', ... 'position', [90 70 1420 860], ... 'leftWidth', 390, ... - 'options', curvature.ui.shellOptions())); + 'options', curvature.ui.appShellOptions())); fig = ui.fig; layFA = ui.filesAnalysisGrid; laySR = ui.summaryResultsGrid; diff --git a/apps/image_measurement/focus_stack/+focus_stack/+ui/createRightAxesPair.m b/apps/image_measurement/focus_stack/+focus_stack/+ui/createRightAxesPair.m new file mode 100644 index 00000000..041af695 --- /dev/null +++ b/apps/image_measurement/focus_stack/+focus_stack/+ui/createRightAxesPair.m @@ -0,0 +1,46 @@ +% App-owned focus-stack preview layout helper. Expected caller: +% labkit_FocusStack_app. Inputs are the shell UI struct, axes titles, and +% whether plot-control panels are needed. Output is the UI struct with +% top/bottom axes and panel fields. Side effects are limited to creating axes +% and optional panels on the shell right grid. +function ui = createRightAxesPair(ui, topTitle, bottomTitle, showControls) +%CREATERIGHTAXESPAIR Create focus-stack preview axes. + + if showControls + ui.topControlsPanel = uipanel(ui.rightGrid, 'Title', topTitle); + ui.topControlsPanel.Layout.Row = 1; + ui.topAxes = createOneAxes(ui.rightGrid, 2, topTitle); + + ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); + ui.bottomControlsPanel.Layout.Row = 3; + ui.bottomAxes = createOneAxes(ui.rightGrid, 4, bottomTitle); + else + ui.topControlsPanel = []; + ui.bottomControlsPanel = []; + ui.topAxes = createOneAxes(ui.rightGrid, 1, topTitle); + ui.bottomAxes = createOneAxes(ui.rightGrid, 2, bottomTitle); + end +end + +function ax = createOneAxes(parent, row, titleText) + ax = uiaxes(parent); + ax.Layout.Row = row; + title(ax, titleText); + labkit.ui.view.draw(ax, 'popout'); + disableAxesInteractivity(ax); +end + +function disableAxesInteractivity(ax) + try + disableDefaultInteractivity(ax); + catch + end + try + ax.Interactions = []; + catch + end + try + ax.Toolbar.Visible = 'off'; + catch + end +end diff --git a/apps/image_measurement/focus_stack/labkit_FocusStack_app.m b/apps/image_measurement/focus_stack/labkit_FocusStack_app.m index 9b8c6987..9246a0f0 100644 --- a/apps/image_measurement/focus_stack/labkit_FocusStack_app.m +++ b/apps/image_measurement/focus_stack/labkit_FocusStack_app.m @@ -25,11 +25,11 @@ S.registrationLines = {}; S.result = focus_stack.state.emptyResult(); - workbenchOpts = struct('rightKind', 'dualPlot', ... + workbenchOpts = struct( ... 'rightTitle', 'Focus Stack Preview', ... - 'topPlotTitle', 'Fused all-in-focus image', ... - 'bottomPlotTitle', 'Focus-depth index map', ... - 'showPlotControls', false); + 'rightGridSize', [2 1], ... + 'rightRowHeight', {{'1x', '1x'}}, ... + 'rightRowSpacing', 10); workbenchOpts.tabs = [ ... labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ... {250, 235, 185, 170}, ... @@ -45,6 +45,8 @@ 'position', [80 60 1440 860], ... 'leftWidth', 390, ... 'options', workbenchOpts)); + ui = focus_stack.ui.createRightAxesPair(ui, ... + 'Fused all-in-focus image', 'Focus-depth index map', false); fig = ui.fig; layFA = ui.filesAnalysisGrid; laySR = ui.summaryResultsGrid; @@ -65,8 +67,9 @@ btnOpenFiles.Layout.Row = 1; btnOpenFiles.Layout.Column = 2; - txtFolder = labkit.ui.view.form(fileGrid, 'readonly', ... - 'Value', 'No images loaded'); + txtFolder = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No images loaded')); txtFolder.Layout.Row = 2; txtFolder.Layout.Column = [1 2]; @@ -74,8 +77,9 @@ lbImages.Layout.Row = 3; lbImages.Layout.Column = [1 2]; - txtStackStatus = labkit.ui.view.form(fileGrid, 'readonly', ... - 'Value', 'Images: 0'); + txtStackStatus = labkit.ui.view.form(fileGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'Images: 0')); txtStackStatus.Layout.Row = 4; txtStackStatus.Layout.Column = [1 2]; @@ -84,11 +88,12 @@ 'columnWidth', {{155, '1x'}})); analysisGrid = analysisPanel.grid; - [lblFusionPreset, ddFusionPreset] = labkit.ui.view.form(analysisGrid, 'dropdown', ... - 'Preset:', ... - 'Items', {'Balanced', 'Crisp details', 'Smooth transitions', 'Noisy images'}, ... - 'Value', 'Balanced', ... - 'ValueChangedFcn', @onFusionPresetChanged); + [lblFusionPreset, ddFusionPreset] = labkit.ui.view.form(analysisGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Preset:', ... + 'items', {{'Balanced', 'Crisp details', 'Smooth transitions', 'Noisy images'}}, ... + 'value', 'Balanced', ... + 'callback', @onFusionPresetChanged)); lblFusionPreset.Layout.Row = 1; lblFusionPreset.Layout.Column = 1; ddFusionPreset.Layout.Row = 1; @@ -100,22 +105,34 @@ chkRegister.Layout.Row = 2; chkRegister.Layout.Column = [1 2]; - [lblFocusWindow, edtFocusWindow] = labkit.ui.view.form(analysisGrid, 'spinner', ... - 'Detail scale (px):', 'Value', 31, 'Limits', [3 99], 'Step', 2); + [lblFocusWindow, edtFocusWindow] = labkit.ui.view.form(analysisGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Detail scale (px):', ... + 'value', 31, ... + 'limits', [3 99], ... + 'step', 2)); lblFocusWindow.Layout.Row = 3; lblFocusWindow.Layout.Column = 1; edtFocusWindow.Layout.Row = 3; edtFocusWindow.Layout.Column = 2; - [lblSmoothRadius, edtSmoothRadius] = labkit.ui.view.form(analysisGrid, 'spinner', ... - 'Blend radius (px):', 'Value', 4, 'Limits', [0 50], 'Step', 1); + [lblSmoothRadius, edtSmoothRadius] = labkit.ui.view.form(analysisGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Blend radius (px):', ... + 'value', 4, ... + 'limits', [0 50], ... + 'step', 1)); lblSmoothRadius.Layout.Row = 4; lblSmoothRadius.Layout.Column = 1; edtSmoothRadius.Layout.Row = 4; edtSmoothRadius.Layout.Column = 2; - [lblUncertainBlend, edtUncertainBlend] = labkit.ui.view.form(analysisGrid, 'spinner', ... - 'Uncertain blend (%):', 'Value', 5, 'Limits', [0 100], 'Step', 1); + [lblUncertainBlend, edtUncertainBlend] = labkit.ui.view.form(analysisGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Uncertain blend (%):', ... + 'value', 5, ... + 'limits', [0 100], ... + 'step', 1)); lblUncertainBlend.Layout.Row = 5; lblUncertainBlend.Layout.Column = 1; edtUncertainBlend.Layout.Row = 5; diff --git a/apps/wearable/ecg_print/+ecg_print/+ui/createControls.m b/apps/wearable/ecg_print/+ecg_print/+ui/createControls.m index 559a9be0..d851e008 100644 --- a/apps/wearable/ecg_print/+ecg_print/+ui/createControls.m +++ b/apps/wearable/ecg_print/+ecg_print/+ui/createControls.m @@ -20,8 +20,9 @@ btnOpen.Layout.Row = 1; btnOpen.Layout.Column = [1 2]; - txtFile = labkit.ui.view.form(recordingGrid, 'readonly', ... - 'Value', 'No file loaded'); + txtFile = labkit.ui.view.form(recordingGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'No file loaded')); txtFile.Layout.Row = 2; txtFile.Layout.Column = [1 2]; @@ -35,58 +36,75 @@ 'columnWidth', {{135, '1x'}})); importGrid = importPanel.grid; - txtImportStatus = labkit.ui.view.form(importGrid, 'readonly', ... - 'Value', 'Open a recording to inspect import settings.'); + txtImportStatus = labkit.ui.view.form(importGrid, struct( ... + 'kind', 'readonly', ... + 'value', 'Open a recording to inspect import settings.')); txtImportStatus.Layout.Row = 1; txtImportStatus.Layout.Column = [1 2]; - [lblHeaderLine, edtHeaderLine] = labkit.ui.view.form(importGrid, 'spinner', ... - 'CSV header line:', 'Value', 0, 'Limits', [0 Inf], 'Step', 1, ... - 'ValueChangedFcn', callbacks.onImportOptionChanged); + [lblHeaderLine, edtHeaderLine] = labkit.ui.view.form(importGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'CSV header line:', ... + 'value', 0, ... + 'limits', [0 Inf], ... + 'step', 1, ... + 'callback', callbacks.onImportOptionChanged)); lblHeaderLine.Layout.Row = 2; lblHeaderLine.Layout.Column = 1; edtHeaderLine.Layout.Row = 2; edtHeaderLine.Layout.Column = 2; - [lblHasHeader, ddHasHeader] = labkit.ui.view.form(importGrid, 'dropdown', ... - 'CSV header:', ... - 'Items', {'Auto', 'Yes', 'No'}, ... - 'Value', 'Auto', ... - 'ValueChangedFcn', callbacks.onImportOptionChanged); + [lblHasHeader, ddHasHeader] = labkit.ui.view.form(importGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'CSV header:', ... + 'items', {{'Auto', 'Yes', 'No'}}, ... + 'value', 'Auto', ... + 'callback', callbacks.onImportOptionChanged)); lblHasHeader.Layout.Row = 3; lblHasHeader.Layout.Column = 1; ddHasHeader.Layout.Row = 3; ddHasHeader.Layout.Column = 2; - [lblTimeColumn, edtTimeColumn] = labkit.ui.view.form(importGrid, 'edit', ... - 'Time column:', 'text', 'Value', '', ... - 'ValueChangedFcn', callbacks.onImportOptionChanged); + [lblTimeColumn, edtTimeColumn] = labkit.ui.view.form(importGrid, struct( ... + 'kind', 'edit', ... + 'label', 'Time column:', ... + 'style', 'text', ... + 'value', '', ... + 'callback', callbacks.onImportOptionChanged)); lblTimeColumn.Layout.Row = 4; lblTimeColumn.Layout.Column = 1; edtTimeColumn.Layout.Row = 4; edtTimeColumn.Layout.Column = 2; - [lblTimeUnit, ddTimeUnit] = labkit.ui.view.form(importGrid, 'dropdown', ... - 'Time unit:', ... - 'Items', {'Auto', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'}, ... - 'Value', 'Auto', ... - 'ValueChangedFcn', callbacks.onImportOptionChanged); + [lblTimeUnit, ddTimeUnit] = labkit.ui.view.form(importGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Time unit:', ... + 'items', {{'Auto', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'}}, ... + 'value', 'Auto', ... + 'callback', callbacks.onImportOptionChanged)); lblTimeUnit.Layout.Row = 5; lblTimeUnit.Layout.Column = 1; ddTimeUnit.Layout.Row = 5; ddTimeUnit.Layout.Column = 2; - [lblSignalColumns, edtSignalColumns] = labkit.ui.view.form(importGrid, 'edit', ... - 'Signal columns:', 'text', 'Value', '', ... - 'ValueChangedFcn', callbacks.onImportOptionChanged); + [lblSignalColumns, edtSignalColumns] = labkit.ui.view.form(importGrid, struct( ... + 'kind', 'edit', ... + 'label', 'Signal columns:', ... + 'style', 'text', ... + 'value', '', ... + 'callback', callbacks.onImportOptionChanged)); lblSignalColumns.Layout.Row = 6; lblSignalColumns.Layout.Column = 1; edtSignalColumns.Layout.Row = 6; edtSignalColumns.Layout.Column = 2; - [lblFallbackFs, edtFallbackFs] = labkit.ui.view.form(importGrid, 'spinner', ... - 'Fallback Fs:', 'Value', 2000, 'Limits', [0 Inf], 'Step', 100, ... - 'ValueChangedFcn', callbacks.onImportOptionChanged); + [lblFallbackFs, edtFallbackFs] = labkit.ui.view.form(importGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Fallback Fs:', ... + 'value', 2000, ... + 'limits', [0 Inf], ... + 'step', 100, ... + 'callback', callbacks.onImportOptionChanged)); lblFallbackFs.Layout.Row = 7; lblFallbackFs.Layout.Column = 1; edtFallbackFs.Layout.Row = 7; @@ -102,23 +120,34 @@ 'columnWidth', {{135, '1x'}})); channelGrid = channelPanel.grid; - [lblChannel, ddChannel] = labkit.ui.view.form(channelGrid, 'dropdown', ... - 'Channel:', 'Items', {'(none)'}, 'Value', '(none)', ... - 'ValueChangedFcn', callbacks.onChannelChanged); + [lblChannel, ddChannel] = labkit.ui.view.form(channelGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Channel:', ... + 'items', {{'(none)'}}, ... + 'value', '(none)', ... + 'callback', callbacks.onChannelChanged)); lblChannel.Layout.Row = 1; lblChannel.Layout.Column = 1; ddChannel.Layout.Row = 1; ddChannel.Layout.Column = 2; - [lblStart, edtStart] = labkit.ui.view.form(channelGrid, 'spinner', ... - 'ROI start (s):', 'Value', 0, 'Limits', [0 Inf], 'Step', 1); + [lblStart, edtStart] = labkit.ui.view.form(channelGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'ROI start (s):', ... + 'value', 0, ... + 'limits', [0 Inf], ... + 'step', 1)); lblStart.Layout.Row = 2; lblStart.Layout.Column = 1; edtStart.Layout.Row = 2; edtStart.Layout.Column = 2; - [lblEnd, edtEnd] = labkit.ui.view.form(channelGrid, 'spinner', ... - 'ROI end (s):', 'Value', 0, 'Limits', [0 Inf], 'Step', 1); + [lblEnd, edtEnd] = labkit.ui.view.form(channelGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'ROI end (s):', ... + 'value', 0, ... + 'limits', [0 Inf], ... + 'step', 1)); lblEnd.Layout.Row = 3; lblEnd.Layout.Column = 1; edtEnd.Layout.Row = 3; @@ -129,63 +158,89 @@ 'columnWidth', {{135, '1x'}})); procGrid = procPanel.grid; - [lblLow, edtLow] = labkit.ui.view.form(procGrid, 'spinner', ... - 'Bandpass low Hz:', 'Value', 0.5, 'Limits', [0 Inf], 'Step', 0.1); + [lblLow, edtLow] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Bandpass low Hz:', ... + 'value', 0.5, ... + 'limits', [0 Inf], ... + 'step', 0.1)); lblLow.Layout.Row = 1; lblLow.Layout.Column = 1; edtLow.Layout.Row = 1; edtLow.Layout.Column = 2; - [lblHigh, edtHigh] = labkit.ui.view.form(procGrid, 'spinner', ... - 'Bandpass high Hz:', 'Value', 40, 'Limits', [0 Inf], 'Step', 1); + [lblHigh, edtHigh] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Bandpass high Hz:', ... + 'value', 40, ... + 'limits', [0 Inf], ... + 'step', 1)); lblHigh.Layout.Row = 2; lblHigh.Layout.Column = 1; edtHigh.Layout.Row = 2; edtHigh.Layout.Column = 2; - [lblPeakMethod, ddPeakMethod] = labkit.ui.view.form(procGrid, 'dropdown', ... - 'Peak method:', ... - 'Items', {'QRS streaming', 'Pan-Tompkins', 'Local peaks'}, ... - 'Value', 'QRS streaming'); + [lblPeakMethod, ddPeakMethod] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Peak method:', ... + 'items', {{'QRS streaming', 'Pan-Tompkins', 'Local peaks'}}, ... + 'value', 'QRS streaming')); lblPeakMethod.Layout.Row = 3; lblPeakMethod.Layout.Column = 1; ddPeakMethod.Layout.Row = 3; ddPeakMethod.Layout.Column = 2; - [lblPeakDist, edtPeakDist] = labkit.ui.view.form(procGrid, 'spinner', ... - 'Peak distance (s):', 'Value', 0.28, 'Limits', [0.01 Inf], 'Step', 0.01); + [lblPeakDist, edtPeakDist] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Peak distance (s):', ... + 'value', 0.28, ... + 'limits', [0.01 Inf], ... + 'step', 0.01)); lblPeakDist.Layout.Row = 4; lblPeakDist.Layout.Column = 1; edtPeakDist.Layout.Row = 4; edtPeakDist.Layout.Column = 2; - [lblWin, edtWin] = labkit.ui.view.form(procGrid, 'spinner', ... - 'Segment half win (s):', 'Value', 0.7, 'Limits', [0.01 Inf], 'Step', 0.05); + [lblWin, edtWin] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Segment half win (s):', ... + 'value', 0.7, ... + 'limits', [0.01 Inf], ... + 'step', 0.05)); lblWin.Layout.Row = 5; lblWin.Layout.Column = 1; edtWin.Layout.Row = 5; edtWin.Layout.Column = 2; - [lblTopN, edtTopN] = labkit.ui.view.form(procGrid, 'spinner', ... - 'Template top N:', 'Value', 30, 'Limits', [1 Inf], 'Step', 1); + [lblTopN, edtTopN] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Template top N:', ... + 'value', 30, ... + 'limits', [1 Inf], ... + 'step', 1)); lblTopN.Layout.Row = 6; lblTopN.Layout.Column = 1; edtTopN.Layout.Row = 6; edtTopN.Layout.Column = 2; - [lblSmooth, edtSmooth] = labkit.ui.view.form(procGrid, 'spinner', ... - 'Smooth beats:', 'Value', 15, 'Limits', [1 Inf], 'Step', 1, ... - 'ValueChangedFcn', callbacks.onRefreshPlots); + [lblSmooth, edtSmooth] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'spinner', ... + 'label', 'Smooth beats:', ... + 'value', 15, ... + 'limits', [1 Inf], ... + 'step', 1, ... + 'callback', callbacks.onRefreshPlots)); lblSmooth.Layout.Row = 7; lblSmooth.Layout.Column = 1; edtSmooth.Layout.Row = 7; edtSmooth.Layout.Column = 2; - [lblView, ddTemplateView] = labkit.ui.view.form(procGrid, 'dropdown', ... - 'Template plot:', ... - 'Items', {'Template + residual band', 'Template + segments'}, ... - 'Value', 'Template + residual band', ... - 'ValueChangedFcn', callbacks.onRefreshPlots); + [lblView, ddTemplateView] = labkit.ui.view.form(procGrid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Template plot:', ... + 'items', {{'Template + residual band', 'Template + segments'}}, ... + 'value', 'Template + residual band', ... + 'callback', callbacks.onRefreshPlots)); lblView.Layout.Row = 8; lblView.Layout.Column = 1; ddTemplateView.Layout.Row = 8; diff --git a/docs/ui.md b/docs/ui.md index f9dc152d..09be33fe 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -16,7 +16,10 @@ The root `labkit.ui.*` flat helper surface has been removed. Apps should call th Every app should start from `labkit.ui.app.createShell`: ```matlab -opts = struct('rightKind', 'dualPlot'); +opts = struct( ... + 'rightTitle', 'Preview', ... + 'rightGridSize', [1 1], ... + 'rightRowHeight', {{'1x'}}); ui = labkit.ui.app.createShell(struct( ... 'title', 'Example App', ... 'position', [90 70 1200 800], ... @@ -41,7 +44,7 @@ opts.tabs = labkit.ui.app.tab( ... struct('resizeRows', [1 2 3])); ``` -The shell owns split panes, scrollable tab grids, and row resize handles. Apps own the controls placed inside returned grids. +The shell owns split panes, scrollable tab grids, row resize handles, and the right-side grid. Apps own the controls and axes placed inside returned grids. ## Views And Forms @@ -55,24 +58,35 @@ grid = section.grid; Use `labkit.ui.view.form` as the single public control entry point. It replaces separate labeled spinner/dropdown/edit/read-only helpers: ```matlab -[lblMode, ddMode] = labkit.ui.view.form(grid, 'dropdown', ... - 'Mode:', 'Items', {'Auto', 'Manual'}, 'Value', 'Auto', ... - 'ValueChangedFcn', @onModeChanged); - -[lblN, edN] = labkit.ui.view.form(grid, 'spinner', ... - 'Samples:', 'Value', 10, 'Limits', [1 Inf], 'Step', 1); - -txtStatus = labkit.ui.view.form(grid, 'readonly', ... - 'Value', 'No file loaded'); - -txtMetric = labkit.ui.view.form(grid, 'info', 3, 'Current value:'); +[lblMode, ddMode] = labkit.ui.view.form(grid, struct( ... + 'kind', 'dropdown', ... + 'label', 'Mode:', ... + 'items', {{'Auto', 'Manual'}}, ... + 'value', 'Auto', ... + 'callback', @onModeChanged)); + +[lblN, edN] = labkit.ui.view.form(grid, struct( ... + 'kind', 'spinner', ... + 'label', 'Samples:', ... + 'value', 10, ... + 'limits', [1 Inf], ... + 'step', 1)); + +txtStatus = labkit.ui.view.form(grid, struct( ... + 'kind', 'readonly', ... + 'value', 'No file loaded')); + +txtMetric = labkit.ui.view.form(grid, struct( ... + 'kind', 'info', ... + 'row', 3, ... + 'label', 'Current value:')); ``` `form` also accepts a section spec with `title`, `row`, `layout`, and `controls`. The returned struct exposes `controls`, `labels`, `setValue(id,value,reason)`, and `getValue(id)`. `setValue` no-ops for unchanged values and suppresses app-facing semantic callbacks for internal/programmatic updates. When manually placing a component in a shell tab grid, use `labkit.ui.view.place(component, parentGrid, logicalRow)`. App code should not depend on physical row indices inserted by row-resize handles. -Use `labkit.ui.view.panel` for reusable component groups such as file panels, log panels, read-only text panels, result tables, plot option panels, and top/bottom plot controls: +Use `labkit.ui.view.panel` for reusable component groups such as file panels, log panels, read-only text panels, and result tables: ```matlab fileUi = labkit.ui.view.panel(layFA, 'files', labels, callbacks); @@ -86,7 +100,6 @@ Use `labkit.ui.view.update` for state changes on existing component handles: labkit.ui.view.update(logUi.textArea, 'appendLog', 'Loaded file.'); [value, idx] = labkit.ui.view.update(fileUi.listbox, ... 'listSelection', names, previousSelection); -labkit.ui.view.update(plotControls, 'swapPlotSelections'); ``` ## Axes And Rendering @@ -97,7 +110,6 @@ Use view helpers for app-neutral rendering boilerplate: ax = labkit.ui.view.axes(parent, 1, 'Preview', 'X', 'Y'); labkit.ui.view.draw(ax, 'reset', 'Preview', true); hImage = labkit.ui.view.draw(ax, 'image', imageData, 'Reference'); -info = labkit.ui.view.draw(ax, 'xy', x, y, labels, opts); labkit.ui.view.draw(ax, 'popout'); ``` diff --git a/tests/gui/structural/labkit/ui/GuiLayoutUiAxesWorkbenchTest.m b/tests/gui/structural/labkit/ui/GuiLayoutUiAxesWorkbenchTest.m index 5315fc08..4da5dcdf 100644 --- a/tests/gui/structural/labkit/ui/GuiLayoutUiAxesWorkbenchTest.m +++ b/tests/gui/structural/labkit/ui/GuiLayoutUiAxesWorkbenchTest.m @@ -18,8 +18,6 @@ function verify_gui_layout_ui_axes_workbench() checkCreateAxesHelper(h); checkCreateAppShellHelper(h); - checkTopBottomPlotControlsHelper(h); - checkTopBottomPlotStateHelpers(h); end function checkCreateAxesHelper(h) @@ -118,125 +116,4 @@ function checkCreateAppShellHelper(h) assert(isempty(custom.probeResizeHandles), ... 'Custom tabs without resizeRows should not create resize handles.'); - dual = labkit.ui.app.createShell(struct( ... - 'title', 'labkit_create_dual_plot_app_shell_probe', ... - 'position', [40 30 1200 760], ... - 'leftWidth', 330, ... - 'options', struct('rightKind', 'dualPlot'))); - cleaner2 = onCleanup(@() delete(dual.fig)); %#ok - assert(h.sameStringCell(dual.rightGrid.RowHeight, {'fit', '1x', 'fit', '1x'}), ... - 'App shell helper should configure the standard dual-plot right region.'); - assert(strcmp(char(dual.topAxes.Title.String), 'Top Plot'), ... - 'App shell helper should create a titled top axes for dual-plot apps.'); - assert(strcmp(char(dual.bottomAxes.Title.String), 'Bottom Plot'), ... - 'App shell helper should create a titled bottom axes for dual-plot apps.'); - - dualNoControls = labkit.ui.app.createShell(struct( ... - 'title', 'labkit_create_dual_plot_no_controls_probe', ... - 'position', [40 30 1200 760], ... - 'leftWidth', 330, ... - 'options', struct('rightKind', 'dualPlot', 'showPlotControls', false))); - cleaner4 = onCleanup(@() delete(dualNoControls.fig)); %#ok - assert(h.sameStringCell(dualNoControls.rightGrid.RowHeight, {'1x', '1x'}), ... - 'App shell helper should support dual-plot output without empty control rows.'); - assert(isempty(dualNoControls.topControlsPanel) && isempty(dualNoControls.bottomControlsPanel), ... - 'Dual-plot output without controls should not create empty plot-control panels.'); - assert(dualNoControls.topAxes.Layout.Row == 1 && dualNoControls.bottomAxes.Layout.Row == 2, ... - 'Dual-plot output without controls should place axes directly in the output grid.'); -end - -function checkTopBottomPlotControlsHelper(h) - shell = createDualPlotWorkbenchForTest('labkit_top_bottom_plot_controls_probe'); - cleaner = onCleanup(@() delete(shell.fig)); %#ok - - topDefaults = struct('x', 'Time (s)', 'y', 'VT: Vf vs time', 'grid', true); - bottomDefaults = struct('x', 'Sample #', 'y', 'IT: Im vs time', 'grid', false); - ui = labkit.ui.view.panel( ... - shell.topControlsPanel, ... - 'topBottomPlotControls', ... - shell.bottomControlsPanel, ... - {'Time (s)', 'Sample #'}, ... - {'VT: Vf vs time', 'IT: Im vs time'}, ... - topDefaults, ... - bottomDefaults, ... - []); - - assert(isequal(ui.topGrid.ColumnWidth, {'fit', '1x', 'fit', '1x', '1x'}), ... - 'Top/bottom plot controls should preserve column widths.'); - assert(isequal(ui.topGrid.Padding, [8 6 8 6]), ... - 'Top/bottom plot controls should preserve grid padding.'); - assert(ui.topGrid.ColumnSpacing == 8, ... - 'Top/bottom plot controls should preserve column spacing.'); - assert(h.sameStringCell(ui.topX.Items, {'Time (s)', 'Sample #'}), ... - 'Top X dropdown should preserve supplied X items.'); - assert(h.sameStringCell(ui.topY.Items, {'VT: Vf vs time', 'IT: Im vs time'}), ... - 'Top Y dropdown should preserve supplied Y items.'); - assert(strcmp(ui.topX.Value, 'Time (s)'), ... - 'Top X dropdown should preserve the supplied default value.'); - assert(strcmp(ui.topY.Value, 'VT: Vf vs time'), ... - 'Top Y dropdown should preserve the supplied default value.'); - assert(ui.topGridCheckbox.Value == true, ... - 'Top grid checkbox should preserve the supplied default value.'); - assert(strcmp(ui.bottomX.Value, 'Sample #'), ... - 'Bottom X dropdown should preserve the supplied default value.'); - assert(strcmp(ui.bottomY.Value, 'IT: Im vs time'), ... - 'Bottom Y dropdown should preserve the supplied default value.'); - assert(ui.bottomGridCheckbox.Value == false, ... - 'Bottom grid checkbox should preserve the supplied default value.'); -end - -function checkTopBottomPlotStateHelpers(h) - shell = createDualPlotWorkbenchForTest('labkit_top_bottom_plot_state_probe'); - cleaner = onCleanup(@() delete(shell.fig)); %#ok - - topDefaults = struct('x', 'Time (s)', 'y', 'VT: Vf vs time', 'grid', true); - bottomDefaults = struct('x', 'Sample #', 'y', 'IT: Im vs time', 'grid', false); - ui = labkit.ui.view.panel( ... - shell.topControlsPanel, ... - 'topBottomPlotControls', ... - shell.bottomControlsPanel, ... - {'Time (s)', 'Sample #'}, ... - {'VT: Vf vs time', 'IT: Im vs time'}, ... - topDefaults, ... - bottomDefaults, ... - []); - - labkit.ui.view.update(ui, 'setPlotSelections', bottomDefaults, topDefaults); - assert(strcmp(ui.topX.Value, 'Sample #') && strcmp(ui.bottomX.Value, 'Time (s)'), ... - 'Top/bottom selection setter should apply supplied X defaults.'); - assert(strcmp(ui.topY.Value, 'IT: Im vs time') && strcmp(ui.bottomY.Value, 'VT: Vf vs time'), ... - 'Top/bottom selection setter should apply supplied Y defaults.'); - - labkit.ui.view.update(ui, 'swapPlotSelections'); - assert(strcmp(ui.topX.Value, 'Time (s)') && strcmp(ui.topY.Value, 'VT: Vf vs time'), ... - 'Top/bottom selection swap should move bottom selections to the top.'); - assert(strcmp(ui.bottomX.Value, 'Sample #') && strcmp(ui.bottomY.Value, 'IT: Im vs time'), ... - 'Top/bottom selection swap should move top selections to the bottom.'); - - shell.topAxes.XScale = 'log'; - shell.bottomAxes.YScale = 'log'; - labkit.ui.view.draw(shell.topAxes, 'reset', 'Top Plot', true); - labkit.ui.view.draw(shell.bottomAxes, 'reset', 'Bottom Plot', true); - assert(strcmp(char(shell.topAxes.Title.String), 'Top Plot'), ... - 'Hard axis reset should preserve the supplied top axes title.'); - assert(strcmp(char(shell.bottomAxes.Title.String), 'Bottom Plot'), ... - 'Hard axis reset should preserve the supplied bottom axes title.'); - assert(strcmp(shell.topAxes.XScale, 'linear') && strcmp(shell.bottomAxes.YScale, 'linear'), ... - 'Hard axis reset should optionally reset axis scales.'); - h.assertAxesPopoutEnabled(shell.topAxes, 'Hard axis reset should install top-axis popout.'); - h.assertAxesPopoutEnabled(shell.bottomAxes, 'Hard axis reset should install bottom-axis popout.'); -end - -function shell = createDualPlotWorkbenchForTest(figName) - opts = struct(); - opts.rightKind = 'dualPlot'; - opts.controlsTitle = 'Controls'; - opts.rightTitle = 'Plots'; - opts.topPlotTitle = 'Top Plot'; - opts.bottomPlotTitle = 'Bottom Plot'; - shell = labkit.ui.app.createShell(struct( ... - 'title', figName, ... - 'position', [40 30 1680 980], ... - 'leftWidth', 430, ... - 'options', opts)); end diff --git a/tests/gui/structural/labkit/ui/GuiLayoutUiBasicControlsTest.m b/tests/gui/structural/labkit/ui/GuiLayoutUiBasicControlsTest.m index 48480038..d60b1760 100644 --- a/tests/gui/structural/labkit/ui/GuiLayoutUiBasicControlsTest.m +++ b/tests/gui/structural/labkit/ui/GuiLayoutUiBasicControlsTest.m @@ -24,7 +24,6 @@ function verify_gui_layout_ui_basic_controls() checkReadOnlyInfoRowHelper(); checkResultTablePanelHelper(h); checkPanelGridHelper(h); - checkPlotOptionsPanelHelper(h); checkFileSelectionPanelHelper(h); end @@ -98,8 +97,12 @@ function checkLabeledSpinnerHelper() cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [1 2]); - [lbl, spinner] = labkit.ui.view.form(grid, 'spinner', 'Probe value:', ... - 'Value', 2, 'Limits', [0 10], 'Step', 0.5); + [lbl, spinner] = labkit.ui.view.form(grid, struct( ... + 'kind', 'spinner', ... + 'label', 'Probe value:', ... + 'value', 2, ... + 'limits', [0 10], ... + 'step', 0.5)); assert(strcmp(lbl.Text, 'Probe value:'), ... 'Labeled spinner helper should preserve label text.'); assert(strcmp(lbl.HorizontalAlignment, 'right'), ... @@ -113,7 +116,9 @@ function checkReadOnlyTextHelpers(h) cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 1]); - field = labkit.ui.view.form(grid, 'readonly', 'Value', 'Status'); + field = labkit.ui.view.form(grid, struct( ... + 'kind', 'readonly', ... + 'value', 'Status')); field.Layout.Row = 1; assert(strcmp(field.Editable, 'off') && strcmp(field.Value, 'Status'), ... 'Read-only text field helper should create a non-editable text field.'); @@ -131,7 +136,10 @@ function checkReadOnlyInfoRowHelper() cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 2]); - [field, lbl] = labkit.ui.view.form(grid, 'info', 2, 'Probe:'); + [field, lbl] = labkit.ui.view.form(grid, struct( ... + 'kind', 'info', ... + 'row', 2, ... + 'label', 'Probe:')); assert(strcmp(lbl.Text, 'Probe:'), 'Read-only info row should preserve label text.'); assert(strcmp(lbl.HorizontalAlignment, 'right'), ... 'Read-only info row should preserve right-aligned labels.'); @@ -195,27 +203,6 @@ function checkPanelGridHelper(h) 'Panel-grid helper should grow undersized fixed parent rows to avoid clipped controls.'); end -function checkPlotOptionsPanelHelper(h) - fig = uifigure('Visible', 'off', 'Name', 'labkit_plot_options_panel_probe'); - cleaner = onCleanup(@() delete(fig)); %#ok - grid = uigridlayout(fig, [3 1]); - - ui = labkit.ui.view.panel(grid, 'plotOptions', 3); - assert(strcmp(ui.panel.Title, 'Plot Options'), 'Plot-options helper should preserve the panel title.'); - assert(ui.panel.Layout.Row == 3, 'Plot-options helper should place the panel in row 3.'); - assert(h.sameStringCell(ui.grid.RowHeight, {'fit', 'fit', 'fit'}), ... - 'Plot-options helper should create fit-height rows.'); - assert(h.sameStringCell(ui.grid.ColumnWidth, {'fit', '1x'}), ... - 'Plot-options helper should preserve column widths.'); - assert(isequal(ui.grid.Padding, [8 8 8 8]), 'Plot-options helper should preserve padding.'); - assert(ui.grid.RowSpacing == 8 && ui.grid.ColumnSpacing == 8, ... - 'Plot-options helper should preserve row and column spacing.'); - - ui2 = labkit.ui.view.panel(grid, 'plotOptions', 2, 2); - assert(ui2.panel.Layout.Row == 2, ... - 'Plot-options helper should support an explicit parent-grid row.'); -end - function checkFileSelectionPanelHelper(h) fig = uifigure('Visible', 'off', 'Name', 'labkit_file_selection_panel_probe'); cleaner = onCleanup(@() delete(fig)); %#ok diff --git a/tests/helpers/architectureTestHelpers.m b/tests/helpers/architectureTestHelpers.m index b3c8b19f..fbee8514 100644 --- a/tests/helpers/architectureTestHelpers.m +++ b/tests/helpers/architectureTestHelpers.m @@ -76,10 +76,9 @@ assert(~contains(appOwnedSource, 'labkit.ui.createTabbedDualPlotShell'), ... [appName ' should not use compatibility shell wrappers directly.']); forbiddenViewHelpers = {'appendLog', 'clearAxes', 'enablePopout', ... - 'fileSelectionPanel', 'logPanel', 'plotOptionsPanel', 'plotXY', ... + 'fileSelectionPanel', 'logPanel', ... 'refreshListboxItems', 'refreshListboxSelection', 'resetAxes', ... - 'resultTable', 'setTopBottomPlotSelections', 'showImage', ... - 'swapTopBottomPlotSelections', 'textPanel', 'topBottomPlotControls'}; + 'resultTable', 'showImage', 'textPanel'}; for iHelper = 1:numel(forbiddenViewHelpers) oldViewCall = ['labkit.ui.view.' forbiddenViewHelpers{iHelper}]; assert(~contains(appOwnedSource, oldViewCall), ... diff --git a/tests/integration/project/AppOwnedWorkflowBoundariesTest.m b/tests/integration/project/AppOwnedWorkflowBoundariesTest.m index 1ea3d402..f4a803af 100644 --- a/tests/integration/project/AppOwnedWorkflowBoundariesTest.m +++ b/tests/integration/project/AppOwnedWorkflowBoundariesTest.m @@ -63,14 +63,17 @@ function verify_app_owned_workflow_boundaries() 'labkit_CSC_app should not call CSC analysis through the reusable +labkit package.'); assert(contains(cscSource, 'labkit.dta.getCurveXY'), ... 'CSC app should select parsed-curve columns through the DTA facade.'); - assert(contains(cscSource, 'labkit.ui.view.draw'), ... - 'CSC plotting should use the reusable prepared-X/Y GUI render facade.'); + assert(contains(cscSource, 'csc.ui.plotXY'), ... + 'CSC plotting should use the app-owned prepared-X/Y GUI renderer.'); + assert(~contains(cscSource, 'labkit.ui.view.draw(axTop, ''xy''') && ... + ~contains(cscSource, 'labkit.ui.view.draw(axBottom, ''xy'''), ... + 'CSC plotting should not use the removed reusable X/Y draw action.'); assert(~contains(cscSource, 'labkit.plot.plotCVCT'), ... 'CSC plotting should not live in reusable +labkit plot.'); h.assertNoPackageMFiles(fullfile(root, 'apps', '+labkit_apps', '+csc'), ... 'CSC transitional app-side'); - assert(exist(fullfile(root, '+labkit', '+ui', '+view', 'draw.m'), 'file') == 2, ... - 'Reusable prepared-X/Y plotting should be routed through the +labkit/+ui/+view draw facade.'); + assert(exist(fullfile(root, '+labkit', '+ui', '+view', 'private', 'plotXY.m'), 'file') ~= 2, ... + 'Prepared X/Y plotting should not remain a reusable +labkit UI private helper.'); assert(exist(fullfile(root, '+labkit', '+analysis', 'computeCSC.m'), 'file') ~= 2, ... 'CSC-specific analysis should not live in reusable +labkit analysis.'); diff --git a/tests/integration/project/PackagePublicSurfaceTest.m b/tests/integration/project/PackagePublicSurfaceTest.m index b9e186b3..f2a06deb 100644 --- a/tests/integration/project/PackagePublicSurfaceTest.m +++ b/tests/integration/project/PackagePublicSurfaceTest.m @@ -35,7 +35,7 @@ function verify_package_public_surface() 'UI app facade'); h.assertPackageMFiles(fullfile(root, '+labkit', '+ui', '+app', 'private'), ... {'addRowResizeHandle.m', 'attachColumnResize.m', ... - 'createTabbedWorkbenchShell.m', 'disableAxesInteractivity.m'}, ... + 'createTabbedWorkbenchShell.m'}, ... 'UI app private implementation'); h.assertPackageMFiles(fullfile(root, '+labkit', '+ui', '+diag'), ... {'createContext.m'}, ... @@ -49,11 +49,9 @@ function verify_package_public_surface() 'createLabeledEditField.m', 'createLabeledSpinner.m', ... 'createReadOnlyInfoRow.m', 'createReadOnlyTextField.m', ... 'enablePopout.m', 'fileSelectionPanel.m', 'layoutRow.m', ... - 'logPanel.m', 'plotOptionsPanel.m', 'plotXY.m', 'popoutAxes.m', ... + 'logPanel.m', 'popoutAxes.m', ... 'refreshListboxItems.m', 'refreshListboxSelection.m', ... - 'resetAxes.m', 'resultTable.m', 'setTopBottomPlotSelections.m', ... - 'showImage.m', 'swapTopBottomPlotSelections.m', 'textPanel.m', ... - 'topBottomPlotControls.m'}, ... + 'resetAxes.m', 'resultTable.m', 'showImage.m', 'textPanel.m'}, ... 'UI view private implementation'); h.assertTopLevelMFiles(fullfile(root, '+labkit', '+ui', '+tool'), ... {'anchorEditor.m', 'createRuntime.m', 'scaleBar.m', ... diff --git a/tests/unit/labkit/ui/PlotXYTest.m b/tests/unit/labkit/ui/PlotXYTest.m deleted file mode 100644 index 741e7187..00000000 --- a/tests/unit/labkit/ui/PlotXYTest.m +++ /dev/null @@ -1,58 +0,0 @@ -classdef PlotXYTest < matlab.unittest.TestCase - %PLOTXYTEST Verify LabKit behavior through official MATLAB tests. - - methods (Test, TestTags = {'Unit'}) - function test_plotXY(testCase) - setupLabKitTestPath(); - verify_plotXY(); - end - end -end - -function verify_plotXY() -%TEST_PLOTXY Verify prepared X/Y plotting helper behavior. - - curve = struct(); - curve.name = 'CURVE1'; - curve.headers = {'T', 'Vf', 'Im'}; - curve.units = {'s', 'V', 'A'}; - curve.numericMask = [true true true]; - curve.data = [0 0.1 NaN; 1 0.2 -1; 2 NaN 2; 3 0.4 3]; - - [x, y, xname, yname] = labkit.dta.getCurveXY(curve, 'T', 'Vf'); - assert(isequal(x, [0; 1; 3]), 'getCurveXY should remove rows with NaN X/Y.'); - assert(isequal(y, [0.1; 0.2; 0.4]), 'getCurveXY should preserve selected Y values.'); - assert(strcmp(xname, 'T') && strcmp(yname, 'Vf'), 'getCurveXY should return selected header names.'); - - [badX, badY] = labkit.dta.getCurveXY(curve, 't', 'Vf'); - assert(isempty(badX) && isempty(badY), 'getCurveXY should preserve stable exact-case column selection.'); - - fig = figure('Visible', 'off'); - cleaner = onCleanup(@() closeIfValid(fig)); - ax = axes(fig); - - opts = struct('holdPlot', false, 'showGrid', true, 'lineWidth', 1.2); - labels = struct('title', curve.name, 'x', xname, 'y', yname); - info = labkit.ui.view.draw(ax, 'xy', x, y, labels, opts); - assert(info.ok, info.message); - assert(isequal(info.x, x), 'plotXY should plot prepared X values.'); - assert(isequal(info.y, y), 'plotXY should plot prepared Y values.'); - assert(strcmp(info.xName, 'T') && strcmp(info.yName, 'Vf'), 'plotXY should report axis names.'); - - lines = findobj(ax, 'Type', 'line'); - assert(numel(lines) == 1, 'plotXY should add one data line.'); - assert(abs(lines(1).LineWidth - 1.2) < 1e-12, 'Line width should match stable default.'); - assert(strcmp(ax.Title.String, 'CURVE1'), 'Plot title should use curve name.'); - assert(strcmp(ax.XLabel.String, 'T'), 'X label should use selected header.'); - assert(strcmp(ax.YLabel.String, 'Vf'), 'Y label should use selected header.'); - - info2 = labkit.ui.view.draw(ax, 'xy', [], y, labels, opts); - assert(~info2.ok, 'Invalid X/Y selection should fail without throwing.'); - assert(strcmp(info2.message, 'invalid X/Y'), 'Invalid selection message should be stable.'); -end - -function closeIfValid(fig) - if isvalid(fig) - close(fig); - end -end