diff --git a/+labkit/+ui/createWorkbench.m b/+labkit/+ui/+app/createShell.m similarity index 53% rename from +labkit/+ui/createWorkbench.m rename to +labkit/+ui/+app/createShell.m index 4b936a1..2d0fc48 100644 --- a/+labkit/+ui/createWorkbench.m +++ b/+labkit/+ui/+app/createShell.m @@ -1,38 +1,52 @@ -function ui = createWorkbench(figName, figPosition, leftWidth, opts) -%CREATEWORKBENCH Deprecated compatibility scientific-app shell. +function ui = createShell(spec) +%CREATESHELL Create the standard LabKit app shell from a named spec. % % Usage: -% ui = labkit.ui.createWorkbench(titleText, position, leftWidth); -% ui = labkit.ui.createWorkbench(titleText, position, leftWidth, opts); -% -% New app code should call labkit.ui.createAppShell with a spec struct. -% This compatibility entry point is retained for one migration cycle. +% ui = labkit.ui.app.createShell(struct( ... +% 'title', "Example", ... +% 'position', [90 70 1200 800], ... +% 'leftWidth', 360, ... +% 'options', struct('rightKind', 'dualPlot'))); % % Inputs: -% figName - figure title. -% figPosition - MATLAB figure position [x y width height]. -% leftWidth - initial left controls width in pixels. -% opts - optional struct. -% -% Options: -% rightKind - "custom" (default) or "dualPlot". -% rightGridSize - [rows columns], default [1 1] for custom right side. -% rightRowHeight - cell row of grid row heights, default {'1x'}. -% rightRowSpacing - scalar pixels, default 8 or 10 for dualPlot. -% showPlotControls - logical, dualPlot only, default true. -% controlsTitle - left panel title, default "Controls". -% rightTitle - right panel title, default "Plots". -% topPlotTitle, bottomPlotTitle - dualPlot titles. -% tabs - tabSpec array. Omit for Files + Analysis / Summary + Results / Log. +% spec - scalar struct with fields: +% title - figure title text. +% 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. +% tabs - labkit.ui.app.tab struct array. % % Output: % ui - struct of figure, layout, tab grids, and right-side handles. +% +% Apps own controls and workflow inside the returned grids. The shell owns +% split-pane layout, tab construction, scrollable tab grids, resizable rows, +% and standard right-pane plumbing. + + if nargin < 1 || ~isstruct(spec) || ~isscalar(spec) + error('labkit:ui:InvalidAppShellSpec', ... + 'createShell requires a scalar struct spec.'); + end + required = {'title', 'position', 'leftWidth'}; + for k = 1:numel(required) + if ~isfield(spec, required{k}) + error('labkit:ui:InvalidAppShellSpec', ... + 'App shell spec is missing "%s".', required{k}); + end + end - if nargin < 4 - opts = struct(); + opts = optionValue(spec, 'options', struct()); + if isfield(spec, 'shellOptions') + opts = spec.shellOptions; end - rightKind = optionValue(opts, 'rightKind', 'custom'); + rightKind = char(string(optionValue(opts, 'rightKind', 'custom'))); rightGridSize = optionValue(opts, 'rightGridSize', [1 1]); rightRowHeight = optionValue(opts, 'rightRowHeight', {'1x'}); rightRowSpacing = optionValue(opts, 'rightRowSpacing', 8); @@ -54,7 +68,7 @@ tabSpecs = optionValue(opts, 'tabs', standardTabs()); ui = createTabbedWorkbenchShell( ... - figName, figPosition, leftWidth, shellLabels, tabSpecs, ... + spec.title, spec.position, spec.leftWidth, shellLabels, tabSpecs, ... rightGridSize, rightRowHeight, rightRowSpacing); if strcmp(rightKind, 'dualPlot') @@ -64,13 +78,13 @@ function tabs = standardTabs() tabs = [ ... - labkit.ui.tabSpec('filesAnalysis', 'Files + Analysis', [3 1], ... + labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [3 1], ... {260, 'fit', 'fit'}, ... struct('resizeRows', [1 2])), ... - labkit.ui.tabSpec('summaryResults', 'Summary + Results', [2 1], ... + labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ... {'fit', '1x'}, ... struct('resizeRows', 1)), ... - labkit.ui.tabSpec('log', 'Log', [1 1], {'1x'})]; + labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})]; end function ui = addDualPlotRegion(ui, opts) @@ -85,7 +99,7 @@ ui.topAxes = uiaxes(ui.rightGrid); ui.topAxes.Layout.Row = 2; title(ui.topAxes, topTitle); - labkit.ui.enableAxesPopout(ui.topAxes); + labkit.ui.view.draw(ui.topAxes, 'popout'); disableAxesInteractivity(ui.topAxes); ui.bottomControlsPanel = uipanel(ui.rightGrid, 'Title', bottomTitle); @@ -100,20 +114,20 @@ ui.topAxes = uiaxes(ui.rightGrid); ui.topAxes.Layout.Row = 1; title(ui.topAxes, topTitle); - labkit.ui.enableAxesPopout(ui.topAxes); + 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.enableAxesPopout(ui.bottomAxes); + labkit.ui.view.draw(ui.bottomAxes, 'popout'); disableAxesInteractivity(ui.bottomAxes); end function value = optionValue(opts, name, defaultValue) value = defaultValue; - if isfield(opts, name) + if isstruct(opts) && isfield(opts, name) value = opts.(name); end end diff --git a/+labkit/+ui/dispatchAppRequest.m b/+labkit/+ui/+app/dispatchRequest.m similarity index 94% rename from +labkit/+ui/dispatchAppRequest.m rename to +labkit/+ui/+app/dispatchRequest.m index 5dde824..3d25648 100644 --- a/+labkit/+ui/dispatchAppRequest.m +++ b/+labkit/+ui/+app/dispatchRequest.m @@ -1,8 +1,8 @@ -function [handled, outputs, debugContext] = dispatchAppRequest(appName, args, nout, handlers) +function [handled, outputs, debugContext] = dispatchRequest(appName, args, nout, handlers) %DISPATCHAPPREQUEST Dispatch app test/debug launch requests. % % Usage: -% [handled, outputs, debug] = labkit.ui.dispatchAppRequest( ... +% [handled, outputs, debug] = labkit.ui.app.dispatchRequest( ... % "labkit_Example_app", varargin, nargout, handlers); % % Inputs: @@ -23,7 +23,7 @@ appName = char(appName); handled = false; outputs = {}; - debugContext = labkit.ui.createDebugContext(appName, struct('enabled', false)); + debugContext = labkit.ui.diag.createContext(appName, struct('enabled', false)); if nargin < 4 handlers = struct('command', {}, 'minArgs', {}, ... @@ -44,7 +44,7 @@ '%s debug mode returns at most the app figure and debug log.', appName); end opts = debugOptions(appName, request, args); - debugContext = labkit.ui.createDebugContext(appName, opts); + debugContext = labkit.ui.diag.createContext(appName, opts); return; end diff --git a/+labkit/+ui/addRowResizeHandle.m b/+labkit/+ui/+app/private/addRowResizeHandle.m similarity index 98% rename from +labkit/+ui/addRowResizeHandle.m rename to +labkit/+ui/+app/private/addRowResizeHandle.m index 3fd5870..fca4224 100644 --- a/+labkit/+ui/addRowResizeHandle.m +++ b/+labkit/+ui/+app/private/addRowResizeHandle.m @@ -2,7 +2,7 @@ %ADDROWRESIZEHANDLE Add a draggable horizontal resize handle between grid rows. % % Usage: -% handle = labkit.ui.addRowResizeHandle(fig, grid, 2, ... +% handle = addRowResizeHandle(fig, grid, 2, ... % struct('topRow', 1, 'bottomRow', 3)); % % Inputs: diff --git a/+labkit/+ui/private/attachColumnResize.m b/+labkit/+ui/+app/private/attachColumnResize.m similarity index 97% rename from +labkit/+ui/private/attachColumnResize.m rename to +labkit/+ui/+app/private/attachColumnResize.m index 1bb5f49..e7e1ef7 100644 --- a/+labkit/+ui/private/attachColumnResize.m +++ b/+labkit/+ui/+app/private/attachColumnResize.m @@ -17,7 +17,7 @@ function attachColumnResize(fig, grid, leftColumn, separatorColumn, opts) % % Notes: % This helper mutates layout handles only; apps should normally request -% resizable shells through labkit.ui.createAppShell. +% resizable shells through labkit.ui.app.createShell. if nargin < 5 opts = struct(); diff --git a/+labkit/+ui/private/createTabbedWorkbenchShell.m b/+labkit/+ui/+app/private/createTabbedWorkbenchShell.m similarity index 95% rename from +labkit/+ui/private/createTabbedWorkbenchShell.m rename to +labkit/+ui/+app/private/createTabbedWorkbenchShell.m index ab1e6c2..7e6cb92 100644 --- a/+labkit/+ui/private/createTabbedWorkbenchShell.m +++ b/+labkit/+ui/+app/private/createTabbedWorkbenchShell.m @@ -2,14 +2,14 @@ %CREATETABBEDWORKBENCHSHELL Build the private tabbed workbench skeleton. % % Called by: -% labkit.ui.createAppShell +% labkit.ui.app.createShell % % Inputs: % figName - figure name/title. % figPosition - uifigure Position vector. % leftWidth - initial fixed width of the left control panel. % labels - struct with controlsPanel and rightPanel text. -% tabSpecs - struct array from labkit.ui.tabSpec/default shell specs. +% tabSpecs - struct array from labkit.ui.app.tab/default shell specs. % rightGridSize - right-side uigridlayout size. % rightRowHeight - right-side grid RowHeight cell array. % rightRowSpacing - right-side grid RowSpacing scalar. @@ -21,7 +21,7 @@ % Notes: % Logical tab rows are expanded with physical resize-handle rows here. % App code should place controls through public helpers that call -% labkit.ui.layoutRow rather than depending on physical row indices. +% labkit.ui.view.place rather than depending on physical row indices. ui = struct(); ui.fig = uifigure('Name', figName, 'Position', figPosition); @@ -161,7 +161,7 @@ function enableScrollableGrid(grid) end opts.topRow = rowMap(topRow); opts.bottomRow = rowMap(topRow + 1); - handles(k) = labkit.ui.addRowResizeHandle(fig, grid, rowMap(topRow) + 1, opts); + handles(k) = addRowResizeHandle(fig, grid, rowMap(topRow) + 1, opts); end end diff --git a/+labkit/+ui/private/disableAxesInteractivity.m b/+labkit/+ui/+app/private/disableAxesInteractivity.m similarity index 100% rename from +labkit/+ui/private/disableAxesInteractivity.m rename to +labkit/+ui/+app/private/disableAxesInteractivity.m diff --git a/+labkit/+ui/runWithBusyState.m b/+labkit/+ui/+app/runBusy.m similarity index 92% rename from +labkit/+ui/runWithBusyState.m rename to +labkit/+ui/+app/runBusy.m index 3a9fa25..fe1efef 100644 --- a/+labkit/+ui/runWithBusyState.m +++ b/+labkit/+ui/+app/runBusy.m @@ -1,9 +1,9 @@ -function varargout = runWithBusyState(fig, workFcn, opts) -%RUNWITHBUSYSTATE Run synchronous GUI work with busy feedback. +function varargout = runBusy(fig, workFcn, opts) +%RUNBUSY Run synchronous GUI work with busy feedback. % % Usage: -% labkit.ui.runWithBusyState(fig, @() refreshResults(), opts); -% result = labkit.ui.runWithBusyState(fig, @() computeResult(), opts); +% labkit.ui.app.runBusy(fig, @() refreshResults(), opts); +% result = labkit.ui.app.runBusy(fig, @() computeResult(), opts); % % Inputs: % fig - owning uifigure or figure. Empty or invalid figures are accepted; @@ -32,14 +32,14 @@ % Notes: % This helper is intended for long, synchronous callbacks. If the callback % permanently changes control enable states, refresh those states after -% runWithBusyState returns so the cleanup restore does not preserve stale +% runBusy returns so the cleanup restore does not preserve stale % pre-run values. if nargin < 3 opts = struct(); end if ~isa(workFcn, 'function_handle') - error('labkit:ui:runWithBusyState:InvalidCallback', ... + error('labkit:ui:runBusy:InvalidCallback', ... 'workFcn must be a function handle.'); end diff --git a/+labkit/+ui/tabSpec.m b/+labkit/+ui/+app/tab.m similarity index 89% rename from +labkit/+ui/tabSpec.m rename to +labkit/+ui/+app/tab.m index 2c8e8e9..47c49d7 100644 --- a/+labkit/+ui/tabSpec.m +++ b/+labkit/+ui/+app/tab.m @@ -1,8 +1,8 @@ -function spec = tabSpec(key, titleText, gridSize, rowHeight, opts) +function spec = tab(key, titleText, gridSize, rowHeight, opts) %TABSPEC Build a tab specification for the shared workbench shell. % % Usage: -% spec = labkit.ui.tabSpec('filesAnalysis', 'Files + Analysis', ... +% spec = labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', ... % [4 1], {240, 220, 280, 160}, struct('resizeRows', [1 2 3])); % % Inputs: @@ -19,7 +19,7 @@ % padding, rowSpacing, columnSpacing - grid layout properties. % % Output: -% spec - struct consumed by createAppShell spec options tabs. +% spec - struct consumed by createShell spec options tabs. if nargin < 4 || isempty(rowHeight) rowHeight = repmat({'fit'}, 1, gridSize(1)); diff --git a/+labkit/+ui/createDebugContext.m b/+labkit/+ui/+diag/createContext.m similarity index 98% rename from +labkit/+ui/createDebugContext.m rename to +labkit/+ui/+diag/createContext.m index 2c55a1a..f8b0348 100644 --- a/+labkit/+ui/createDebugContext.m +++ b/+labkit/+ui/+diag/createContext.m @@ -1,8 +1,8 @@ -function debugContext = createDebugContext(appName, opts) +function debugContext = createContext(appName, opts) %CREATEDEBUGCONTEXT Create an app-neutral debug and trace context. % % Usage: -% debug = labkit.ui.createDebugContext("labkit_Example_app", opts); +% debug = labkit.ui.diag.createContext("labkit_Example_app", opts); % debug.append("Loaded file"); % debug.trace("button pressed"); % debug.trace("scaleBar", "reference changed", "user"); @@ -95,7 +95,7 @@ function appendTraceToTextLog(line) if isempty(textArea) || ~isvalid(textArea) return; end - labkit.ui.appendLog(textArea, line); + labkit.ui.view.update(textArea, 'appendLog', line); end end diff --git a/+labkit/+ui/createAnchorCurveEditor.m b/+labkit/+ui/+tool/anchorEditor.m similarity index 96% rename from +labkit/+ui/createAnchorCurveEditor.m rename to +labkit/+ui/+tool/anchorEditor.m index 2659987..125302a 100644 --- a/+labkit/+ui/createAnchorCurveEditor.m +++ b/+labkit/+ui/+tool/anchorEditor.m @@ -1,14 +1,14 @@ -function editor = createAnchorCurveEditor(runtime, imageSize, opts) -%CREATEANCHORCURVEEDITOR Create reusable editable anchor-curve interaction. +function editor = anchorEditor(runtime, imageSize, opts) +%ANCHOREDITOR Create reusable editable anchor-curve interaction. % % Usage: -% runtime = labkit.ui.createInteractionRuntime(ax); -% editor = labkit.ui.createAnchorCurveEditor(runtime, size(image), ... +% runtime = labkit.ui.tool.createRuntime(ax); +% editor = labkit.ui.tool.anchorEditor(runtime, size(image), ... % struct('closed', true, 'style', 'Curve', 'onChanged', @onChanged)); % editor.start(points); % % Inputs: -% runtime - interaction runtime returned by labkit.ui.createInteractionRuntime. +% runtime - interaction runtime returned by labkit.ui.tool.createRuntime. % imageSize - [height width] or image size used for zoom/limit clamping. % opts - optional struct. % @@ -47,7 +47,7 @@ isa(runtime.axes, 'function_handle') && ... isfield(runtime, 'createSession') && ... isa(runtime.createSession, 'function_handle'), ... - 'First input must be a labkit.ui.createInteractionRuntime result.'); + 'First input must be a labkit.ui.tool.createRuntime result.'); state.runtime = runtime; state.ax = runtime.axes(); @@ -206,7 +206,7 @@ function deleteEditor() if ~isempty(state.anchorLine) && isvalid(state.anchorLine) delete(state.anchorLine); end - labkit.ui.enableAxesPopout(state.ax); + labkit.ui.view.draw(state.ax, 'popout'); end function onAxesClicked(~, ~) @@ -317,7 +317,7 @@ function ensureGraphics() created = true; end if created - labkit.ui.enableAxesPopout(state.ax); + labkit.ui.view.draw(state.ax, 'popout'); end end diff --git a/+labkit/+ui/createImageAxesRuntime.m b/+labkit/+ui/+tool/createRuntime.m similarity index 97% rename from +labkit/+ui/createImageAxesRuntime.m rename to +labkit/+ui/+tool/createRuntime.m index b41b6de..6ab61ef 100644 --- a/+labkit/+ui/createImageAxesRuntime.m +++ b/+labkit/+ui/+tool/createRuntime.m @@ -1,8 +1,8 @@ -function runtime = createImageAxesRuntime(ax, opts) -%CREATEIMAGEAXESRUNTIME Deprecated compatibility runtime for image axes. +function runtime = createRuntime(ax, opts) +%CREATERUNTIME Create a runtime that owns image-axes interaction sessions. % % Usage: -% runtime = labkit.ui.createImageAxesRuntime(ax, ... +% runtime = labkit.ui.tool.createRuntime(ax, ... % struct('figure', fig, 'defaultScrollFcn', @onPreviewScroll)); % session = runtime.createSession(struct( ... % 'name', 'curveEditor', ... @@ -10,8 +10,8 @@ % 'onScroll', @onScroll, ... % 'installScrollWheel', true)); % -% New app code should call labkit.ui.createInteractionRuntime. This -% compatibility entry point is retained for one migration cycle. +% The runtime is the public owner for pointer, drag, scroll, and hit-test sessions. + % % Inputs: % ax - UI axes used by image tools. @@ -122,7 +122,7 @@ function setTraceCallback(fcn) function installDefaultCallbacks() if isValidHandle(state.ax) - labkit.ui.enableAxesPopout(state.ax); + labkit.ui.view.draw(state.ax, 'popout'); end if ~isValidHandle(state.fig) || ~isempty(state.activeScrollToken) return; diff --git a/+labkit/+ui/private/addOrInsertAnchor.m b/+labkit/+ui/+tool/private/addOrInsertAnchor.m similarity index 98% rename from +labkit/+ui/private/addOrInsertAnchor.m rename to +labkit/+ui/+tool/private/addOrInsertAnchor.m index 363158c..24227ad 100644 --- a/+labkit/+ui/private/addOrInsertAnchor.m +++ b/+labkit/+ui/+tool/private/addOrInsertAnchor.m @@ -2,7 +2,7 @@ %ADDORINSERTANCHOR Apply anchor insertion policy for createAnchorCurveEditor. % % Expected caller: -% labkit.ui.createAnchorCurveEditor when users double-click/add anchors. +% labkit.ui.tool.anchorEditor when users double-click/add anchors. % % Inputs: % points - existing N-by-2 normalized anchor coordinates. diff --git a/+labkit/+ui/private/anchorCurvePoints.m b/+labkit/+ui/+tool/private/anchorCurvePoints.m similarity index 97% rename from +labkit/+ui/private/anchorCurvePoints.m rename to +labkit/+ui/+tool/private/anchorCurvePoints.m index f228a17..3d459fb 100644 --- a/+labkit/+ui/private/anchorCurvePoints.m +++ b/+labkit/+ui/+tool/private/anchorCurvePoints.m @@ -2,7 +2,7 @@ %ANCHORCURVEPOINTS Build displayed anchor path samples for curve editors. % % Expected caller: -% labkit.ui.createAnchorCurveEditor and sibling private insertion helpers. +% labkit.ui.tool.anchorEditor and sibling private insertion helpers. % % Inputs: % points - N-by-2 anchor coordinates in image pixel coordinates. diff --git a/+labkit/+ui/createLabeledDropdown.m b/+labkit/+ui/+tool/private/createLabeledDropdown.m similarity index 100% rename from +labkit/+ui/createLabeledDropdown.m rename to +labkit/+ui/+tool/private/createLabeledDropdown.m diff --git a/+labkit/+ui/createLabeledEditField.m b/+labkit/+ui/+tool/private/createLabeledEditField.m similarity index 100% rename from +labkit/+ui/createLabeledEditField.m rename to +labkit/+ui/+tool/private/createLabeledEditField.m diff --git a/+labkit/+ui/createLabeledSpinner.m b/+labkit/+ui/+tool/private/createLabeledSpinner.m similarity index 100% rename from +labkit/+ui/createLabeledSpinner.m rename to +labkit/+ui/+tool/private/createLabeledSpinner.m diff --git a/+labkit/+ui/createReadOnlyInfoRow.m b/+labkit/+ui/+tool/private/createReadOnlyInfoRow.m similarity index 85% rename from +labkit/+ui/createReadOnlyInfoRow.m rename to +labkit/+ui/+tool/private/createReadOnlyInfoRow.m index 203dfe4..2f424eb 100644 --- a/+labkit/+ui/createReadOnlyInfoRow.m +++ b/+labkit/+ui/+tool/private/createReadOnlyInfoRow.m @@ -10,7 +10,7 @@ % field - read-only text field initialized to "-". % lbl - label handle. - [lbl, field] = labkit.ui.createLabeledEditField(parent, labelText, 'text', ... + [lbl, field] = createLabeledEditField(parent, labelText, 'text', ... 'Editable', 'off', ... 'Value', '-'); lbl.Layout.Row = row; diff --git a/+labkit/+ui/createReadOnlyTextField.m b/+labkit/+ui/+tool/private/createReadOnlyTextField.m similarity index 100% rename from +labkit/+ui/createReadOnlyTextField.m rename to +labkit/+ui/+tool/private/createReadOnlyTextField.m diff --git a/+labkit/+ui/private/defaultScaleBarUnits.m b/+labkit/+ui/+tool/private/defaultScaleBarUnits.m similarity index 100% rename from +labkit/+ui/private/defaultScaleBarUnits.m rename to +labkit/+ui/+tool/private/defaultScaleBarUnits.m diff --git a/+labkit/+ui/private/drawScaleBarOverlay.m b/+labkit/+ui/+tool/private/drawScaleBarOverlay.m similarity index 89% rename from +labkit/+ui/private/drawScaleBarOverlay.m rename to +labkit/+ui/+tool/private/drawScaleBarOverlay.m index fc69839..0396aeb 100644 --- a/+labkit/+ui/private/drawScaleBarOverlay.m +++ b/+labkit/+ui/+tool/private/drawScaleBarOverlay.m @@ -2,11 +2,11 @@ %DRAWSCALEBAROVERLAY Draw a prepared scale-bar spec onto image axes. % % Expected caller: -% labkit.ui.createScaleBarTool renderOverlay method. +% labkit.ui.tool.scaleBar renderOverlay method. % % Inputs/outputs: % ax - axes/uiaxes receiving overlay objects. -% scaleBar - struct returned by createScaleBarPanel.scaleBarSpec. +% scaleBar - struct returned by the private scaleBarPanel scaleBarSpec. % Returns a struct with line and label graphics handles, or empty when no % scale bar is supplied. % diff --git a/+labkit/+ui/private/normalizeScaleBarUnit.m b/+labkit/+ui/+tool/private/normalizeScaleBarUnit.m similarity index 100% rename from +labkit/+ui/private/normalizeScaleBarUnit.m rename to +labkit/+ui/+tool/private/normalizeScaleBarUnit.m diff --git a/+labkit/+ui/createScaleBarPanel.m b/+labkit/+ui/+tool/private/scaleBarPanel.m similarity index 93% rename from +labkit/+ui/createScaleBarPanel.m rename to +labkit/+ui/+tool/private/scaleBarPanel.m index 568be8e..7b5b558 100644 --- a/+labkit/+ui/createScaleBarPanel.m +++ b/+labkit/+ui/+tool/private/scaleBarPanel.m @@ -1,8 +1,8 @@ -function ui = createScaleBarPanel(parent, row, opts) +function ui = scaleBarPanel(parent, row, opts) %CREATESCALEBARPANEL Create reusable scale-bar calibration controls. % % Usage: -% ui = labkit.ui.createScaleBarPanel(parentGrid, 2, opts); +% ui = scaleBarPanel(parentGrid, 2, opts); % ui.setReferencePixels(125); % cal = ui.calibration(); % [pxPerUnit, unitName] = ui.pixelsPerUnit(); @@ -60,7 +60,7 @@ defaultScaleBarLength = optionValue(opts, 'defaultScaleBarLength', 1); panelTitle = char(string(optionValue(opts, 'title', 'Scale Bar'))); - panelUi = labkit.ui.createPanelGrid(parent, panelTitle, row, [10 2], ... + panelUi = labkit.ui.view.section(parent, panelTitle, row, [10 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', ... 'fit', 'fit', 'fit', 'fit', 'fit'}}, ... 'columnWidth', {{145, '1x'}})); @@ -72,7 +72,7 @@ btnMeasureReference.Layout.Row = 1; btnMeasureReference.Layout.Column = [1 2]; - [lblReferencePx, edtReferencePx] = labkit.ui.createLabeledSpinner(grid, ... + [lblReferencePx, edtReferencePx] = createLabeledSpinner(grid, ... 'Reference pixels:', 'Value', 0, 'Limits', [0 Inf], 'Step', 1, ... 'ValueChangedFcn', @onCalibrationChanged); lblReferencePx.Layout.Row = 2; @@ -80,7 +80,7 @@ edtReferencePx.Layout.Row = 2; edtReferencePx.Layout.Column = 2; - [lblReferenceLen, edtReferenceLen] = labkit.ui.createLabeledSpinner(grid, ... + [lblReferenceLen, edtReferenceLen] = createLabeledSpinner(grid, ... 'Reference length:', 'Value', defaultReferenceLength, ... 'Limits', [0 Inf], 'Step', 10, ... 'ValueChangedFcn', @onCalibrationChanged); @@ -89,7 +89,7 @@ edtReferenceLen.Layout.Row = 3; edtReferenceLen.Layout.Column = 2; - [lblUnit, ddUnit] = labkit.ui.createLabeledDropdown(grid, ... + [lblUnit, ddUnit] = createLabeledDropdown(grid, ... 'Scale unit:', 'Items', units, 'Value', defaultChoice(defaultUnit, units), ... 'ValueChangedFcn', @onCalibrationChanged); lblUnit.Layout.Row = 4; @@ -97,7 +97,7 @@ ddUnit.Layout.Row = 4; ddUnit.Layout.Column = 2; - [lblBarLen, edtBarLen] = labkit.ui.createLabeledSpinner(grid, ... + [lblBarLen, edtBarLen] = createLabeledSpinner(grid, ... 'Scale bar length:', 'Value', defaultScaleBarLength, ... 'Limits', [0 Inf], 'Step', 10, ... 'ValueChangedFcn', @onScaleBarChanged); @@ -106,7 +106,7 @@ edtBarLen.Layout.Row = 5; edtBarLen.Layout.Column = 2; - [lblPosition, ddPosition] = labkit.ui.createLabeledDropdown(grid, ... + [lblPosition, ddPosition] = createLabeledDropdown(grid, ... 'Scale position:', 'Items', positions, ... 'Value', defaultChoice(defaultPosition, positions), ... 'ValueChangedFcn', @onScaleBarChanged); @@ -115,7 +115,7 @@ ddPosition.Layout.Row = 6; ddPosition.Layout.Column = 2; - [lblColor, ddColor] = labkit.ui.createLabeledDropdown(grid, ... + [lblColor, ddColor] = createLabeledDropdown(grid, ... 'Scale color:', 'Items', colors, ... 'Value', defaultChoice(defaultColor, colors), ... 'ValueChangedFcn', @onScaleBarChanged); @@ -130,9 +130,9 @@ btnPlaceScaleBar.Layout.Row = 8; btnPlaceScaleBar.Layout.Column = [1 2]; - [txtReferencePx, lblReferencePxReadout] = labkit.ui.createReadOnlyInfoRow( ... + [txtReferencePx, lblReferencePxReadout] = createReadOnlyInfoRow( ... grid, 9, 'Reference px:'); - [txtPxPerUnit, lblPxPerUnit] = labkit.ui.createReadOnlyInfoRow( ... + [txtPxPerUnit, lblPxPerUnit] = createReadOnlyInfoRow( ... grid, 10, 'Pixels/unit:'); lblReferencePxReadout.HorizontalAlignment = 'right'; lblPxPerUnit.HorizontalAlignment = 'right'; @@ -218,7 +218,7 @@ function clearReferencePixels() end function cal = calibration() - cal = labkit.ui.scaleBarCalibration(referencePixels(), referenceLength(), ... + cal = labkit.ui.tool.scaleBarCalibration(referencePixels(), referenceLength(), ... ddUnit.Value, struct('units', {units}, 'defaultUnit', defaultUnit)); end diff --git a/+labkit/+ui/createScaleBarTool.m b/+labkit/+ui/+tool/scaleBar.m similarity index 95% rename from +labkit/+ui/createScaleBarTool.m rename to +labkit/+ui/+tool/scaleBar.m index 049aa29..51ec421 100644 --- a/+labkit/+ui/createScaleBarTool.m +++ b/+labkit/+ui/+tool/scaleBar.m @@ -1,9 +1,9 @@ -function tool = createScaleBarTool(parent, row, runtime, opts) -%CREATESCALEBARTOOL Create a reusable image scale-bar interaction tool. +function tool = scaleBar(parent, row, runtime, opts) +%SCALEBAR Create a reusable image scale-bar interaction tool. % % Usage: -% runtime = labkit.ui.createInteractionRuntime(imageAxes); -% tool = labkit.ui.createScaleBarTool(parentGrid, 3, runtime, opts); +% runtime = labkit.ui.tool.createRuntime(imageAxes); +% tool = labkit.ui.tool.scaleBar(parentGrid, 3, runtime, opts); % tool.setImageSize(size(imageData)); % tool.setBackground(hImage); % cal = tool.calibration(); @@ -12,13 +12,13 @@ % Inputs: % parent - uigridlayout parent that will receive the scale-bar panel. % row - logical parent row for the panel. -% runtime - interaction runtime returned by labkit.ui.createInteractionRuntime. +% runtime - interaction runtime returned by labkit.ui.tool.createRuntime. % opts - optional struct. % % Options: % title, units, positions, colors, defaultUnit, defaultReferenceLength, % defaultScaleBarLength, defaultPosition, and defaultColor are forwarded to -% createScaleBarPanel. Defaults are the standard scale-bar UI defaults. +% the private scale-bar panel. Defaults are the standard scale-bar UI defaults. % imageSize - optional initial image size. % onBeforeReferenceEdit - callback before reference endpoint editing starts. % onReferenceEditChanged - callback after reference edit mode or points change. @@ -53,7 +53,7 @@ isa(runtime.axes, 'function_handle') && ... isfield(runtime, 'createSession') && ... isa(runtime.createSession, 'function_handle'), ... - 'Third input must be a labkit.ui.createInteractionRuntime result.'); + 'Third input must be a labkit.ui.tool.createRuntime result.'); state.runtime = runtime; state.ax = runtime.axes(); @@ -72,7 +72,7 @@ panelOpts.onCalibrationChanged = @onPanelCalibrationChanged; panelOpts.onScaleBarChanged = @onPanelScaleBarChanged; panelOpts.onPlaceScaleBar = @onPlaceScaleBarButton; - scalePanel = labkit.ui.createScaleBarPanel(parent, row, panelOpts); + scalePanel = scaleBarPanel(parent, row, panelOpts); tool = scalePanel; tool.setImageSize = @setImageSize; @@ -130,7 +130,7 @@ function resetForNewImage(imageSize) end function cal = calibration() - cal = labkit.ui.scaleBarCalibration(scalePanel.referencePixels(), ... + cal = labkit.ui.tool.scaleBarCalibration(scalePanel.referencePixels(), ... scalePanel.referenceLength(), scalePanel.scaleUnit(), ... struct('units', {scalePanel.controls.unitDropdown.Items}, ... 'defaultUnit', scalePanel.controls.unitDropdown.Items{1}, ... @@ -308,7 +308,7 @@ function ensureReferenceEditor() refreshBackgroundFromAxes(); if isempty(state.referenceEditor) trace('ensureReferenceEditor create editor'); - state.referenceEditor = labkit.ui.createAnchorCurveEditor(state.runtime, state.imageSize, ... + state.referenceEditor = labkit.ui.tool.anchorEditor(state.runtime, state.imageSize, ... struct('closed', false, ... 'style', 'Straight lines', ... 'maxPoints', 2, ... diff --git a/+labkit/+ui/scaleBarCalibration.m b/+labkit/+ui/+tool/scaleBarCalibration.m similarity index 98% rename from +labkit/+ui/scaleBarCalibration.m rename to +labkit/+ui/+tool/scaleBarCalibration.m index 3718a36..1942158 100644 --- a/+labkit/+ui/scaleBarCalibration.m +++ b/+labkit/+ui/+tool/scaleBarCalibration.m @@ -2,7 +2,7 @@ %SCALEBARCALIBRATION Build a reusable image scale-bar calibration struct. % % Usage: -% cal = labkit.ui.scaleBarCalibration(80, 20, "mm"); +% cal = labkit.ui.tool.scaleBarCalibration(80, 20, "mm"); % if cal.isCalibrated % physicalLength = pixelLength / cal.pixelsPerUnit; % end diff --git a/+labkit/+ui/createAxes.m b/+labkit/+ui/+view/axes.m similarity index 71% rename from +labkit/+ui/createAxes.m rename to +labkit/+ui/+view/axes.m index fc5dad4..46299e3 100644 --- a/+labkit/+ui/createAxes.m +++ b/+labkit/+ui/+view/axes.m @@ -1,4 +1,4 @@ -function ax = createAxes(parent, row, titleText, xLabelText, yLabelText) +function ax = axes(parent, row, titleText, xLabelText, yLabelText) %CREATEAXES Create an axes and apply its initial layout and labels. % % Inputs: @@ -10,9 +10,9 @@ % ax - UI axes with standard popout context action enabled. ax = uiaxes(parent); - ax.Layout.Row = labkit.ui.layoutRow(parent, row); + ax.Layout.Row = layoutRow(parent, row); title(ax, titleText); xlabel(ax, xLabelText); ylabel(ax, yLabelText); - labkit.ui.enableAxesPopout(ax); + enablePopout(ax); end diff --git a/+labkit/+ui/+view/draw.m b/+labkit/+ui/+view/draw.m new file mode 100644 index 0000000..e733bde --- /dev/null +++ b/+labkit/+ui/+view/draw.m @@ -0,0 +1,74 @@ +function varargout = draw(ax, action, varargin) +%DRAW Apply an app-neutral rendering action to axes. +% +% 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". +% 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. + + switch normalizeAction(action) + case 'reset' + titleText = positional(varargin, 1, ''); + resetScaleAndTicks = positional(varargin, 2, false); + resetAxes(ax, titleText, resetScaleAndTicks); + out = []; + case 'image' + imageData = positional(varargin, 1, []); + 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 = []; + case 'popout' + enablePopout(ax); + out = []; + otherwise + error('labkit_ui:draw:UnknownAction', ... + 'Unknown LabKit view draw action "%s".', char(action)); + end + + if nargout > 0 + varargout{1} = out; + end +end + +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) + value = defaultValue; + if numel(args) >= index && ~isempty(args{index}) + value = args{index}; + end +end diff --git a/+labkit/+ui/+view/form.m b/+labkit/+ui/+view/form.m new file mode 100644 index 0000000..898efda --- /dev/null +++ b/+labkit/+ui/+view/form.m @@ -0,0 +1,299 @@ +function varargout = form(parent, spec, varargin) +%FORM Create LabKit form controls from a unified control spec. +% +% Usage: +% [lbl, spinner] = labkit.ui.view.form(parent, struct( ... +% '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], ... +% 'controls', [struct('id','mode','kind','dropdown', ...)])); +% +% 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. +% +% 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. +% +% Outputs: +% Single-control call returns [labelHandle, controlHandle] for labeled +% controls or the control handle for button/checkbox/readonly without a +% label. Section calls return a struct with panel, grid, controls, +% labels, setValue, and getValue. +% +% setValue(id, value, reason) no-ops when the value is unchanged and +% suppresses app-facing semantic callbacks for "internal" updates. + + 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); + end + + if ~isstruct(spec) || ~isscalar(spec) + error('labkit:ui:view:InvalidFormSpec', ... + 'form requires a scalar struct spec or control kind.'); + end + + if isfield(spec, 'controls') + ui = createFormSection(parent, spec); + varargout = {ui}; + return; + end + + [labelHandle, controlHandle] = createOne(parent, spec); + if nargout <= 1 + varargout = {controlHandle}; + elseif isfield(spec, 'kind') && strcmpi(char(string(spec.kind)), 'info') + varargout = {controlHandle, labelHandle}; + else + varargout = {labelHandle, controlHandle}; + 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, ... + optionValue(spec, 'title', ''), ... + optionValue(spec, 'row', []), ... + layout, optionValue(spec, 'sectionOptions', struct())); + ui.controls = struct(); + ui.labels = struct(); + ui.setValue = @setValue; + ui.getValue = @getValue; + + controls = spec.controls; + for k = 1:numel(controls) + controlSpec = controls(k); + if ~isfield(controlSpec, 'row') + controlSpec.row = k; + end + [lbl, ctrl] = createOne(ui.grid, controlSpec); + if isfield(controlSpec, 'id') && strlength(string(controlSpec.id)) > 0 + id = matlab.lang.makeValidName(char(string(controlSpec.id))); + ui.controls.(id) = ctrl; + if ~isempty(lbl) + ui.labels.(id) = lbl; + end + end + end + + function setValue(id, value, reason) + if nargin < 3 + reason = "programmatic"; + end + name = matlab.lang.makeValidName(char(string(id))); + if ~isfield(ui.controls, name) + error('labkit:ui:view:UnknownControl', ... + 'Unknown form control "%s".', char(string(id))); + end + ctrl = ui.controls.(name); + if ~isprop(ctrl, 'Value') || valuesEqual(ctrl.Value, value) + return; + end + oldCallback = callbackProperty(ctrl); + cleanupObj = suppressCallback(ctrl, oldCallback, reason); %#ok + ctrl.Value = value; + end + + function value = getValue(id) + name = matlab.lang.makeValidName(char(string(id))); + if ~isfield(ui.controls, name) + error('labkit:ui:view:UnknownControl', ... + 'Unknown form control "%s".', char(string(id))); + end + ctrl = ui.controls.(name); + if isprop(ctrl, 'Value') + value = ctrl.Value; + else + value = []; + end + end +end + +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]; + + switch kind + case 'spinner' + [lbl, ctrl] = createLabeledSpinner(parent, labelText, args{:}); + case 'dropdown' + [lbl, ctrl] = createLabeledDropdown(parent, labelText, args{:}); + case 'edit' + style = optionValue(spec, 'style', 'text'); + [lbl, ctrl] = createLabeledEditField(parent, labelText, style, args{:}); + case 'readonly' + lbl = []; + ctrl = createReadOnlyTextField(parent, args{:}); + case 'info' + row = optionValue(spec, 'row', []); + [ctrl, lbl] = createReadOnlyInfoRow(parent, row, labelText); + case 'button' + lbl = []; + ctrl = uibutton(parent, args{:}); + if isfield(spec, 'text') + ctrl.Text = spec.text; + elseif isfield(spec, 'label') + ctrl.Text = spec.label; + end + case 'checkbox' + lbl = []; + ctrl = uicheckbox(parent, args{:}); + if isfield(spec, 'text') + ctrl.Text = spec.text; + elseif isfield(spec, 'label') + ctrl.Text = spec.label; + end + otherwise + error('labkit:ui:view:UnknownControlKind', ... + 'Unsupported form control kind "%s".', kind); + end + + if isfield(spec, 'row') && ~isempty(spec.row) + placeHandle(lbl, spec.row, 1); + if strcmp(kind, 'button') || strcmp(kind, 'checkbox') || strcmp(kind, 'readonly') + placeHandle(ctrl, spec.row, optionValue(spec, 'column', [1 2])); + else + placeHandle(ctrl, spec.row, optionValue(spec, 'column', 2)); + end + end +end + +function args = commonArgs(spec) + args = {}; + if isfield(spec, 'items') + args = [args, {'Items', spec.items}]; + end + if isfield(spec, 'value') + args = [args, {'Value', spec.value}]; + end + if isfield(spec, 'limits') + args = [args, {'Limits', spec.limits}]; + end + if isfield(spec, 'step') + args = [args, {'Step', spec.step}]; + end + if isfield(spec, 'enabled') + args = [args, {'Enable', onOff(spec.enabled)}]; + end + if isfield(spec, 'callback') + if any(strcmpi(char(string(optionValue(spec, 'kind', 'edit'))), ... + {'button'})) + args = [args, {'ButtonPushedFcn', spec.callback}]; + else + args = [args, {'ValueChangedFcn', spec.callback}]; + end + end +end + +function placeHandle(h, row, column) + if isempty(h) || ~isvalid(h) + return; + end + h.Layout.Row = row; + h.Layout.Column = column; +end + +function oldCallback = callbackProperty(ctrl) + oldCallback = struct('property', '', 'value', []); + for prop = {'ValueChangedFcn', 'ButtonPushedFcn'} + name = prop{1}; + if isprop(ctrl, name) + oldCallback.property = name; + oldCallback.value = ctrl.(name); + return; + end + end +end + +function cleanupObj = suppressCallback(ctrl, oldCallback, reason) + if strcmp(string(reason), "user") || isempty(oldCallback.property) + cleanupObj = onCleanup(@() []); + return; + end + ctrl.(oldCallback.property) = []; + cleanupObj = onCleanup(@() restoreCallback(ctrl, oldCallback)); +end + +function restoreCallback(ctrl, oldCallback) + if ~isempty(ctrl) && isvalid(ctrl) && isprop(ctrl, oldCallback.property) + ctrl.(oldCallback.property) = oldCallback.value; + end +end + +function tf = valuesEqual(a, b) + try + tf = isequaln(a, b); + catch + tf = false; + end +end + +function text = onOff(value) + if islogical(value) && isscalar(value) + if value + text = 'on'; + else + text = 'off'; + end + else + text = char(string(value)); + end +end + +function value = optionValue(opts, name, defaultValue) + value = defaultValue; + if isstruct(opts) && isfield(opts, name) + value = opts.(name); + end +end diff --git a/+labkit/+ui/+view/panel.m b/+labkit/+ui/+view/panel.m new file mode 100644 index 0000000..aa54212 --- /dev/null +++ b/+labkit/+ui/+view/panel.m @@ -0,0 +1,154 @@ +function ui = panel(parent, kind, varargin) +%PANEL Create a reusable view component group. +% +% App-facing contract: +% ui = labkit.ui.view.panel(parent, kind, ...) +% 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". +% spec - optional struct alternative with kind plus fields matching the +% positional arguments described below. +% +% Positional forms: +% panel(parent, "files", labels, callbacks, opts) +% 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. + + if isstruct(kind) + ui = panelFromSpec(parent, kind); + return; + end + + switch normalizeKind(kind) + case 'files' + labels = positional(varargin, 1, struct()); + callbacks = positional(varargin, 2, struct()); + opts = positional(varargin, 3, struct()); + ui = fileSelectionPanel(parent, labels, callbacks, opts); + case 'log' + row = positional(varargin, 1, 1); + initialValue = positional(varargin, 2, {'GUI started.'}); + ui = logPanel(parent, row, initialValue); + case 'text' + titleText = positional(varargin, 1, ''); + row = positional(varargin, 2, 1); + lines = positional(varargin, 3, {}); + opts = positional(varargin, 4, struct()); + ui = textPanel(parent, titleText, row, lines, opts); + case 'table' + titleText = positional(varargin, 1, ''); + row = positional(varargin, 2, 1); + 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)); + end +end + +function ui = panelFromSpec(parent, spec) + kind = requireField(spec, 'kind'); + switch normalizeKind(kind) + case 'files' + labels = fieldOr(spec, 'labels', struct()); + callbacks = fieldOr(spec, 'callbacks', struct()); + opts = mergeFieldOptions(fieldOr(spec, 'options', struct()), spec, {'row'}); + ui = fileSelectionPanel(parent, labels, callbacks, opts); + case 'log' + ui = logPanel(parent, fieldOr(spec, 'row', 1), ... + fieldOr(spec, 'initialValue', {'GUI started.'})); + case 'text' + ui = textPanel(parent, fieldOr(spec, 'title', ''), ... + fieldOr(spec, 'row', 1), fieldOr(spec, 'lines', {}), ... + fieldOr(spec, 'options', struct())); + case 'table' + columnNames = fieldOr(spec, 'columnNames', {}); + 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)); + end +end + +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) + value = defaultValue; + if numel(args) >= index && ~isempty(args{index}) + value = args{index}; + end +end + +function value = fieldOr(spec, name, defaultValue) + value = defaultValue; + if isfield(spec, name) && ~isempty(spec.(name)) + value = spec.(name); + end +end + +function value = requireField(spec, name) + if ~isfield(spec, name) || isempty(spec.(name)) + error('labkit_ui:panel:MissingField', ... + 'labkit.ui.view.panel spec requires field "%s".', name); + end + value = spec.(name); +end + +function opts = mergeFieldOptions(opts, spec, names) + for k = 1:numel(names) + name = names{k}; + if isfield(spec, name) && ~isempty(spec.(name)) + opts.(name) = spec.(name); + end + end +end diff --git a/+labkit/+ui/+view/place.m b/+labkit/+ui/+view/place.m new file mode 100644 index 0000000..25b261b --- /dev/null +++ b/+labkit/+ui/+view/place.m @@ -0,0 +1,21 @@ +function place(component, parent, row, column) +%PLACE Place a component on a LabKit logical shell row. +% +% Inputs: +% component - MATLAB UI component with a Layout property. +% parent - parent grid. Shell tab grids may contain a private logical row +% map inserted by labkit.ui.app.createShell. +% row - logical row in the parent grid. +% column - optional Layout.Column value. +% +% Output: +% Mutates component.Layout.Row and optionally component.Layout.Column. + + if isempty(component) || ~isvalid(component) + return; + end + component.Layout.Row = layoutRow(parent, row); + if nargin >= 4 && ~isempty(column) + component.Layout.Column = column; + end +end diff --git a/+labkit/+ui/appendLog.m b/+labkit/+ui/+view/private/appendLog.m similarity index 100% rename from +labkit/+ui/appendLog.m rename to +labkit/+ui/+view/private/appendLog.m diff --git a/+labkit/+ui/clearAxisObjects.m b/+labkit/+ui/+view/private/clearAxes.m similarity index 70% rename from +labkit/+ui/clearAxisObjects.m rename to +labkit/+ui/+view/private/clearAxes.m index 0b71504..ee964b5 100644 --- a/+labkit/+ui/clearAxisObjects.m +++ b/+labkit/+ui/+view/private/clearAxes.m @@ -1,5 +1,5 @@ -function clearAxisObjects(ax) -%CLEARAXISOBJECTS Remove plotted children without resetting axes configuration. +function clearAxes(ax) +%CLEARAXES Remove plotted children without resetting axes configuration. % % Inputs: % ax - target axes. diff --git a/+labkit/+ui/+view/private/createLabeledDropdown.m b/+labkit/+ui/+view/private/createLabeledDropdown.m new file mode 100644 index 0000000..6971754 --- /dev/null +++ b/+labkit/+ui/+view/private/createLabeledDropdown.m @@ -0,0 +1,17 @@ +function [lbl, dd] = createLabeledDropdown(parent, labelText, varargin) +%CREATELABELEDDROPDOWN Create a right-aligned label followed by a dropdown. +% +% Inputs: +% parent - parent grid. +% labelText - visible label. +% varargin - name/value arguments forwarded to uidropdown. +% +% Output: +% lbl - uilabel handle. +% dd - uidropdown handle. + + lbl = uilabel(parent, ... + 'Text', labelText, ... + 'HorizontalAlignment', 'right'); + dd = uidropdown(parent, varargin{:}); +end diff --git a/+labkit/+ui/+view/private/createLabeledEditField.m b/+labkit/+ui/+view/private/createLabeledEditField.m new file mode 100644 index 0000000..f7aa38d --- /dev/null +++ b/+labkit/+ui/+view/private/createLabeledEditField.m @@ -0,0 +1,18 @@ +function [lbl, field] = createLabeledEditField(parent, labelText, style, varargin) +%CREATELABELEDEDITFIELD Create a right-aligned label followed by an edit field. +% +% Inputs: +% parent - parent grid. +% labelText - visible label. +% style - uieditfield style, for example "text" or "numeric". +% varargin - name/value arguments forwarded to uieditfield. +% +% Output: +% lbl - uilabel handle. +% field - uieditfield handle. + + lbl = uilabel(parent, ... + 'Text', labelText, ... + 'HorizontalAlignment', 'right'); + field = uieditfield(parent, style, varargin{:}); +end diff --git a/+labkit/+ui/+view/private/createLabeledSpinner.m b/+labkit/+ui/+view/private/createLabeledSpinner.m new file mode 100644 index 0000000..63c3dd2 --- /dev/null +++ b/+labkit/+ui/+view/private/createLabeledSpinner.m @@ -0,0 +1,17 @@ +function [lbl, spinner] = createLabeledSpinner(parent, labelText, varargin) +%CREATELABELEDSPINNER Create a right-aligned label followed by a spinner. +% +% Inputs: +% parent - parent grid. +% labelText - visible label. +% varargin - name/value arguments forwarded to uispinner. +% +% Output: +% lbl - uilabel handle. +% spinner - uispinner handle. + + lbl = uilabel(parent, ... + 'Text', labelText, ... + 'HorizontalAlignment', 'right'); + spinner = uispinner(parent, varargin{:}); +end diff --git a/+labkit/+ui/+view/private/createReadOnlyInfoRow.m b/+labkit/+ui/+view/private/createReadOnlyInfoRow.m new file mode 100644 index 0000000..2f424eb --- /dev/null +++ b/+labkit/+ui/+view/private/createReadOnlyInfoRow.m @@ -0,0 +1,20 @@ +function [field, lbl] = createReadOnlyInfoRow(parent, row, labelText) +%CREATEREADONLYINFOROW Create a labeled read-only text field row. +% +% Inputs: +% parent - parent grid. +% row - row inside parent. +% labelText - visible label. +% +% Output: +% field - read-only text field initialized to "-". +% lbl - label handle. + + [lbl, field] = createLabeledEditField(parent, labelText, 'text', ... + 'Editable', 'off', ... + 'Value', '-'); + lbl.Layout.Row = row; + lbl.Layout.Column = 1; + field.Layout.Row = row; + field.Layout.Column = 2; +end diff --git a/+labkit/+ui/+view/private/createReadOnlyTextField.m b/+labkit/+ui/+view/private/createReadOnlyTextField.m new file mode 100644 index 0000000..dc0a51d --- /dev/null +++ b/+labkit/+ui/+view/private/createReadOnlyTextField.m @@ -0,0 +1,14 @@ +function field = createReadOnlyTextField(parent, varargin) +%CREATEREADONLYTEXTFIELD Create a read-only single-line text field. +% +% Inputs: +% parent - parent grid. +% varargin - name/value arguments forwarded to uieditfield. +% +% Output: +% field - read-only text uieditfield handle. + + field = uieditfield(parent, 'text', ... + 'Editable', 'off', ... + varargin{:}); +end diff --git a/+labkit/+ui/enableAxesPopout.m b/+labkit/+ui/+view/private/enablePopout.m similarity index 95% rename from +labkit/+ui/enableAxesPopout.m rename to +labkit/+ui/+view/private/enablePopout.m index 00d8ca9..eba465c 100644 --- a/+labkit/+ui/enableAxesPopout.m +++ b/+labkit/+ui/+view/private/enablePopout.m @@ -1,4 +1,4 @@ -function enableAxesPopout(ax) +function enablePopout(ax) %ENABLEAXESPOPOUT Add a context-menu action to copy an axes to a figure. % % Inputs: @@ -34,7 +34,7 @@ function enableAxesPopout(ax) uimenu(menu, ... 'Text', 'Open axes in new figure', ... 'Tag', 'labkitAxesPopoutMenu', ... - 'MenuSelectedFcn', @(~,~) labkit.ui.popoutAxes(ax)); + 'MenuSelectedFcn', @(~,~) popoutAxes(ax)); end attachMenuToAxesChildren(ax, menu); installChildrenListener(ax, menu); diff --git a/+labkit/+ui/createFileSelectionPanel.m b/+labkit/+ui/+view/private/fileSelectionPanel.m similarity index 96% rename from +labkit/+ui/createFileSelectionPanel.m rename to +labkit/+ui/+view/private/fileSelectionPanel.m index e13a5be..81eaa48 100644 --- a/+labkit/+ui/createFileSelectionPanel.m +++ b/+labkit/+ui/+view/private/fileSelectionPanel.m @@ -1,10 +1,10 @@ -function ui = createFileSelectionPanel(parent, labels, callbacks, opts) +function ui = fileSelectionPanel(parent, labels, callbacks, opts) %CREATEFILESELECTIONPANEL Create a shared file-action panel with a listbox. % % Usage: % labels = struct('panelTitle','Files','openFiles','Open file(s)'); % callbacks = struct('onOpenFiles',@onOpen,'onExport',@onExport); -% ui = labkit.ui.createFileSelectionPanel(parent, labels, callbacks); +% ui = fileSelectionPanel(parent, labels, callbacks); % % Inputs: % parent - parent grid. @@ -41,7 +41,7 @@ 'rowHeight', {{'fit', '1x', 'fit'}}, ... 'columnWidth', {{'1x'}}, ... 'columnSpacing', 0); - ui = labkit.ui.createPanelGrid( ... + ui = labkit.ui.view.section( ... parent, labelValue(labels, 'panelTitle', 'Files'), row, [3 1], gridOpts); if showRemoveSelected diff --git a/+labkit/+ui/layoutRow.m b/+labkit/+ui/+view/private/layoutRow.m similarity index 100% rename from +labkit/+ui/layoutRow.m rename to +labkit/+ui/+view/private/layoutRow.m diff --git a/+labkit/+ui/createLogPanel.m b/+labkit/+ui/+view/private/logPanel.m similarity index 82% rename from +labkit/+ui/createLogPanel.m rename to +labkit/+ui/+view/private/logPanel.m index 99d8bc3..492e5b5 100644 --- a/+labkit/+ui/createLogPanel.m +++ b/+labkit/+ui/+view/private/logPanel.m @@ -1,4 +1,4 @@ -function ui = createLogPanel(parent, row, initialValue) +function ui = logPanel(parent, row, initialValue) %CREATELOGPANEL Create a log panel with a read-only text area. % % Inputs: @@ -17,7 +17,7 @@ end opts = struct('rowHeight', {{'1x'}}, 'columnWidth', {{'1x'}}); - ui = labkit.ui.createPanelGrid(parent, 'Log', row, [1 1], opts); + ui = labkit.ui.view.section(parent, 'Log', row, [1 1], opts); ui.textArea = uitextarea(ui.grid, ... 'Editable', 'off', ... diff --git a/+labkit/+ui/createPlotOptionsPanel.m b/+labkit/+ui/+view/private/plotOptionsPanel.m similarity index 60% rename from +labkit/+ui/createPlotOptionsPanel.m rename to +labkit/+ui/+view/private/plotOptionsPanel.m index 1994ca0..5523fce 100644 --- a/+labkit/+ui/createPlotOptionsPanel.m +++ b/+labkit/+ui/+view/private/plotOptionsPanel.m @@ -1,4 +1,4 @@ -function ui = createPlotOptionsPanel(parent, rowCount, row) +function ui = plotOptionsPanel(parent, rowCount, row) %CREATEPLOTOPTIONSPANEL Create the shared plot-options panel grid. % % Inputs: @@ -7,11 +7,11 @@ % row - optional logical parent row, default 3. % % Output: -% ui - panel-grid struct from createPanelGrid. +% ui - section struct from labkit.ui.view.section. if nargin < 3 || isempty(row) row = 3; end - ui = labkit.ui.createPanelGrid(parent, 'Plot Options', row, [rowCount 2]); + ui = labkit.ui.view.section(parent, 'Plot Options', row, [rowCount 2]); end diff --git a/+labkit/+ui/plotXY.m b/+labkit/+ui/+view/private/plotXY.m similarity index 96% rename from +labkit/+ui/plotXY.m rename to +labkit/+ui/+view/private/plotXY.m index c3138c9..cde6f90 100644 --- a/+labkit/+ui/plotXY.m +++ b/+labkit/+ui/+view/private/plotXY.m @@ -2,7 +2,7 @@ %PLOTXY Plot one prepared X/Y numeric series. % % Usage: -% info = labkit.ui.plotXY(ax, x, y, struct('x','Time','y','Voltage')); +% info = plotXY(ax, x, y, struct('x','Time','y','Voltage')); % % Inputs: % ax - target axes. diff --git a/+labkit/+ui/popoutAxes.m b/+labkit/+ui/+view/private/popoutAxes.m similarity index 100% rename from +labkit/+ui/popoutAxes.m rename to +labkit/+ui/+view/private/popoutAxes.m diff --git a/+labkit/+ui/refreshListboxItems.m b/+labkit/+ui/+view/private/refreshListboxItems.m similarity index 75% rename from +labkit/+ui/refreshListboxItems.m rename to +labkit/+ui/+view/private/refreshListboxItems.m index c2bfea0..052e55b 100644 --- a/+labkit/+ui/refreshListboxItems.m +++ b/+labkit/+ui/+view/private/refreshListboxItems.m @@ -8,5 +8,5 @@ function refreshListboxItems(lb, names) % Output: % Mutates lb.Items and lb.Value in place, defaulting to all items. - labkit.ui.refreshListboxSelection(lb, names, lb.Value, struct('defaultSelection', 'all')); + refreshListboxSelection(lb, names, lb.Value, struct('defaultSelection', 'all')); end diff --git a/+labkit/+ui/refreshListboxSelection.m b/+labkit/+ui/+view/private/refreshListboxSelection.m similarity index 95% rename from +labkit/+ui/refreshListboxSelection.m rename to +labkit/+ui/+view/private/refreshListboxSelection.m index 13c2d66..2048755 100644 --- a/+labkit/+ui/refreshListboxSelection.m +++ b/+labkit/+ui/+view/private/refreshListboxSelection.m @@ -2,8 +2,8 @@ %REFRESHLISTBOXSELECTION Refresh listbox items while preserving valid selection. % % Usage: -% [value, idx] = labkit.ui.refreshListboxSelection(lb, names, oldValue); -% [value, idx] = labkit.ui.refreshListboxSelection(lb, names, [], ... +% [value, idx] = refreshListboxSelection(lb, names, oldValue); +% [value, idx] = refreshListboxSelection(lb, names, [], ... % struct('defaultSelection', 'all')); % % Inputs: diff --git a/+labkit/+ui/hardResetAxis.m b/+labkit/+ui/+view/private/resetAxes.m similarity index 89% rename from +labkit/+ui/hardResetAxis.m rename to +labkit/+ui/+view/private/resetAxes.m index fbce4ab..b4de3a0 100644 --- a/+labkit/+ui/hardResetAxis.m +++ b/+labkit/+ui/+view/private/resetAxes.m @@ -1,4 +1,4 @@ -function hardResetAxis(ax, ttl, resetScaleAndTicks) +function resetAxes(ax, ttl, resetScaleAndTicks) %HARDRESETAXIS Reset an app axes to an empty titled state. % % Inputs: @@ -29,5 +29,5 @@ function hardResetAxis(ax, ttl, resetScaleAndTicks) ylabel(ax, ''); grid(ax, 'off'); box(ax, 'on'); - labkit.ui.enableAxesPopout(ax); + enablePopout(ax); end diff --git a/+labkit/+ui/createResultTablePanel.m b/+labkit/+ui/+view/private/resultTable.m similarity index 78% rename from +labkit/+ui/createResultTablePanel.m rename to +labkit/+ui/+view/private/resultTable.m index e3499d5..3683921 100644 --- a/+labkit/+ui/createResultTablePanel.m +++ b/+labkit/+ui/+view/private/resultTable.m @@ -1,4 +1,4 @@ -function ui = createResultTablePanel(parent, titleText, row, columnNames, initialData) +function ui = resultTable(parent, titleText, row, columnNames, initialData) %CREATERESULTTABLEPANEL Create a titled result-table panel. % % Inputs: @@ -16,7 +16,7 @@ end opts = struct('rowHeight', {{'1x'}}, 'columnWidth', {{'1x'}}); - ui = labkit.ui.createPanelGrid(parent, titleText, row, [1 1], opts); + ui = labkit.ui.view.section(parent, titleText, row, [1 1], opts); ui.table = uitable(ui.grid); ui.table.ColumnName = columnNames; diff --git a/+labkit/+ui/setTopBottomPlotSelections.m b/+labkit/+ui/+view/private/setTopBottomPlotSelections.m similarity index 100% rename from +labkit/+ui/setTopBottomPlotSelections.m rename to +labkit/+ui/+view/private/setTopBottomPlotSelections.m diff --git a/+labkit/+ui/showImageAxes.m b/+labkit/+ui/+view/private/showImage.m similarity index 87% rename from +labkit/+ui/showImageAxes.m rename to +labkit/+ui/+view/private/showImage.m index e7b2708..df14074 100644 --- a/+labkit/+ui/showImageAxes.m +++ b/+labkit/+ui/+view/private/showImage.m @@ -1,9 +1,9 @@ -function hImage = showImageAxes(ax, imageData, titleText, opts) +function hImage = showImage(ax, imageData, titleText, opts) %SHOWIMAGEAXES Render an image in a UI axes with LabKit defaults. % % Usage: -% hImage = labkit.ui.showImageAxes(ax, rgbImage, 'Reference'); -% hImage = labkit.ui.showImageAxes(ax, img, 'Mask', ... +% hImage = showImage(ax, rgbImage, 'Reference'); +% hImage = showImage(ax, img, 'Mask', ... % struct('hitTest', 'on', 'pickableParts', 'all')); % % Inputs: @@ -44,7 +44,7 @@ if optionValue(opts, 'enableNavigation', true) enableImageNavigation(ax); end - labkit.ui.enableAxesPopout(ax); + enablePopout(ax); end function enableImageNavigation(ax) diff --git a/+labkit/+ui/swapTopBottomPlotSelections.m b/+labkit/+ui/+view/private/swapTopBottomPlotSelections.m similarity index 100% rename from +labkit/+ui/swapTopBottomPlotSelections.m rename to +labkit/+ui/+view/private/swapTopBottomPlotSelections.m diff --git a/+labkit/+ui/createReadOnlyTextPanel.m b/+labkit/+ui/+view/private/textPanel.m similarity index 81% rename from +labkit/+ui/createReadOnlyTextPanel.m rename to +labkit/+ui/+view/private/textPanel.m index ce7c286..ec530e0 100644 --- a/+labkit/+ui/createReadOnlyTextPanel.m +++ b/+labkit/+ui/+view/private/textPanel.m @@ -1,4 +1,4 @@ -function ui = createReadOnlyTextPanel(parent, titleText, row, lines, opts) +function ui = textPanel(parent, titleText, row, lines, opts) %CREATEREADONLYTEXTPANEL Create a titled read-only multi-line text panel. % % Inputs: @@ -9,7 +9,7 @@ % opts - optional struct. % % Options: -% panelOptions - struct forwarded to createPanelGrid. +% panelOptions - struct forwarded to labkit.ui.view.section. % % Output: % ui - struct with panel, grid, and textArea fields. @@ -28,7 +28,7 @@ panelOpts = mergeStruct(panelOpts, opts.panelOptions); end - ui = labkit.ui.createPanelGrid(parent, titleText, row, [1 1], panelOpts); + ui = labkit.ui.view.section(parent, titleText, row, [1 1], panelOpts); ui.textArea = uitextarea(ui.grid, 'Editable', 'off'); ui.textArea.Value = lines; end diff --git a/+labkit/+ui/createTopBottomPlotControls.m b/+labkit/+ui/+view/private/topBottomPlotControls.m similarity index 92% rename from +labkit/+ui/createTopBottomPlotControls.m rename to +labkit/+ui/+view/private/topBottomPlotControls.m index 4b3ac7c..6103a59 100644 --- a/+labkit/+ui/createTopBottomPlotControls.m +++ b/+labkit/+ui/+view/private/topBottomPlotControls.m @@ -1,4 +1,4 @@ -function ui = createTopBottomPlotControls(topPanel, bottomPanel, xItems, yItems, topDefaults, bottomDefaults, valueChangedFcn) +function ui = topBottomPlotControls(topPanel, bottomPanel, xItems, yItems, topDefaults, bottomDefaults, valueChangedFcn) %CREATETOPBOTTOMPLOTCONTROLS Create shared top/bottom plot controls. % % Inputs: diff --git a/+labkit/+ui/createPanelGrid.m b/+labkit/+ui/+view/section.m similarity index 93% rename from +labkit/+ui/createPanelGrid.m rename to +labkit/+ui/+view/section.m index 020d983..1f958f1 100644 --- a/+labkit/+ui/createPanelGrid.m +++ b/+labkit/+ui/+view/section.m @@ -1,9 +1,9 @@ -function ui = createPanelGrid(parent, titleText, row, gridSize, opts) +function ui = section(parent, titleText, row, gridSize, opts) %CREATEPANELGRID Create a standard titled panel containing a grid layout. % % Usage: -% ui = labkit.ui.createPanelGrid(parent, 'Inputs', 1, [3 2]); -% ui = labkit.ui.createPanelGrid(parent, 'Inputs', 1, [3 2], ... +% ui = labkit.ui.view.section(parent, 'Inputs', 1, [3 2]); +% ui = labkit.ui.view.section(parent, 'Inputs', 1, [3 2], ... % struct('rowHeight', {{'fit','fit','1x'}}, 'columnWidth', {{120,'1x'}})); % % Inputs: @@ -32,7 +32,7 @@ ui = struct(); ui.panel = uipanel(parent, 'Title', titleText); if nargin >= 3 && ~isempty(row) - ui.panel.Layout.Row = labkit.ui.layoutRow(parent, row); + ui.panel.Layout.Row = layoutRow(parent, row); end ui.grid = uigridlayout(ui.panel, gridSize); diff --git a/+labkit/+ui/+view/update.m b/+labkit/+ui/+view/update.m new file mode 100644 index 0000000..15f291a --- /dev/null +++ b/+labkit/+ui/+view/update.m @@ -0,0 +1,77 @@ +function varargout = update(target, action, varargin) +%UPDATE Apply an app-neutral state update to an existing UI handle group. +% +% App-facing contract: +% 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(). +% action - state update action. +% varargin - action-specific payload described above. +% +% Output: +% listSelection returns the applied value and selected indices. Other +% actions mutate target in place and return [] when captured. + + switch normalizeAction(action) + case 'appendlog' + appendLog(target, positional(varargin, 1, '')); + out = {[]}; + case 'listitems' + refreshListboxItems(target, positional(varargin, 1, {})); + out = {[]}; + case 'listselection' + names = positional(varargin, 1, {}); + preferredSelection = positional(varargin, 2, target.Value); + 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)); + end + + for k = 1:min(nargout, numel(out)) + varargout{k} = out{k}; + end + for k = (numel(out) + 1):nargout + varargout{k} = []; + end +end + +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) + value = defaultValue; + if numel(args) >= index && ~isempty(args{index}) + value = args{index}; + end +end diff --git a/+labkit/+ui/createAppDebugLog.m b/+labkit/+ui/createAppDebugLog.m deleted file mode 100644 index 0cd3d7f..0000000 --- a/+labkit/+ui/createAppDebugLog.m +++ /dev/null @@ -1,15 +0,0 @@ -function debugLog = createAppDebugLog(appName, opts) -%CREATEAPPDEBUGLOG Deprecated wrapper for createDebugContext. -% -% Usage: -% debugLog = labkit.ui.createAppDebugLog(appName, opts); -% -% This compatibility surface is retained for one migration cycle. New app -% code should call labkit.ui.createDebugContext so debug launch, trace, and -% callback instrumentation all use the current UI diagnostics contract. - - if nargin < 2 - opts = struct(); - end - debugLog = labkit.ui.createDebugContext(appName, opts); -end diff --git a/+labkit/+ui/createAppShell.m b/+labkit/+ui/createAppShell.m deleted file mode 100644 index 3759dcd..0000000 --- a/+labkit/+ui/createAppShell.m +++ /dev/null @@ -1,49 +0,0 @@ -function ui = createAppShell(spec) -%CREATEAPPSHELL Create the standard LabKit app shell from a named spec. -% -% Usage: -% spec = struct('title', "Example", ... -% 'position', [90 70 1200 800], ... -% 'leftWidth', 360, ... -% 'options', struct('rightKind', 'dualPlot')); -% ui = labkit.ui.createAppShell(spec); -% -% Inputs: -% spec - struct with fields: -% title - figure title text. -% position - MATLAB figure position [x y width height]. -% leftWidth - initial left controls width in pixels. -% options - optional app shell options. -% -% Output: -% ui - struct of figure, layout, tab grids, and right-side handles. -% -% This is the app-facing shell entry point. Apps own controls and workflow -% inside the returned grids; the shell owns split-pane layout, tab host -% construction, scrollable tab grids, and standard right-pane plumbing. - - if nargin < 1 || ~isstruct(spec) - error('labkit:ui:InvalidAppShellSpec', ... - 'createAppShell requires a scalar struct spec.'); - end - required = {'title', 'position', 'leftWidth'}; - for k = 1:numel(required) - if ~isfield(spec, required{k}) - error('labkit:ui:InvalidAppShellSpec', ... - 'App shell spec is missing "%s".', required{k}); - end - end - - opts = optionValue(spec, 'options', struct()); - if isfield(spec, 'shellOptions') - opts = spec.shellOptions; - end - ui = labkit.ui.createWorkbench(spec.title, spec.position, spec.leftWidth, opts); -end - -function value = optionValue(opts, name, defaultValue) - value = defaultValue; - if isstruct(opts) && isfield(opts, name) - value = opts.(name); - end -end diff --git a/+labkit/+ui/createInteractionRuntime.m b/+labkit/+ui/createInteractionRuntime.m deleted file mode 100644 index 5a31202..0000000 --- a/+labkit/+ui/createInteractionRuntime.m +++ /dev/null @@ -1,37 +0,0 @@ -function runtime = createInteractionRuntime(ax, opts) -%CREATEINTERACTIONRUNTIME Create a managed interaction runtime for axes tools. -% -% Usage: -% runtime = labkit.ui.createInteractionRuntime(ax, ... -% struct('figure', fig, 'defaultScrollFcn', @onPreviewScroll)); -% session = runtime.createSession(struct( ... -% 'name', 'toolName', ... -% 'onPointerDown', @onPointerDown, ... -% 'onScroll', @onScroll, ... -% 'installScrollWheel', true)); -% -% Inputs: -% ax - UI axes used by an image or axes interaction tool. -% opts - optional struct. -% -% Options: -% figure - owning figure, default ancestor(ax, 'figure'). -% defaultScrollFcn - default scroll callback restored when no session owns -% scrolling, default []. -% onInteractionChanged - callback(active, name), default []. -% onTrace - callback(message), default []. Receives verbose lifecycle trace. -% -% Output: -% runtime - struct with axes, figure, setFigure, setDefaultScrollFcn, -% setTraceCallback, installDefaultCallbacks, createSession, -% isInteractionActive, and delete. -% -% Runtime sessions own pointer, drag, scroll, hit-test, and callback restore -% mechanics. Apps and composed tools should use sessions instead of mutating -% figure or axes pointer callbacks directly. - - if nargin < 2 - opts = struct(); - end - runtime = labkit.ui.createImageAxesRuntime(ax, opts); -end diff --git a/+labkit/+ui/handleAppRequest.m b/+labkit/+ui/handleAppRequest.m deleted file mode 100644 index 6e3a92e..0000000 --- a/+labkit/+ui/handleAppRequest.m +++ /dev/null @@ -1,17 +0,0 @@ -function [handled, outputs, debugLog] = handleAppRequest(appName, args, nout, handlers) -%HANDLEAPPREQUEST Deprecated wrapper for dispatchAppRequest. -% -% Usage: -% [handled, outputs, debugLog] = labkit.ui.handleAppRequest( ... -% appName, varargin, nargout, handlers); -% -% This compatibility surface is retained for one migration cycle. New app -% code should call labkit.ui.dispatchAppRequest. - - if nargin < 4 - handlers = struct('command', {}, 'minArgs', {}, ... - 'maxArgs', {}, 'maxOutputs', {}, 'run', {}); - end - [handled, outputs, debugLog] = labkit.ui.dispatchAppRequest( ... - appName, args, nout, handlers); -end diff --git a/+labkit/AGENTS.md b/+labkit/AGENTS.md index 5d8ff82..6b8febd 100644 --- a/+labkit/AGENTS.md +++ b/+labkit/AGENTS.md @@ -20,8 +20,8 @@ - `labkit.biosignal` stays GUI-free and independent from DTA/app code. - `labkit.ui` stays parser/data/analysis-free; apps pass prepared values, labels, tables, callbacks, and handles into UI helpers. - Reusable UI tools may own domain-neutral interaction workflows such as image scale-bar controls, reference editing, unit normalization, and overlay placement. Keep those tools independent from app result schemas, scientific formulas, file formats, and workflow wording. -- App-facing UI APIs are `labkit.ui.createAppShell`, `labkit.ui.createDebugContext`, `labkit.ui.createInteractionRuntime`, and `labkit.ui.dispatchAppRequest`. Treat `createWorkbench`, `createAppDebugLog`, `createImageAxesRuntime`, and `handleAppRequest` as deprecated compatibility surface. -- Image-axis tools that need pointer, drag, scroll, or hit-test ownership must use `labkit.ui.createInteractionRuntime` sessions instead of each helper managing figure/axes callbacks independently. +- App-facing UI APIs live under `labkit.ui.app.*`, `labkit.ui.view.*`, `labkit.ui.tool.*`, and `labkit.ui.diag.*`. Do not reintroduce flat `labkit.ui.*` helper files. +- Image-axis tools that need pointer, drag, scroll, or hit-test ownership must use `labkit.ui.tool.createRuntime` sessions instead of each helper managing figure/axes callbacks independently. - Tool callbacks must keep user-facing semantic callbacks separate from internal refresh/sync callbacks, no-op when a setter receives the current value, and trace callback reason/source when a tool exposes debug trace. - Do not introduce MATLAB classes unless explicitly approved. diff --git a/.agents/skills/labkit-app-builder/SKILL.md b/.agents/skills/labkit-app-builder/SKILL.md index 2a9bdcf..daa431f 100644 --- a/.agents/skills/labkit-app-builder/SKILL.md +++ b/.agents/skills/labkit-app-builder/SKILL.md @@ -104,7 +104,7 @@ Use the closest existing app as the starting pattern, then reduce it to the actu Build the app in this order: -1. Add or update the app entry point with `labkit.ui.createAppShell`. +1. Add or update the app entry point with `labkit.ui.app.createShell`. 2. Wire file loading through the appropriate facade or app-local reader. 3. Store state in one app struct; avoid globals, base workspace state, and hidden local paths. 4. Rebuild the user workflow around stable controls, previews, summaries, and exports; do not reproduce command-line debug staging. diff --git a/.agents/skills/labkit-boundary-guard/SKILL.md b/.agents/skills/labkit-boundary-guard/SKILL.md index 851901a..9cb7ea9 100644 --- a/.agents/skills/labkit-boundary-guard/SKILL.md +++ b/.agents/skills/labkit-boundary-guard/SKILL.md @@ -12,7 +12,7 @@ Preserve LabKit's app-first architecture: - apps own experiment-specific workflow - `+labkit` owns small, stable UI/DTA/biosignal facades - no public helper-dump packages -- UI apps should use the stable app-facing shell/debug/request/runtime APIs and treat older helper names as deprecated compatibility surface +- UI apps should use the layered `labkit.ui.app/view/tool/diag` facades; the older flat helper surface has been removed ## Required Read Order @@ -35,7 +35,7 @@ Before moving code into `+labkit`, prove that the helper: If this is not proven, keep the code app-local. -For UI boundary work, prefer `labkit.ui.createAppShell`, `labkit.ui.createDebugContext`, `labkit.ui.createInteractionRuntime`, and `labkit.ui.dispatchAppRequest`. Keep control micro-helpers private unless they are a deliberate public facade addition. +For UI boundary work, prefer `labkit.ui.app.createShell`, `labkit.ui.app.dispatchRequest`, `labkit.ui.diag.createContext`, `labkit.ui.tool.createRuntime`, and the unified `labkit.ui.view.section/form/panel/axes/draw/update/place` facade. Keep control micro-helpers and one-off component builders private unless they are a deliberate public facade addition. ## Validation diff --git a/AGENTS.md b/AGENTS.md index c03b8c9..bb5ff64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ Read component docs only when relevant: - Preserve behavior unless the user explicitly asks for a behavior change. - Keep app-specific formulas, thresholds, plots, result schemas, exports, and workflow decisions in the owning app. - Keep reusable `+labkit` API growth conservative and domain-neutral. -- New app-facing UI work should use `labkit.ui.createAppShell`, `labkit.ui.createDebugContext`, `labkit.ui.createInteractionRuntime`, and `labkit.ui.dispatchAppRequest`; older UI helper names are deprecated compatibility surface. +- New app-facing UI work should use `labkit.ui.app.*`, `labkit.ui.view.*`, `labkit.ui.tool.*`, and `labkit.ui.diag.*`; the older flat `labkit.ui.*` helper surface has been removed. - New app code must not call `labkit.io.*`, `labkit.data.*`, `labkit.analysis.*`, or `labkit.util.*`; use `labkit.dta.*`, `labkit.biosignal.*`, `labkit.ui.*`, or app-local helpers. - Do not reintroduce root-level legacy command wrappers, app-specific public helper packages, or public helper-dump packages such as `+labkit/+analysis`, `+data`, `+io`, or `+util`. - Do not convert struct models to MATLAB classes, rewrite all GUIs, replace separate app entry points with one launcher, or migrate code to another language without explicit approval. diff --git a/README.md b/README.md index 34241c3..f5ae8a1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ LabKit MATLAB Workbench is an internal app workbench for scientific GUI tools. I Current app families cover electrochemistry, DIC image workflows, image measurement, and wearable biosignal review. Reusable code is organized behind three app-facing facades: -- `labkit.ui.*`: layered GUI foundation for app shells, controls, axes, interaction runtimes, tools, diagnostics, logs, and image/plot helpers. +- `labkit.ui.app/view/tool/diag`: layered GUI foundation for app shells, views/forms, axes rendering, interaction runtimes, composed tools, diagnostics, logs, and image/plot helpers. - `labkit.dta.*`: GUI-free Gamry DTA loading, sessions, parsed curve access, and pulse helpers. - `labkit.biosignal.*`: GUI-free recording loading, filtering, ECG peak detection, segments, templates, and SNR-style measurements. diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 98c9633..eb3b14c 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -14,10 +14,10 @@ Apps are first-class deliverables. Do not treat them as examples for a hidden pl - Keep domain formulas, thresholds, integration rules, option defaults, plot labels, result fields, export columns, failed-row behavior, alerts, and log wording app-local unless the user explicitly approves a boundary change. - When a documented UI tool owns app-neutral controls or interaction mechanics, consume it instead of reimplementing widget state or normalization. Keep app calculations, summaries, alerts, and exports local. -- Use `labkit.ui.createAppShell` for app GUIs. -- Use `labkit.ui.dispatchAppRequest` for internal test/debug launch routing and `labkit.ui.createDebugContext` only when an app has an app-specific nonstandard request path. +- Use `labkit.ui.app.createShell` for app GUIs. +- Use `labkit.ui.app.dispatchRequest` for internal test/debug launch routing and `labkit.ui.diag.createContext` only when an app has an app-specific nonstandard request path. - Debug launches should attach the Log tab text area, emit a startup trace line, and instrument high-level component callbacks after controls are built. -- Image apps with custom preview scroll, drawing, ROI, scale-bar, or other axes interaction should create a `labkit.ui.createInteractionRuntime` and pass that runtime into reusable tools. Do not set image-tool `WindowScrollWheelFcn`, `WindowButtonMotionFcn`, `WindowButtonUpFcn`, or axes `ButtonDownFcn` directly in app code. +- Image apps with custom preview scroll, drawing, ROI, scale-bar, or other axes interaction should create a `labkit.ui.tool.createRuntime` and pass that runtime into reusable tools. Do not set image-tool `WindowScrollWheelFcn`, `WindowButtonMotionFcn`, `WindowButtonUpFcn`, or axes `ButtonDownFcn` directly in app code. - DTA-backed apps use `labkit.dta.*` for discovery, loading, sessions, pulse detection, and parsed curve/table access. - Biosignal-backed apps use `labkit.biosignal.*` for recording loading, channel extraction, waveform processing, events, segments, measurements, and group comparisons. - Do not create app-specific public helper packages to make local workflow code look reusable. diff --git a/apps/dic/labkit_DICPostprocess_app.m b/apps/dic/labkit_DICPostprocess_app.m index 0bbd73e..671df7d 100644 --- a/apps/dic/labkit_DICPostprocess_app.m +++ b/apps/dic/labkit_DICPostprocess_app.m @@ -1,7 +1,7 @@ function varargout = labkit_DICPostprocess_app(varargin) %LABKIT_DICPOSTPROCESS_APP Ncorr strain summary and overlay export app. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_DICPostprocess_app', varargin, nargout); if requestHandled varargout = requestOutputs; @@ -34,16 +34,16 @@ 'bottomPlotTitle', 'EYY Overlay', ... 'showPlotControls', false); workbenchOpts.tabs = [ ... - labkit.ui.tabSpec('filesAnalysis', 'Files + Analysis', [4 1], ... + labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ... {240, 230, 260, 120}, ... struct('resizeRows', [1 2 3], ... 'resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 90))), ... - labkit.ui.tabSpec('summaryResults', 'Summary + Results', [2 1], ... + labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ... {210, '1x'}, ... struct('resizeRows', 1, ... 'resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 90))), ... - labkit.ui.tabSpec('log', 'Log', [1 1], {'1x'})]; - ui = labkit.ui.createAppShell(struct( ... + labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})]; + ui = labkit.ui.app.createShell(struct( ... 'title', 'DIC Strain Postprocess', ... 'position', [90 70 1450 880], ... 'leftWidth', 390, ... @@ -54,7 +54,7 @@ laySR = ui.summaryResultsGrid; layLog = ui.logGrid; - filePanel = labkit.ui.createPanelGrid(layFA, 'Inputs', 1, [6 2], ... + filePanel = labkit.ui.view.section(layFA, 'Inputs', 1, [6 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}}, ... 'columnWidth', {{'1x', '1x'}})); fileGrid = filePanel.grid; @@ -69,13 +69,13 @@ btnMask.Layout.Row = 2; btnMask.Layout.Column = [1 2]; - txtMat = labkit.ui.createReadOnlyTextField(fileGrid, 'Value', 'No MAT file loaded'); + txtMat = labkit.ui.view.form(fileGrid, 'readonly', 'Value', 'No MAT file loaded'); txtMat.Layout.Row = 3; txtMat.Layout.Column = [1 2]; - txtReference = labkit.ui.createReadOnlyTextField(fileGrid, 'Value', 'No reference image loaded'); + txtReference = labkit.ui.view.form(fileGrid, 'readonly', 'Value', 'No reference image loaded'); txtReference.Layout.Row = 4; txtReference.Layout.Column = [1 2]; - txtMask = labkit.ui.createReadOnlyTextField(fileGrid, 'Value', 'No mask image loaded'); + txtMask = labkit.ui.view.form(fileGrid, 'readonly', 'Value', 'No mask image loaded'); txtMask.Layout.Row = 5; txtMask.Layout.Column = [1 2]; @@ -84,43 +84,43 @@ btnGenerate.Layout.Row = 6; btnGenerate.Layout.Column = [1 2]; - optionPanel = labkit.ui.createPanelGrid(layFA, 'Overlay Options', 2, [6 2], ... + optionPanel = labkit.ui.view.section(layFA, 'Overlay Options', 2, [6 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}})); optionGrid = optionPanel.grid; - [~, edAlpha] = labkit.ui.createLabeledSpinner(optionGrid, 'Alpha:', ... + [~, edAlpha] = labkit.ui.view.form(optionGrid, 'spinner', 'Alpha:', ... 'Value', 0.60, 'Limits', [0 1], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edMin] = labkit.ui.createLabeledSpinner(optionGrid, 'Color min:', ... + [~, edMin] = labkit.ui.view.form(optionGrid, 'spinner', 'Color min:', ... 'Value', -0.15, 'Step', 0.01, 'ValueChangedFcn', @onOptionsChanged); - [~, edMax] = labkit.ui.createLabeledSpinner(optionGrid, 'Color max:', ... + [~, edMax] = labkit.ui.view.form(optionGrid, 'spinner', 'Color max:', ... 'Value', 0.15, 'Step', 0.01, 'ValueChangedFcn', @onOptionsChanged); - [~, edOversample] = labkit.ui.createLabeledSpinner(optionGrid, 'Oversample:', ... + [~, edOversample] = labkit.ui.view.form(optionGrid, 'spinner', 'Oversample:', ... 'Value', 6, 'Limits', [1 20], 'Step', 1, 'ValueChangedFcn', @onOptionsChanged); - [~, edSigma] = labkit.ui.createLabeledSpinner(optionGrid, 'Smooth sigma:', ... + [~, edSigma] = labkit.ui.view.form(optionGrid, 'spinner', 'Smooth sigma:', ... 'Value', 0.8, 'Limits', [0 Inf], 'Step', 0.1, 'ValueChangedFcn', @onOptionsChanged); - [~, edResolution] = labkit.ui.createLabeledSpinner(optionGrid, 'Export DPI:', ... + [~, edResolution] = labkit.ui.view.form(optionGrid, 'spinner', 'Export DPI:', ... 'Value', 1000, 'Limits', [72 2400], 'Step', 50); - imagePanel = labkit.ui.createPanelGrid(layFA, 'Optical Image Enhancement', 3, [7 2], ... + 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.createLabeledSpinner(imageGrid, 'Brightness:', ... + [~, edBrightness] = labkit.ui.view.form(imageGrid, 'spinner', 'Brightness:', ... 'Value', 0, 'Limits', [-1 1], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edContrast] = labkit.ui.createLabeledSpinner(imageGrid, 'Contrast:', ... + [~, edContrast] = labkit.ui.view.form(imageGrid, 'spinner', 'Contrast:', ... 'Value', 1, 'Limits', [0.05 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edGamma] = labkit.ui.createLabeledSpinner(imageGrid, 'Gamma:', ... + [~, edGamma] = labkit.ui.view.form(imageGrid, 'spinner', 'Gamma:', ... 'Value', 1, 'Limits', [0.05 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edSaturation] = labkit.ui.createLabeledSpinner(imageGrid, 'Saturation:', ... + [~, edSaturation] = labkit.ui.view.form(imageGrid, 'spinner', 'Saturation:', ... 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edRedGain] = labkit.ui.createLabeledSpinner(imageGrid, 'Red gain:', ... + [~, edRedGain] = labkit.ui.view.form(imageGrid, 'spinner', 'Red gain:', ... 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edGreenGain] = labkit.ui.createLabeledSpinner(imageGrid, 'Green gain:', ... + [~, edGreenGain] = labkit.ui.view.form(imageGrid, 'spinner', 'Green gain:', ... 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - [~, edBlueGain] = labkit.ui.createLabeledSpinner(imageGrid, 'Blue gain:', ... + [~, edBlueGain] = labkit.ui.view.form(imageGrid, 'spinner', 'Blue gain:', ... 'Value', 1, 'Limits', [0 5], 'Step', 0.05, 'ValueChangedFcn', @onOptionsChanged); - exportPanel = labkit.ui.createPanelGrid(layFA, 'Exports', 4, [3 2], ... + exportPanel = labkit.ui.view.section(layFA, 'Exports', 4, [3 2], ... struct('rowHeight', {{'fit', 'fit', 'fit'}}, 'columnWidth', {{'1x', '1x'}})); exportGrid = exportPanel.grid; btnSaveOverlays = uibutton(exportGrid, 'Text', 'Save overlay PNGs', ... @@ -136,15 +136,15 @@ btnExportColorbar.Layout.Row = 3; btnExportColorbar.Layout.Column = [1 2]; - resultUi = labkit.ui.createResultTablePanel(laySR, 'ROI Strain Summary', 1, ... + resultUi = labkit.ui.view.panel(laySR, 'table', 'ROI Strain Summary', 1, ... {'Metric', 'EXX', 'EYY'}); resultTable = resultUi.table; txtSummary = uitextarea(laySR, 'Editable', 'off'); - txtSummary.Layout.Row = labkit.ui.layoutRow(laySR, 2); + labkit.ui.view.place(txtSummary, laySR, 2); txtSummary.Value = {'No DIC result loaded.'}; - logUi = labkit.ui.createLogPanel(layLog, 1, {'Ready.'}); + logUi = labkit.ui.view.panel(layLog, 'log', 1, {'Ready.'}); txtLog = logUi.textArea; if debugLog.enabled debugLog.attachTextLog(txtLog); @@ -152,8 +152,8 @@ debugLog.instrumentFigure(fig); end - labkit.ui.hardResetAxis(ui.topAxes, 'EXX Overlay', true); - labkit.ui.hardResetAxis(ui.bottomAxes, 'EYY Overlay', true); + labkit.ui.view.draw(ui.topAxes, 'reset', 'EXX Overlay', true); + labkit.ui.view.draw(ui.bottomAxes, 'reset', 'EYY Overlay', true); if nargout >= 1 varargout{1} = fig; @@ -349,7 +349,7 @@ function refreshSummaryText() end function addLog(msg) - labkit.ui.appendLog(txtLog, msg); + labkit.ui.view.update(txtLog, 'appendLog', msg); debugLog.append(msg); end end @@ -517,7 +517,7 @@ function addLog(msg) end function showImage(ax, imageData, titleText) - labkit.ui.showImageAxes(ax, imageData, titleText); + labkit.ui.view.draw(ax, 'image', imageData, titleText); end function exportOverlayFigure(overlayImage, componentName, colorRange, resolution, outfile) diff --git a/apps/dic/labkit_DICPreprocess_app.m b/apps/dic/labkit_DICPreprocess_app.m index 39970c4..c6daac2 100644 --- a/apps/dic/labkit_DICPreprocess_app.m +++ b/apps/dic/labkit_DICPreprocess_app.m @@ -1,7 +1,7 @@ function varargout = labkit_DICPreprocess_app(varargin) %LABKIT_DICPREPROCESS_APP Image registration and paired-crop app for DIC workflows. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_DICPreprocess_app', varargin, nargout); if requestHandled varargout = requestOutputs; @@ -47,28 +47,28 @@ 'bottomPlotTitle', 'Current Preview', ... 'showPlotControls', false); workbenchOpts.tabs = [ ... - labkit.ui.tabSpec('filesAnalysis', 'Files + Analysis', [4 1], ... + labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ... {240, 210, 330, 170}, ... struct('resizeRows', [1 2 3], ... 'resizeOptions', struct('minTopHeight', 120, 'minBottomHeight', 80))), ... - labkit.ui.tabSpec('summaryResults', 'Summary + Results', [2 1], ... + labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ... {150, '1x'}, ... struct('resizeRows', 1, ... 'resizeOptions', struct('minTopHeight', 90, 'minBottomHeight', 90))), ... - labkit.ui.tabSpec('log', 'Log', [1 1], {'1x'})]; - ui = labkit.ui.createAppShell(struct( ... + labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})]; + ui = labkit.ui.app.createShell(struct( ... 'title', 'DIC Image Preprocess', ... 'position', [80 60 1400 860], ... 'leftWidth', 370, ... 'options', workbenchOpts)); fig = ui.fig; - imageRuntime = labkit.ui.createInteractionRuntime(ui.topAxes, ... + imageRuntime = labkit.ui.tool.createRuntime(ui.topAxes, ... struct('figure', fig, 'defaultScrollFcn', @onPreviewScrollZoom)); layFA = ui.filesAnalysisGrid; laySR = ui.summaryResultsGrid; layLog = ui.logGrid; - filePanel = labkit.ui.createPanelGrid(layFA, 'Images', 1, [4 2], ... + filePanel = labkit.ui.view.section(layFA, 'Images', 1, [4 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit'}}, ... 'columnWidth', {{'1x', '1x'}})); fileGrid = filePanel.grid; @@ -82,16 +82,16 @@ btnMoving.Layout.Row = 1; btnMoving.Layout.Column = 2; - txtReference = labkit.ui.createReadOnlyTextField(fileGrid, ... + txtReference = labkit.ui.view.form(fileGrid, 'readonly', ... 'Value', 'No reference image loaded'); txtReference.Layout.Row = 2; txtReference.Layout.Column = [1 2]; - txtMoving = labkit.ui.createReadOnlyTextField(fileGrid, ... + txtMoving = labkit.ui.view.form(fileGrid, 'readonly', ... 'Value', 'No moving image loaded'); txtMoving.Layout.Row = 3; txtMoving.Layout.Column = [1 2]; - [lblPreview, ddPreview] = labkit.ui.createLabeledDropdown(fileGrid, 'Preview:', ... + [lblPreview, 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', @(~,~) refreshPreview()); @@ -100,7 +100,7 @@ ddPreview.Layout.Row = 4; ddPreview.Layout.Column = 2; - actionPanel = labkit.ui.createPanelGrid(layFA, 'Registration + Crop', 2, [6 2], ... + actionPanel = labkit.ui.view.section(layFA, 'Registration + Crop', 2, [6 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}}, ... 'columnWidth', {{'1x', '1x'}})); actionGrid = actionPanel.grid; @@ -140,7 +140,7 @@ 'ButtonPushedFcn', @onResetToOriginals); btnResetCurrent.Layout.Row = 6; btnResetCurrent.Layout.Column = [1 2]; - maskPanel = labkit.ui.createPanelGrid(layFA, 'Mask ROI', 3, [7 2], ... + maskPanel = labkit.ui.view.section(layFA, 'Mask ROI', 3, [7 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}}, ... 'columnWidth', {{'1x', '1x'}})); maskGrid = maskPanel.grid; @@ -149,7 +149,7 @@ 'ButtonPushedFcn', @onStartMaskEdit); btnStartMask.Layout.Row = 1; btnStartMask.Layout.Column = [1 2]; - [lblBoundaryStyle, ddBoundaryStyle] = labkit.ui.createLabeledDropdown(maskGrid, 'Boundary:', ... + [lblBoundaryStyle, ddBoundaryStyle] = labkit.ui.view.form(maskGrid, 'dropdown', 'Boundary:', ... 'Items', {'Curve', 'Straight lines'}, ... 'Value', 'Curve', ... 'ValueChangedFcn', @onBoundaryStyleChanged); @@ -198,7 +198,7 @@ btnSaveMask.Layout.Row = 7; btnSaveMask.Layout.Column = [1 2]; - labkit.ui.createReadOnlyTextPanel(layFA, 'Workflow Notes', 4, { ... + labkit.ui.view.panel(layFA, 'text', 'Workflow Notes', 4, { ... '1. Load a reference image and a moving image.', ... '2. Align or crop the current working pair in any order; each apply step can be undone.', ... '3. False-color preview compares the current pair even before alignment.', ... @@ -209,10 +209,10 @@ txtSummary.Value = {'No images loaded.'}; txtDetails = uitextarea(laySR, 'Editable', 'off'); - txtDetails.Layout.Row = labkit.ui.layoutRow(laySR, 2); + labkit.ui.view.place(txtDetails, laySR, 2); txtDetails.Value = {'Alignment and crop details will appear here.'}; - logUi = labkit.ui.createLogPanel(layLog, 1, {'Ready.'}); + logUi = labkit.ui.view.panel(layLog, 'log', 1, {'Ready.'}); txtLog = logUi.textArea; if debugLog.enabled debugLog.attachTextLog(txtLog); @@ -469,7 +469,7 @@ function onStartMaskEdit(~, ~) S.maskPoints = []; S.maskHistory = S.maskHistory([]); S.maskBoundaryStyle = string(ddBoundaryStyle.Value); - S.maskEditor = labkit.ui.createAnchorCurveEditor(imageRuntime, size(S.currentReferenceImage), ... + S.maskEditor = labkit.ui.tool.anchorEditor(imageRuntime, size(S.currentReferenceImage), ... struct('closed', true, ... 'style', S.maskBoundaryStyle, ... 'installScrollWheel', false, ... @@ -915,12 +915,12 @@ function updateUndoButton() end function resetPreviewAxes() - labkit.ui.hardResetAxis(ui.topAxes, 'Reference', true); - labkit.ui.hardResetAxis(ui.bottomAxes, 'Current Preview', true); + labkit.ui.view.draw(ui.topAxes, 'reset', 'Reference', true); + labkit.ui.view.draw(ui.bottomAxes, 'reset', 'Current Preview', true); end function addLog(msg) - labkit.ui.appendLog(txtLog, msg); + labkit.ui.view.update(txtLog, 'appendLog', msg); debugLog.append(msg); end end @@ -1082,7 +1082,7 @@ function addLog(msg) end function hImage = showImage(ax, imageData, titleText) - hImage = labkit.ui.showImageAxes(ax, imageData, titleText); + hImage = labkit.ui.view.draw(ax, 'image', imageData, titleText); end function imageSize = axesImageSize(ax) diff --git a/apps/electrochem/labkit_CIC_app.m b/apps/electrochem/labkit_CIC_app.m index 5fd5364..cee3b35 100644 --- a/apps/electrochem/labkit_CIC_app.m +++ b/apps/electrochem/labkit_CIC_app.m @@ -23,7 +23,7 @@ % reports the highest safe file among all loaded files. % - By default, the evaluation point is 10 us after the end of each phase, % matching the convention commonly used in the literature the user shared. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_CIC_app', varargin, nargout, cicAppTestHandlers()); if requestHandled varargout = requestOutputs; @@ -44,7 +44,7 @@ S.current = []; %% ===================== Figure & Layout ===================== - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'Gamry CIC GUI (Voltage Transient)', ... 'position', [40 30 1680 980], ... 'leftWidth', 430, ... @@ -68,12 +68,12 @@ 'clearAll', 'Clear all', ... 'export', 'Export results CSV', ... 'loadedText', 'No files loaded'); - fileUi = labkit.ui.createFileSelectionPanel(layFA, fileLabels, fileCallbacks); + fileUi = labkit.ui.view.panel(layFA, 'files', fileLabels, fileCallbacks); lbFiles = fileUi.listbox; txtLoaded = fileUi.loadedText; %% ===================== Analysis settings ===================== - settingsUi = labkit.ui.createPanelGrid(layFA, 'Analysis Settings', 2, [9 2]); + settingsUi = labkit.ui.view.section(layFA, 'Analysis Settings', 2, [9 2]); gs = settingsUi.grid; uilabel(gs,'Text','Window preset:','HorizontalAlignment','right'); @@ -83,19 +83,19 @@ 'ValueChangedFcn',@(~,~) onPresetChanged()); ddPreset.Layout.Row = 1; ddPreset.Layout.Column = 2; - [lblCathLim, edCathLim] = labkit.ui.createLabeledSpinner(gs, 'Cathodic limit (V):', ... + [lblCathLim, edCathLim] = labkit.ui.view.form(gs, 'spinner', 'Cathodic limit (V):', ... 'Value', -0.6, 'Limits', [-10 10], 'Step', 0.01, ... 'ValueDisplayFormat','%.6g','ValueChangedFcn',@(~,~) analyzeCurrentFile()); lblCathLim.Layout.Row = 2; lblCathLim.Layout.Column = 1; edCathLim.Layout.Row = 2; edCathLim.Layout.Column = 2; - [lblAnodLim, edAnodLim] = labkit.ui.createLabeledSpinner(gs, 'Anodic limit (V):', ... + [lblAnodLim, edAnodLim] = labkit.ui.view.form(gs, 'spinner', 'Anodic limit (V):', ... 'Value', 0.8, 'Limits', [-10 10], 'Step', 0.01, ... 'ValueDisplayFormat','%.6g','ValueChangedFcn',@(~,~) analyzeCurrentFile()); lblAnodLim.Layout.Row = 3; lblAnodLim.Layout.Column = 1; edAnodLim.Layout.Row = 3; edAnodLim.Layout.Column = 2; - [lblDelayUs, edDelayUs] = labkit.ui.createLabeledSpinner(gs, 'Sample delay after pulse end:', ... + [lblDelayUs, edDelayUs] = labkit.ui.view.form(gs, 'spinner', 'Sample delay after pulse end:', ... 'Value', 10, 'Limits', [0 inf], 'Step', 1, ... 'ValueDisplayFormat','%.6g','ValueChangedFcn',@(~,~) analyzeCurrentFile()); lblDelayUs.Layout.Row = 4; lblDelayUs.Layout.Column = 1; @@ -132,23 +132,23 @@ cbUseMeasuredCurrent.Layout.Row = 9; cbUseMeasuredCurrent.Layout.Column = [1 2]; %% ===================== Quick info ===================== - infoUi = labkit.ui.createPanelGrid(laySR, 'Current File Summary', 1, [11 2]); + infoUi = labkit.ui.view.section(laySR, 'Current File Summary', 1, [11 2]); gi = infoUi.grid; - S.txtControlMode = labkit.ui.createReadOnlyInfoRow(gi,1,'Control mode:'); - S.txtDetect = labkit.ui.createReadOnlyInfoRow(gi,2,'Detection:'); - S.txtDelay = labkit.ui.createReadOnlyInfoRow(gi,3,'Delay used:'); - S.txtArea = labkit.ui.createReadOnlyInfoRow(gi,4,'Area:'); - S.txtEmc = labkit.ui.createReadOnlyInfoRow(gi,5,'Emc:'); - S.txtEma = labkit.ui.createReadOnlyInfoRow(gi,6,'Ema:'); - S.txtQc = labkit.ui.createReadOnlyInfoRow(gi,7,'Cathodic Q/CIC:'); - S.txtQa = labkit.ui.createReadOnlyInfoRow(gi,8,'Anodic Q/CIC:'); - S.txtQt = labkit.ui.createReadOnlyInfoRow(gi,9,'Total Q/CIC:'); - S.txtSafe = labkit.ui.createReadOnlyInfoRow(gi,10,'Safety:'); - S.txtBest = labkit.ui.createReadOnlyInfoRow(gi,11,'Best safe among loaded:'); + S.txtControlMode = labkit.ui.view.form(gi, 'info', 1, 'Control mode:'); + S.txtDetect = labkit.ui.view.form(gi, 'info', 2, 'Detection:'); + S.txtDelay = labkit.ui.view.form(gi, 'info', 3, 'Delay used:'); + S.txtArea = labkit.ui.view.form(gi, 'info', 4, 'Area:'); + S.txtEmc = labkit.ui.view.form(gi, 'info', 5, 'Emc:'); + S.txtEma = labkit.ui.view.form(gi, 'info', 6, 'Ema:'); + S.txtQc = labkit.ui.view.form(gi, 'info', 7, 'Cathodic Q/CIC:'); + S.txtQa = labkit.ui.view.form(gi, 'info', 8, 'Anodic Q/CIC:'); + S.txtQt = labkit.ui.view.form(gi, 'info', 9, 'Total Q/CIC:'); + S.txtSafe = labkit.ui.view.form(gi, 'info', 10, 'Safety:'); + S.txtBest = labkit.ui.view.form(gi, 'info', 11, 'Best safe among loaded:'); %% ===================== Actions ===================== - actionUi = labkit.ui.createPanelGrid(layFA, 'Plot / Debug', 3, [2 3]); + actionUi = labkit.ui.view.section(layFA, 'Plot / Debug', 3, [2 3]); ga = actionUi.grid; btnRefresh = uibutton(ga,'Text','Refresh plots','ButtonPushedFcn',@(~,~) refreshPlots()); @@ -166,20 +166,21 @@ cbShowShading.Layout.Row = 2; cbShowShading.Layout.Column = 3; %% ===================== Results table ===================== - tableUi = labkit.ui.createResultTablePanel(laySR, 'Batch Results', 2, ... + tableUi = labkit.ui.view.panel(laySR, 'table', 'Batch Results', 2, ... {'File','Amp(A)','Emc(V)','Ema(V)','Qc(mC/cm^2)','Qa(mC/cm^2)','Qtot(mC/cm^2)','Safe'}, ... cell(0,8)); tbl = tableUi.table; %% ===================== Log ===================== - logUi = labkit.ui.createLogPanel(layLog, 1); + logUi = labkit.ui.view.panel(layLog, 'log', 1); txtLog = logUi.textArea; %% ===================== Right: plots ===================== 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.createTopBottomPlotControls( ... + plotControls = labkit.ui.view.panel( ... ui.topControlsPanel, ... + 'topBottomPlotControls', ... ui.bottomControlsPanel, ... {'Time (s)', 'Sample #'}, ... {'VT: Vf vs time', 'IT: Im vs time'}, ... @@ -371,14 +372,14 @@ function clearAllFiles() function refreshFileList() if isempty(S.items) - labkit.ui.refreshListboxSelection(lbFiles, {}); + labkit.ui.view.update(lbFiles, 'listSelection', {}); txtLoaded.Value = fileLabels.loadedText; S.current = []; return; end names = {S.items.name}; - [~, idx] = labkit.ui.refreshListboxSelection(lbFiles, names, S.current); + [~, idx] = labkit.ui.view.update(lbFiles, 'listSelection', names, S.current); S.current = idx(1); txtLoaded.Value = sprintf('%d file(s) loaded', numel(S.items)); end @@ -516,8 +517,8 @@ function refreshCICUnitDisplays() end function refreshPlots() - labkit.ui.clearAxisObjects(axTop); - labkit.ui.clearAxisObjects(axBottom); + labkit.ui.view.draw(axTop, 'clear'); + labkit.ui.view.draw(axBottom, 'clear'); if isempty(S.items) || isempty(S.current) || S.current < 1 || S.current > numel(S.items) title(axTop,'Top Plot'); title(axBottom,'Bottom Plot'); @@ -622,7 +623,7 @@ function plotOneAxis(ax, A, xChoice, yChoice, showGrid) end function swapPlots() - labkit.ui.swapTopBottomPlotSelections(ddTopX, ddTopY, ddBotX, ddBotY); + labkit.ui.view.update(plotControls, 'swapPlotSelections'); refreshPlots(); end @@ -632,13 +633,13 @@ function resetAxes() end function restoreDefaultPlotSelections() - labkit.ui.setTopBottomPlotSelections(ddTopX, ddTopY, ddBotX, ddBotY, ... + labkit.ui.view.update(plotControls, 'setPlotSelections', ... topPlotDefaults, bottomPlotDefaults); end function resetAxesToDefaultState() - labkit.ui.hardResetAxis(axTop, 'Top Plot', true); - labkit.ui.hardResetAxis(axBottom, 'Bottom Plot', true); + labkit.ui.view.draw(axTop, 'reset', 'Top Plot', true); + labkit.ui.view.draw(axBottom, 'reset', 'Bottom Plot', true); end function exportResultsCSV() @@ -662,7 +663,7 @@ function exportResultsCSV() %% ===================== Logging ===================== function addLog(msg) - labkit.ui.appendLog(txtLog, msg); + labkit.ui.view.update(txtLog, 'appendLog', msg); debugLog.append(msg); end diff --git a/apps/electrochem/labkit_CSC_app.m b/apps/electrochem/labkit_CSC_app.m index e18f9fb..5d41211 100644 --- a/apps/electrochem/labkit_CSC_app.m +++ b/apps/electrochem/labkit_CSC_app.m @@ -22,9 +22,9 @@ % [testLoadFile, isLoadDiagnostics] = parseCSCLoadDiagnosticsRequest(varargin); if isLoadDiagnostics - debugLog = labkit.ui.createDebugContext('labkit_CSC_app', struct('enabled', false)); + debugLog = labkit.ui.diag.createContext('labkit_CSC_app', struct('enabled', false)); else - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_CSC_app', varargin, nargout, cscAppTestHandlers()); if requestHandled varargout = requestOutputs; @@ -56,7 +56,7 @@ S.currentCurve = 1; %% ===================== Figure & Layout ===================== - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'Gamry DTA GUI (literature CSC)', ... 'position', [50 30 1580 950], ... 'leftWidth', 390, ... @@ -80,20 +80,20 @@ 'clearAll', 'Clear all', ... 'export', 'Reload selected', ... 'loadedText', 'No files loaded'); - fileUi = labkit.ui.createFileSelectionPanel(layFA, fileLabels, fileCallbacks); + fileUi = labkit.ui.view.panel(layFA, 'files', fileLabels, fileCallbacks); lbFiles = fileUi.listbox; txtLoaded = fileUi.loadedText; % -------- Curve -------- - curveUi = labkit.ui.createPanelGrid(layFA, 'Curve', 2, [4 2]); + curveUi = labkit.ui.view.section(layFA, 'Curve', 2, [4 2]); gf = curveUi.grid; uilabel(gf,'Text','File:','HorizontalAlignment','right'); - txtFile = labkit.ui.createReadOnlyTextField(gf); + txtFile = labkit.ui.view.form(gf, 'readonly'); txtFile.Layout.Row = 1; txtFile.Layout.Column = 2; uilabel(gf,'Text','Scan rate:','HorizontalAlignment','right'); - txtScan = labkit.ui.createReadOnlyTextField(gf); + txtScan = labkit.ui.view.form(gf, 'readonly'); txtScan.Layout.Row = 2; txtScan.Layout.Column = 2; uilabel(gf,'Text','Curve:','HorizontalAlignment','right'); @@ -105,7 +105,7 @@ % -------- Actions -------- actionOpts = struct('columnWidth', {{'1x', '1x'}}); - actionUi = labkit.ui.createPanelGrid(layFA, 'Actions', 3, [2 2], actionOpts); + actionUi = labkit.ui.view.section(layFA, 'Actions', 3, [2 2], actionOpts); ga = actionUi.grid; btnSwap = uibutton(ga,'Text','Swap Top/Bottom','ButtonPushedFcn',@(~,~) onSwapPlots()); @@ -118,7 +118,7 @@ btnClear.Layout.Row = 2; btnClear.Layout.Column = 2; % -------- Comparison / CSC -------- - compUi = labkit.ui.createPanelGrid(laySR, 'CSC / Comparison', 1, [8 2]); + compUi = labkit.ui.view.section(laySR, 'CSC / Comparison', 1, [8 2]); gc = compUi.grid; uilabel(gc,'Text','Mode:','HorizontalAlignment','right'); @@ -134,23 +134,23 @@ edArea.Layout.Row = 2; edArea.Layout.Column = 2; uilabel(gc,'Text','CT charge / CSC:','HorizontalAlignment','right'); - txtQct = labkit.ui.createReadOnlyTextField(gc); + txtQct = labkit.ui.view.form(gc, 'readonly'); txtQct.Layout.Row = 3; txtQct.Layout.Column = 2; uilabel(gc,'Text','CV charge / CSC:','HorizontalAlignment','right'); - txtQcv = labkit.ui.createReadOnlyTextField(gc); + txtQcv = labkit.ui.view.form(gc, 'readonly'); txtQcv.Layout.Row = 4; txtQcv.Layout.Column = 2; uilabel(gc,'Text','Difference:','HorizontalAlignment','right'); - txtDiff = labkit.ui.createReadOnlyTextField(gc); + txtDiff = labkit.ui.view.form(gc, 'readonly'); txtDiff.Layout.Row = 5; txtDiff.Layout.Column = 2; uilabel(gc,'Text','Relative diff:','HorizontalAlignment','right'); - txtRel = labkit.ui.createReadOnlyTextField(gc); + txtRel = labkit.ui.view.form(gc, 'readonly'); txtRel.Layout.Row = 6; txtRel.Layout.Column = 2; uilabel(gc,'Text','max|dt-|dV|/v|:','HorizontalAlignment','right'); - txtDtErr = labkit.ui.createReadOnlyTextField(gc); + txtDtErr = labkit.ui.view.form(gc, 'readonly'); txtDtErr.Layout.Row = 7; txtDtErr.Layout.Column = 2; lblStatus = uilabel(gc,'Text','Ready'); @@ -158,15 +158,16 @@ lblStatus.FontWeight = 'bold'; % -------- Log -------- - logUi = labkit.ui.createLogPanel(layLog, 1, {'GUI started.'}); + logUi = labkit.ui.view.panel(layLog, 'log', 1, {'GUI started.'}); txtLog = logUi.textArea; txtLog.Value = {'GUI started.'}; % -------- Top/bottom controls -------- topPlotDefaults = struct('x', '(none)', 'y', '(none)', 'grid', true); bottomPlotDefaults = struct('x', '(none)', 'y', '(none)', 'grid', true); - plotControls = labkit.ui.createTopBottomPlotControls( ... + plotControls = labkit.ui.view.panel( ... ui.topControlsPanel, ... + 'topBottomPlotControls', ... ui.bottomControlsPanel, ... {'(none)'}, ... {'(none)'}, ... @@ -318,11 +319,11 @@ function reloadSelectedFile() function refreshFileList() if isempty(S.items) - labkit.ui.refreshListboxSelection(lbFiles, {}); + labkit.ui.view.update(lbFiles, 'listSelection', {}); txtLoaded.Value = 'No files loaded'; return; end - [~, idx] = labkit.ui.refreshListboxSelection(lbFiles, {S.items.name}, S.current); + [~, idx] = labkit.ui.view.update(lbFiles, 'listSelection', {S.items.name}, S.current); S.current = idx(1); txtLoaded.Value = sprintf('%d file(s) loaded', numel(S.items)); end @@ -478,7 +479,7 @@ function plotTop() opts = struct('holdPlot', cbTopHold.Value, 'showGrid', cbTopGrid.Value, 'lineWidth', 1.2); [x, y, xName, yName] = labkit.dta.getCurveXY(c, ddTopX.Value, ddTopY.Value); labels = struct('title', c.name, 'x', xName, 'y', yName); - info = labkit.ui.plotXY(axTop, x, y, labels, opts); + info = labkit.ui.view.draw(axTop, 'xy', x, y, labels, opts); if ~info.ok addLog('Top plot skipped: invalid X/Y.'); return; @@ -492,7 +493,7 @@ function plotBottom() opts = struct('holdPlot', cbBotHold.Value, 'showGrid', cbBotGrid.Value, 'lineWidth', 1.2); [x, y, xName, yName] = labkit.dta.getCurveXY(c, ddBotX.Value, ddBotY.Value); labels = struct('title', c.name, 'x', xName, 'y', yName); - info = labkit.ui.plotXY(axBottom, x, y, labels, opts); + info = labkit.ui.view.draw(axBottom, 'xy', x, y, labels, opts); if ~info.ok addLog('Bottom plot skipped: invalid X/Y.'); return; @@ -574,7 +575,7 @@ function refreshCompare() end function addLog(msg) - labkit.ui.appendLog(txtLog, msg); + labkit.ui.view.update(txtLog, 'appendLog', msg); debugLog.append(msg); end diff --git a/apps/electrochem/labkit_ChronoOverlay_app.m b/apps/electrochem/labkit_ChronoOverlay_app.m index d2e9e02..9636be5 100644 --- a/apps/electrochem/labkit_ChronoOverlay_app.m +++ b/apps/electrochem/labkit_ChronoOverlay_app.m @@ -2,7 +2,7 @@ %LABKIT_CHRONOOVERLAY_APP Chrono voltage/current overlay and export app. % Single-file app that composes +labkit GUI/DTA APIs and owns overlay workflow choices. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_ChronoOverlay_app', varargin, nargout, chronoOverlayAppTestHandlers()); if requestHandled varargout = requestOutputs; @@ -26,7 +26,7 @@ workbenchOpts.rightGridSize = [2 1]; workbenchOpts.rightRowHeight = {'1x', '1x'}; workbenchOpts.rightRowSpacing = 10; - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'Gamry Multi-DTA Plot Export GUI', ... 'position', [80 60 1480 900], ... 'leftWidth', 340, ... @@ -52,20 +52,20 @@ 'clearAll', 'Clear all', ... 'export', 'Export curves CSV', ... 'loadedText', 'No files loaded'); - fileUi = labkit.ui.createFileSelectionPanel(layFA, fileLabels, fileCallbacks, ... + fileUi = labkit.ui.view.panel(layFA, 'files', fileLabels, fileCallbacks, ... struct('showRemoveSelected', true, 'multiselect', 'on')); lbFiles = fileUi.listbox; txtLoaded = fileUi.loadedText; - plotOptionsUi = labkit.ui.createPlotOptionsPanel(layFA, 4, 2); + plotOptionsUi = labkit.ui.view.panel(layFA, 'plotOptions', 4, 2); gp = plotOptionsUi.grid; - [~, ddXAxis] = labkit.ui.createLabeledDropdown(gp, 'X axis:', ... + [~, ddXAxis] = labkit.ui.view.form(gp, 'dropdown', 'X axis:', ... 'Items', {'Time (s)', 'Time (ms)', 'Sample #'}, ... 'Value', 'Time (s)', ... 'ValueChangedFcn', @(~,~) refreshPlots()); - [~, edLineWidth] = labkit.ui.createLabeledSpinner(gp, 'Line width:', ... + [~, edLineWidth] = labkit.ui.view.form(gp, 'spinner', 'Line width:', ... 'Value', 1.3, ... 'Limits', [0.1 10], ... 'Step', 0.1, ... @@ -85,7 +85,7 @@ cbGrid.Layout.Row = 4; cbGrid.Layout.Column = [1 2]; - infoUi = labkit.ui.createReadOnlyTextPanel(laySR, 'Usage', 1, { ... + infoUi = labkit.ui.view.panel(laySR, 'text', 'Usage', 1, { ... 'Usage:', ... '1. Open multiple .DTA files.', ... '2. Curves are aligned to the center of the blank time between cathodic and anodic phases.', ... @@ -95,7 +95,7 @@ }); txtInfo = infoUi.textArea; - logUi = labkit.ui.createLogPanel(layLog, 1); + logUi = labkit.ui.view.panel(layLog, 'log', 1); txtLog = logUi.textArea; if debugLog.enabled debugLog.attachTextLog(txtLog); @@ -103,8 +103,8 @@ debugLog.instrumentFigure(fig); end - axV = labkit.ui.createAxes(right, 1, 'Voltage', 'Time (s)', 'Vf (V)'); - axI = labkit.ui.createAxes(right, 2, 'Current', 'Time (s)', 'Im (A)'); + axV = labkit.ui.view.axes(right, 1, 'Voltage', 'Time (s)', 'Vf (V)'); + axI = labkit.ui.view.axes(right, 2, 'Current', 'Time (s)', 'Im (A)'); if nargout >= 1 varargout{1} = fig; end @@ -213,11 +213,11 @@ function onClearAll(~, ~) function refreshFileList() if isempty(S.items) - labkit.ui.refreshListboxItems(lbFiles, {}); + labkit.ui.view.update(lbFiles, 'listItems', {}); txtLoaded.Value = 'No files loaded'; return; end - labkit.ui.refreshListboxItems(lbFiles, {S.items.name}); + labkit.ui.view.update(lbFiles, 'listItems', {S.items.name}); txtLoaded.Value = sprintf('%d file(s) loaded', numel(S.items)); end @@ -269,7 +269,7 @@ function onExportCSV(~, ~) end function addLog(msg) - labkit.ui.appendLog(txtLog, msg); + labkit.ui.view.update(txtLog, 'appendLog', msg); debugLog.append(msg); end end diff --git a/apps/electrochem/labkit_EIS_app.m b/apps/electrochem/labkit_EIS_app.m index 0f29492..68d7c4e 100644 --- a/apps/electrochem/labkit_EIS_app.m +++ b/apps/electrochem/labkit_EIS_app.m @@ -2,7 +2,7 @@ %LABKIT_EIS_APP EIS overlay/export app. % Single-file app that composes +labkit GUI/DTA APIs and owns EIS workflow choices. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_EIS_app', varargin, nargout, eisAppTestHandlers()); if requestHandled varargout = requestOutputs; @@ -39,7 +39,7 @@ workbenchOpts.rightGridSize = [1 1]; workbenchOpts.rightRowHeight = {'1x'}; workbenchOpts.rightRowSpacing = 8; - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'Gamry EIS Multi-DTA Plot GUI', ... 'position', [80 60 1500 900], ... 'leftWidth', 360, ... @@ -65,31 +65,31 @@ 'clearAll', 'Clear all', ... 'export', 'Export current plot CSV', ... 'loadedText', 'No files loaded'); - fileUi = labkit.ui.createFileSelectionPanel(layFA, fileLabels, fileCallbacks, ... + fileUi = labkit.ui.view.panel(layFA, 'files', fileLabels, fileCallbacks, ... struct('showRemoveSelected', true, 'multiselect', 'on')); lbFiles = fileUi.listbox; txtLoaded = fileUi.loadedText; - plotOptionsUi = labkit.ui.createPlotOptionsPanel(layFA, 8, 2); + plotOptionsUi = labkit.ui.view.panel(layFA, 'plotOptions', 8, 2); gp = plotOptionsUi.grid; - [~, ddX] = labkit.ui.createLabeledDropdown(gp, 'X axis:', ... + [~, ddX] = labkit.ui.view.form(gp, 'dropdown', 'X axis:', ... 'Items', axisItems, ... 'Value', 'Zreal (ohm)', ... 'ValueChangedFcn', @(~,~) refreshPlot()); - [~, ddY] = labkit.ui.createLabeledDropdown(gp, 'Y axis:', ... + [~, ddY] = labkit.ui.view.form(gp, 'dropdown', 'Y axis:', ... 'Items', axisItems, ... 'Value', '-Zimag (ohm)', ... 'ValueChangedFcn', @(~,~) refreshPlot()); - [~, edLineWidth] = labkit.ui.createLabeledSpinner(gp, 'Line width:', ... + [~, edLineWidth] = labkit.ui.view.form(gp, 'spinner', 'Line width:', ... 'Value', 1.4, ... 'Limits', [0.1 10], ... 'Step', 0.1, ... 'ValueChangedFcn', @(~,~) refreshPlot()); - [~, edMarkerSize] = labkit.ui.createLabeledSpinner(gp, 'Marker size:', ... + [~, edMarkerSize] = labkit.ui.view.form(gp, 'spinner', 'Marker size:', ... 'Value', 6, ... 'Limits', [1 20], ... 'Step', 1, ... @@ -133,7 +133,7 @@ 'Value', true, ... 'ValueChangedFcn', @(~,~) refreshPlot()); - infoUi = labkit.ui.createReadOnlyTextPanel(laySR, 'Usage', 1, { ... + infoUi = labkit.ui.view.panel(laySR, 'text', 'Usage', 1, { ... 'Usage:', ... '1. Open one or more EIS .DTA files containing ZCURVE.', ... '2. Choose any X and Y axis combination.', ... @@ -142,13 +142,13 @@ '5. CSV export writes one shared row index with X/Y pairs per file.'}); txtInfo = infoUi.textArea; - logUi = labkit.ui.createLogPanel(layLog, 1); + logUi = labkit.ui.view.panel(layLog, 'log', 1); txtLog = logUi.textArea; - ax = labkit.ui.createAxes(right, 1, 'EIS Overlay', 'Zreal (ohm)', '-Zimag (ohm)'); + ax = labkit.ui.view.axes(right, 1, 'EIS Overlay', 'Zreal (ohm)', '-Zimag (ohm)'); txtSummary = uitextarea(laySR, 'Editable', 'off'); - txtSummary.Layout.Row = labkit.ui.layoutRow(laySR, 2); + labkit.ui.view.place(txtSummary, laySR, 2); txtSummary.Value = {'No files loaded.'}; if debugLog.enabled debugLog.attachTextLog(txtLog); @@ -250,11 +250,11 @@ function onClearAll(~, ~) function refreshFileList() if isempty(S.items) - labkit.ui.refreshListboxItems(lbFiles, {}); + labkit.ui.view.update(lbFiles, 'listItems', {}); txtLoaded.Value = 'No files loaded'; return; end - labkit.ui.refreshListboxItems(lbFiles, {S.items.name}); + labkit.ui.view.update(lbFiles, 'listItems', {S.items.name}); txtLoaded.Value = sprintf('%d file(s) loaded', numel(S.items)); end @@ -312,7 +312,7 @@ function onExportCSV(~, ~) end function addLog(msg) - labkit.ui.appendLog(txtLog, msg); + labkit.ui.view.update(txtLog, 'appendLog', msg); debugLog.append(msg); end end diff --git a/apps/electrochem/labkit_VTResistance_app.m b/apps/electrochem/labkit_VTResistance_app.m index 72907f4..364aae4 100644 --- a/apps/electrochem/labkit_VTResistance_app.m +++ b/apps/electrochem/labkit_VTResistance_app.m @@ -10,7 +10,7 @@ % - Estimate steady phase voltage by median(Vf) in the same selected window. % - Compute baseline-corrected resistance as abs((Vss - Vbaseline) / Iss). - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_VTResistance_app', varargin, nargout, vtAppTestHandlers()); if requestHandled varargout = requestOutputs; @@ -30,7 +30,7 @@ S.items = S.session.items; S.current = []; - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'Gamry VT Steady Resistance GUI', ... 'position', [40 30 1680 980], ... 'leftWidth', 430, ... @@ -53,11 +53,11 @@ 'clearAll', 'Clear all', ... 'export', 'Export results CSV', ... 'loadedText', 'No files loaded'); - fileUi = labkit.ui.createFileSelectionPanel(layFA, fileLabels, fileCallbacks); + fileUi = labkit.ui.view.panel(layFA, 'files', fileLabels, fileCallbacks); lbFiles = fileUi.listbox; txtLoaded = fileUi.loadedText; - settingsUi = labkit.ui.createPanelGrid(layFA, 'Analysis Settings', 2, [3 2]); + settingsUi = labkit.ui.view.section(layFA, 'Analysis Settings', 2, [3 2]); gs = settingsUi.grid; uilabel(gs,'Text','Pulse detection:','HorizontalAlignment','right'); @@ -84,7 +84,7 @@ ddVoltageMode.Layout.Row = 3; ddVoltageMode.Layout.Column = 2; - actionUi = labkit.ui.createPanelGrid(layFA, 'Plot / Debug', 3, [2 3]); + actionUi = labkit.ui.view.section(layFA, 'Plot / Debug', 3, [2 3]); ga = actionUi.grid; btnReanalyze = uibutton(ga,'Text','Re-analyze file','ButtonPushedFcn',@(~,~) analyzeCurrentFile()); @@ -101,35 +101,36 @@ cbShowShading = uicheckbox(ga,'Text','Shade windows','Value',true,'ValueChangedFcn',@(~,~) refreshPlots()); cbShowShading.Layout.Row = 2; cbShowShading.Layout.Column = 3; - infoUi = labkit.ui.createPanelGrid(laySR, 'Current File Summary', 1, [13 2]); + infoUi = labkit.ui.view.section(laySR, 'Current File Summary', 1, [13 2]); gi = infoUi.grid; - S.txtControlMode = labkit.ui.createReadOnlyInfoRow(gi,1,'Control mode:'); - S.txtDetect = labkit.ui.createReadOnlyInfoRow(gi,2,'Detection:'); - S.txtWindow = labkit.ui.createReadOnlyInfoRow(gi,3,'Window:'); - S.txtCathIV = labkit.ui.createReadOnlyInfoRow(gi,4,'Cathodic I / Vss:'); - S.txtAnodIV = labkit.ui.createReadOnlyInfoRow(gi,5,'Anodic I / Vss:'); - S.txtCathBase = labkit.ui.createReadOnlyInfoRow(gi,6,'Cathodic baseline:'); - S.txtAnodBase = labkit.ui.createReadOnlyInfoRow(gi,7,'Anodic baseline:'); - S.txtCathBaseWin = labkit.ui.createReadOnlyInfoRow(gi,8,'Cath baseline window:'); - S.txtAnodBaseWin = labkit.ui.createReadOnlyInfoRow(gi,9,'Anod baseline window:'); - S.txtCathR = labkit.ui.createReadOnlyInfoRow(gi,10,'Cathodic R:'); - S.txtAnodR = labkit.ui.createReadOnlyInfoRow(gi,11,'Anodic R:'); - S.txtAvgR = labkit.ui.createReadOnlyInfoRow(gi,12,'Average R:'); - S.txtStatus = labkit.ui.createReadOnlyInfoRow(gi,13,'Status:'); - - tableUi = labkit.ui.createResultTablePanel(laySR, 'Batch Results', 2, ... + 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:'); + + 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'}, ... cell(0,9)); tbl = tableUi.table; - logUi = labkit.ui.createLogPanel(layLog, 1); + logUi = labkit.ui.view.panel(layLog, 'log', 1); txtLog = logUi.textArea; 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.createTopBottomPlotControls( ... + plotControls = labkit.ui.view.panel( ... ui.topControlsPanel, ... + 'topBottomPlotControls', ... ui.bottomControlsPanel, ... {'Time (s)', 'Sample #'}, ... {'VT: Vf vs time', 'IT: Im vs time'}, ... @@ -282,14 +283,14 @@ function clearAllFiles() function refreshFileList() if isempty(S.items) - labkit.ui.refreshListboxSelection(lbFiles, {}); + labkit.ui.view.update(lbFiles, 'listSelection', {}); txtLoaded.Value = fileLabels.loadedText; S.current = []; return; end names = {S.items.name}; - [~, idx] = labkit.ui.refreshListboxSelection(lbFiles, names, S.current); + [~, idx] = labkit.ui.view.update(lbFiles, 'listSelection', names, S.current); S.current = idx(1); txtLoaded.Value = sprintf('%d file(s) loaded', numel(S.items)); end @@ -363,8 +364,8 @@ function refreshResultsSummary() end function refreshPlots() - labkit.ui.clearAxisObjects(axTop); - labkit.ui.clearAxisObjects(axBottom); + labkit.ui.view.draw(axTop, 'clear'); + labkit.ui.view.draw(axBottom, 'clear'); if isempty(S.items) || isempty(S.current) || S.current < 1 || S.current > numel(S.items) title(axTop,'Top Plot'); title(axBottom,'Bottom Plot'); @@ -464,7 +465,7 @@ function plotOneAxis(ax, A, xChoice, yChoice, showGrid) end function swapPlots() - labkit.ui.swapTopBottomPlotSelections(ddTopX, ddTopY, ddBotX, ddBotY); + labkit.ui.view.update(plotControls, 'swapPlotSelections'); refreshPlots(); end @@ -474,13 +475,13 @@ function resetAxes() end function restoreDefaultPlotSelections() - labkit.ui.setTopBottomPlotSelections(ddTopX, ddTopY, ddBotX, ddBotY, ... + labkit.ui.view.update(plotControls, 'setPlotSelections', ... topPlotDefaults, bottomPlotDefaults); end function resetAxesToDefaultState() - labkit.ui.hardResetAxis(axTop, 'Top Plot'); - labkit.ui.hardResetAxis(axBottom, 'Bottom Plot'); + labkit.ui.view.draw(axTop, 'reset', 'Top Plot'); + labkit.ui.view.draw(axBottom, 'reset', 'Bottom Plot'); end function exportResultsCSV() @@ -502,7 +503,7 @@ function exportResultsCSV() end function addLog(msg) - labkit.ui.appendLog(txtLog, msg); + labkit.ui.view.update(txtLog, 'appendLog', msg); debugLog.append(msg); end diff --git a/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m b/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m index 8066c6e..1226f7e 100644 --- a/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m +++ b/apps/image_measurement/curvature/labkit_CurvatureMeasurement_app.m @@ -1,7 +1,7 @@ function varargout = labkit_CurvatureMeasurement_app(varargin) %LABKIT_CURVATUREMEASUREMENT_APP Measure curve radius and curvature from images. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_CurvatureMeasurement_app', varargin, nargout, curvatureAppTestHandlers()); if requestHandled varargout = requestOutputs; @@ -32,16 +32,16 @@ 'rightGridSize', [1 1], ... 'rightRowHeight', {{'1x'}}); workbenchOpts.tabs = [ ... - labkit.ui.tabSpec('filesAnalysis', 'Files + Analysis', [5 1], ... + labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [5 1], ... {140, 105, 355, 225, 160}, ... struct('resizeRows', [1 2 3 4], ... 'resizeOptions', struct('minTopHeight', 140, 'minBottomHeight', 90))), ... - labkit.ui.tabSpec('summaryResults', 'Summary + Results', [2 1], ... + labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ... {170, '1x'}, ... struct('resizeRows', 1)), ... - labkit.ui.tabSpec('log', 'Log', [1 1], {'1x'})]; + labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})]; - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'Image Curvature Measurement', ... 'position', [90 70 1420 860], ... 'leftWidth', 390, ... @@ -52,12 +52,12 @@ layLog = ui.logGrid; ui.topAxes = uiaxes(ui.rightGrid); ui.topAxes.Layout.Row = 1; - imageRuntime = labkit.ui.createInteractionRuntime(ui.topAxes, ... + imageRuntime = labkit.ui.tool.createRuntime(ui.topAxes, ... struct('figure', fig, ... 'defaultScrollFcn', @onPreviewScroll, ... 'onTrace', debugLog.trace)); - imagePanel = labkit.ui.createPanelGrid(layFA, 'Image', 1, [3 2], ... + imagePanel = labkit.ui.view.section(layFA, 'Image', 1, [3 2], ... struct('rowHeight', {{'fit', 'fit', 'fit'}}, ... 'columnWidth', {{145, '1x'}})); imageGrid = imagePanel.grid; @@ -67,17 +67,17 @@ btnOpenImage.Layout.Row = 1; btnOpenImage.Layout.Column = [1 2]; - txtImage = labkit.ui.createReadOnlyTextField(imageGrid, ... + txtImage = labkit.ui.view.form(imageGrid, 'readonly', ... 'Value', 'No image loaded'); txtImage.Layout.Row = 2; txtImage.Layout.Column = [1 2]; - txtPointCount = labkit.ui.createReadOnlyTextField(imageGrid, ... + txtPointCount = labkit.ui.view.form(imageGrid, 'readonly', ... 'Value', 'Points: 0'); txtPointCount.Layout.Row = 3; txtPointCount.Layout.Column = [1 2]; - editPanel = labkit.ui.createPanelGrid(layFA, 'Curve Editing', 2, [2 2], ... + editPanel = labkit.ui.view.section(layFA, 'Curve Editing', 2, [2 2], ... struct('rowHeight', {{'fit', 'fit'}}, ... 'columnWidth', {{145, '1x'}})); editGrid = editPanel.grid; @@ -98,7 +98,7 @@ btnClearCurve.Layout.Row = 2; btnClearCurve.Layout.Column = 2; - scaleTool = labkit.ui.createScaleBarTool(layFA, 3, imageRuntime, ... + scaleTool = labkit.ui.tool.scaleBar(layFA, 3, imageRuntime, ... struct('onBeforeReferenceEdit', @onBeforeReferenceEdit, ... 'onReferenceEditChanged', @onReferenceEditChanged, ... 'onCalibrationChanged', @onCalibrationSettingsChanged, ... @@ -107,7 +107,7 @@ 'onError', @onScaleToolError, ... 'onTrace', debugLog.trace)); - fitPanel = labkit.ui.createPanelGrid(layFA, 'Fit + Export', 4, [7 2], ... + fitPanel = labkit.ui.view.section(layFA, 'Fit + Export', 4, [7 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}}, ... 'columnWidth', {{145, '1x'}})); fitGrid = fitPanel.grid; @@ -116,7 +116,7 @@ chkDensify.Layout.Row = 1; chkDensify.Layout.Column = [1 2]; - [lblDenseN, edtDenseN] = labkit.ui.createLabeledSpinner(fitGrid, ... + [lblDenseN, edtDenseN] = labkit.ui.view.form(fitGrid, 'spinner', ... 'Dense point count:', 'Value', 300, 'Limits', [3 Inf], 'Step', 25); lblDenseN.Layout.Row = 2; lblDenseN.Layout.Column = 1; @@ -148,7 +148,7 @@ btnExportOverlay.Layout.Row = 7; btnExportOverlay.Layout.Column = [1 2]; - labkit.ui.createReadOnlyTextPanel(layFA, 'Workflow Notes', 5, { ... + labkit.ui.view.panel(layFA, 'text', 'Workflow Notes', 5, { ... '1. Open an image and start curve editing.', ... '2. Double-click blank image space to add/insert points; drag points to move; double-click a point to delete it.', ... '3. Calibrate with measured or typed reference pixels, a real reference length, and a unit.', ... @@ -160,10 +160,10 @@ resultTable.Layout.Row = 1; txtDetails = uitextarea(laySR, 'Editable', 'off'); - txtDetails.Layout.Row = labkit.ui.layoutRow(laySR, 2); + labkit.ui.view.place(txtDetails, laySR, 2); txtDetails.Value = {'No curvature result yet.'}; - logUi = labkit.ui.createLogPanel(layLog, 1, {'Ready.'}); + logUi = labkit.ui.view.panel(layLog, 'log', 1, {'Ready.'}); txtLog = logUi.textArea; if debugLog.enabled @@ -407,7 +407,7 @@ function ensureCurveEditor() return; end if isempty(S.curveEditor) - S.curveEditor = labkit.ui.createAnchorCurveEditor(imageRuntime, size(S.image), ... + S.curveEditor = labkit.ui.tool.anchorEditor(imageRuntime, size(S.image), ... struct('closed', false, ... 'style', 'Curve', ... 'onTrace', debugLog.trace, ... @@ -500,7 +500,7 @@ function refreshImageOverlay() return; end - hImage = labkit.ui.showImageAxes(ax, S.image, 'Image + Circle Fit', ... + hImage = labkit.ui.view.draw(ax, 'image', S.image, 'Image + Circle Fit', ... struct('clearAxes', false)); hold(ax, 'on'); @@ -535,7 +535,7 @@ function refreshImageOverlay() scaleTool.renderOverlay(ax); hold(ax, 'off'); - labkit.ui.enableAxesPopout(ax); + labkit.ui.view.draw(ax, 'popout'); end function plotStaticCurveAnchors(ax) @@ -629,7 +629,7 @@ function resetAxes() end function addLog(message) - labkit.ui.appendLog(txtLog, message); + labkit.ui.view.update(txtLog, 'appendLog', message); debugLog.append(message); end diff --git a/apps/image_measurement/curvature/private/computeCurvatureFit.m b/apps/image_measurement/curvature/private/computeCurvatureFit.m index ac7a180..0fee916 100644 --- a/apps/image_measurement/curvature/private/computeCurvatureFit.m +++ b/apps/image_measurement/curvature/private/computeCurvatureFit.m @@ -23,7 +23,7 @@ end if nargin < 3 || isempty(calibration) - calibration = labkit.ui.scaleBarCalibration(); + calibration = labkit.ui.tool.scaleBarCalibration(); end if nargin < 4 || isempty(doDensify) diff --git a/apps/image_measurement/curvature/private/computeCurveLength.m b/apps/image_measurement/curvature/private/computeCurveLength.m index 6a4ba96..fc7092f 100644 --- a/apps/image_measurement/curvature/private/computeCurveLength.m +++ b/apps/image_measurement/curvature/private/computeCurveLength.m @@ -23,7 +23,7 @@ end if nargin < 3 || isempty(calibration) - calibration = labkit.ui.scaleBarCalibration(); + calibration = labkit.ui.tool.scaleBarCalibration(); end lengthPx = sum(hypot(diff(xPix), diff(yPix))); diff --git a/apps/image_measurement/curvature/private/scaleOptionsFromStruct.m b/apps/image_measurement/curvature/private/scaleOptionsFromStruct.m index 3460096..3ba2556 100644 --- a/apps/image_measurement/curvature/private/scaleOptionsFromStruct.m +++ b/apps/image_measurement/curvature/private/scaleOptionsFromStruct.m @@ -33,7 +33,7 @@ referenceLength = 1; scaleUnit = 'mm'; end - calibration = labkit.ui.scaleBarCalibration(referencePx, referenceLength, scaleUnit); + calibration = labkit.ui.tool.scaleBarCalibration(referencePx, referenceLength, scaleUnit); end function value = positiveOrNaN(value) diff --git a/apps/image_measurement/focus_stack/labkit_FocusStack_app.m b/apps/image_measurement/focus_stack/labkit_FocusStack_app.m index 9b3c499..b473d13 100644 --- a/apps/image_measurement/focus_stack/labkit_FocusStack_app.m +++ b/apps/image_measurement/focus_stack/labkit_FocusStack_app.m @@ -1,7 +1,7 @@ function varargout = labkit_FocusStack_app(varargin) %LABKIT_FOCUSSTACK_APP Fuse a focus image stack into one all-in-focus image. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_FocusStack_app', varargin, nargout, focusStackAppTestHandlers()); if requestHandled varargout = requestOutputs; @@ -31,16 +31,16 @@ 'bottomPlotTitle', 'Focus-depth index map', ... 'showPlotControls', false); workbenchOpts.tabs = [ ... - labkit.ui.tabSpec('filesAnalysis', 'Files + Analysis', [4 1], ... + labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [4 1], ... {250, 235, 185, 170}, ... struct('resizeRows', [1 2 3], ... 'resizeOptions', struct('minTopHeight', 130, 'minBottomHeight', 90))), ... - labkit.ui.tabSpec('summaryResults', 'Summary + Results', [2 1], ... + labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ... {220, '1x'}, ... struct('resizeRows', 1)), ... - labkit.ui.tabSpec('log', 'Log', [1 1], {'1x'})]; + labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})]; - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'Microscope Focus Stack Fusion', ... 'position', [80 60 1440 860], ... 'leftWidth', 390, ... @@ -50,7 +50,7 @@ laySR = ui.summaryResultsGrid; layLog = ui.logGrid; - filePanel = labkit.ui.createPanelGrid(layFA, 'Images', 1, [4 2], ... + filePanel = labkit.ui.view.section(layFA, 'Images', 1, [4 2], ... struct('rowHeight', {{'fit', 'fit', 105, 'fit'}}, ... 'columnWidth', {{'1x', '1x'}})); fileGrid = filePanel.grid; @@ -65,7 +65,7 @@ btnOpenFiles.Layout.Row = 1; btnOpenFiles.Layout.Column = 2; - txtFolder = labkit.ui.createReadOnlyTextField(fileGrid, ... + txtFolder = labkit.ui.view.form(fileGrid, 'readonly', ... 'Value', 'No images loaded'); txtFolder.Layout.Row = 2; txtFolder.Layout.Column = [1 2]; @@ -74,17 +74,17 @@ lbImages.Layout.Row = 3; lbImages.Layout.Column = [1 2]; - txtStackStatus = labkit.ui.createReadOnlyTextField(fileGrid, ... + txtStackStatus = labkit.ui.view.form(fileGrid, 'readonly', ... 'Value', 'Images: 0'); txtStackStatus.Layout.Row = 4; txtStackStatus.Layout.Column = [1 2]; - analysisPanel = labkit.ui.createPanelGrid(layFA, 'Fusion Options', 2, [6 2], ... + analysisPanel = labkit.ui.view.section(layFA, 'Fusion Options', 2, [6 2], ... struct('rowHeight', {{'fit', 'fit', 'fit', 'fit', 'fit', 'fit'}}, ... 'columnWidth', {{155, '1x'}})); analysisGrid = analysisPanel.grid; - [lblFusionPreset, ddFusionPreset] = labkit.ui.createLabeledDropdown(analysisGrid, ... + [lblFusionPreset, ddFusionPreset] = labkit.ui.view.form(analysisGrid, 'dropdown', ... 'Preset:', ... 'Items', {'Balanced', 'Crisp details', 'Smooth transitions', 'Noisy images'}, ... 'Value', 'Balanced', ... @@ -100,21 +100,21 @@ chkRegister.Layout.Row = 2; chkRegister.Layout.Column = [1 2]; - [lblFocusWindow, edtFocusWindow] = labkit.ui.createLabeledSpinner(analysisGrid, ... + [lblFocusWindow, edtFocusWindow] = labkit.ui.view.form(analysisGrid, 'spinner', ... '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.createLabeledSpinner(analysisGrid, ... + [lblSmoothRadius, edtSmoothRadius] = labkit.ui.view.form(analysisGrid, 'spinner', ... '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.createLabeledSpinner(analysisGrid, ... + [lblUncertainBlend, edtUncertainBlend] = labkit.ui.view.form(analysisGrid, 'spinner', ... 'Uncertain blend (%):', 'Value', 5, 'Limits', [0 100], 'Step', 1); lblUncertainBlend.Layout.Row = 5; lblUncertainBlend.Layout.Column = 1; @@ -127,7 +127,7 @@ btnRun.Layout.Row = 6; btnRun.Layout.Column = [1 2]; - exportPanel = labkit.ui.createPanelGrid(layFA, 'Export', 3, [3 1], ... + exportPanel = labkit.ui.view.section(layFA, 'Export', 3, [3 1], ... struct('rowHeight', {{'fit', 'fit', 'fit'}})); exportGrid = exportPanel.grid; @@ -144,7 +144,7 @@ 'ButtonPushedFcn', @onExportSummary); btnExportSummary.Layout.Row = 3; - labkit.ui.createReadOnlyTextPanel(layFA, 'Workflow Notes', 4, { ... + labkit.ui.view.panel(layFA, 'text', 'Workflow Notes', 4, { ... '1. Load a folder or select one or more image files from the same microscope field of view.', ... '2. Use file selection when a folder contains bad frames that should be excluded.', ... '3. Start with Balanced. Use Crisp for fine texture, Smooth for visible seams, Noisy for grainy images.', ... @@ -156,10 +156,10 @@ resultTable.Layout.Row = 1; txtDetails = uitextarea(laySR, 'Editable', 'off'); - txtDetails.Layout.Row = labkit.ui.layoutRow(laySR, 2); + labkit.ui.view.place(txtDetails, laySR, 2); txtDetails.Value = {'Load a focus image folder or select image files to begin.'}; - logUi = labkit.ui.createLogPanel(layLog, 1, {'Ready.'}); + logUi = labkit.ui.view.panel(layLog, 'log', 1, {'Ready.'}); txtLog = logUi.textArea; if debugLog.enabled debugLog.attachTextLog(txtLog); @@ -255,7 +255,7 @@ function onRunFocusStack(~, ~) busyOpts.message = 'Fusing selected microscope images...'; busyOpts.controls = focusStackBusyControls(); try - payload = labkit.ui.runWithBusyState(fig, ... + payload = labkit.ui.app.runBusy(fig, ... @() runFocusStackComputation(opts, registerStack), busyOpts); catch ME showError('Focus stacking failed', ME.message); @@ -385,15 +385,15 @@ function onExportSummary(~, ~) function refreshPreview() if S.result.ok - labkit.ui.showImageAxes(ui.topAxes, S.result.fused, ... + labkit.ui.view.draw(ui.topAxes, 'image', S.result.fused, ... 'Fused all-in-focus image'); - labkit.ui.showImageAxes(ui.bottomAxes, ... + labkit.ui.view.draw(ui.bottomAxes, 'image', ... focusIndexRgb(S.result.focusIndex, S.result.inputCount), ... 'Focus-depth index map'); elseif ~isempty(S.images) - labkit.ui.showImageAxes(ui.topAxes, previewImage(S.images{1}), ... + labkit.ui.view.draw(ui.topAxes, 'image', previewImage(S.images{1}), ... 'First source image'); - labkit.ui.hardResetAxis(ui.bottomAxes, 'Focus-depth index map', true); + labkit.ui.view.draw(ui.bottomAxes, 'reset', 'Focus-depth index map', true); else resetPreviewAxes(); end @@ -432,12 +432,12 @@ function updateControls() end function resetPreviewAxes() - labkit.ui.hardResetAxis(ui.topAxes, 'Fused all-in-focus image', true); - labkit.ui.hardResetAxis(ui.bottomAxes, 'Focus-depth index map', true); + labkit.ui.view.draw(ui.topAxes, 'reset', 'Fused all-in-focus image', true); + labkit.ui.view.draw(ui.bottomAxes, 'reset', 'Focus-depth index map', true); end function addLog(message) - labkit.ui.appendLog(txtLog, message); + labkit.ui.view.update(txtLog, 'appendLog', message); debugLog.append(message); end diff --git a/apps/wearable/labkit_ECGPrint_app.m b/apps/wearable/labkit_ECGPrint_app.m index 098c67f..0c11e7d 100644 --- a/apps/wearable/labkit_ECGPrint_app.m +++ b/apps/wearable/labkit_ECGPrint_app.m @@ -1,7 +1,7 @@ function varargout = labkit_ECGPrint_app(varargin) %LABKIT_ECGPRINT_APP Explore ECG quality, SNR, and printable waveforms. - [requestHandled, requestOutputs, debugLog] = labkit.ui.dispatchAppRequest( ... + [requestHandled, requestOutputs, debugLog] = labkit.ui.app.dispatchRequest( ... 'labkit_ECGPrint_app', varargin, nargout); if requestHandled varargout = requestOutputs; @@ -34,15 +34,15 @@ 'rightRowHeight', {{'1.2x', '1x', '1x', '1x'}}, ... 'rightRowSpacing', 8); opts.tabs = [ ... - labkit.ui.tabSpec('filesAnalysis', 'Files + Analysis', [6 1], ... + labkit.ui.app.tab('filesAnalysis', 'Files + Analysis', [6 1], ... {140, 255, 120, 235, 100, 125}, ... struct('resizeRows', [1 2 3 4 5])), ... - labkit.ui.tabSpec('summaryResults', 'Summary + Results', [2 1], ... + labkit.ui.app.tab('summaryResults', 'Summary + Results', [2 1], ... {210, '1x'}, ... struct('resizeRows', 1)), ... - labkit.ui.tabSpec('log', 'Log', [1 1], {'1x'})]; + labkit.ui.app.tab('log', 'Log', [1 1], {'1x'})]; - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'ECG Signal Print + SNR Explorer', ... 'position', [80 70 1480 880], ... 'leftWidth', 410, ... @@ -52,7 +52,7 @@ laySR = ui.summaryResultsGrid; layLog = ui.logGrid; - recordingPanel = labkit.ui.createPanelGrid(layFA, 'Recording', 1, [3 2], ... + recordingPanel = labkit.ui.view.section(layFA, 'Recording', 1, [3 2], ... struct('rowHeight', {repmat({'fit'}, 1, 3)}, ... 'columnWidth', {{135, '1x'}})); recordingGrid = recordingPanel.grid; @@ -61,7 +61,7 @@ btnOpen.Layout.Row = 1; btnOpen.Layout.Column = [1 2]; - txtFile = labkit.ui.createReadOnlyTextField(recordingGrid, 'Value', 'No file loaded'); + txtFile = labkit.ui.view.form(recordingGrid, 'readonly', 'Value', 'No file loaded'); txtFile.Layout.Row = 2; txtFile.Layout.Column = [1 2]; @@ -70,17 +70,17 @@ btnPreviewHeader.Layout.Row = 3; btnPreviewHeader.Layout.Column = [1 2]; - importPanel = labkit.ui.createPanelGrid(layFA, 'Import Parsing', 2, [8 2], ... + importPanel = labkit.ui.view.section(layFA, 'Import Parsing', 2, [8 2], ... struct('rowHeight', {repmat({'fit'}, 1, 8)}, ... 'columnWidth', {{135, '1x'}})); importGrid = importPanel.grid; - txtImportStatus = labkit.ui.createReadOnlyTextField(importGrid, ... + txtImportStatus = labkit.ui.view.form(importGrid, 'readonly', ... 'Value', 'Open a recording to inspect import settings.'); txtImportStatus.Layout.Row = 1; txtImportStatus.Layout.Column = [1 2]; - [lblHeaderLine, edtHeaderLine] = labkit.ui.createLabeledSpinner(importGrid, ... + [lblHeaderLine, edtHeaderLine] = labkit.ui.view.form(importGrid, 'spinner', ... 'CSV header line:', 'Value', 0, 'Limits', [0 Inf], 'Step', 1, ... 'ValueChangedFcn', @onImportOptionChanged); lblHeaderLine.Layout.Row = 2; @@ -88,7 +88,7 @@ edtHeaderLine.Layout.Row = 2; edtHeaderLine.Layout.Column = 2; - [lblHasHeader, ddHasHeader] = labkit.ui.createLabeledDropdown(importGrid, ... + [lblHasHeader, ddHasHeader] = labkit.ui.view.form(importGrid, 'dropdown', ... 'CSV header:', ... 'Items', {'Auto', 'Yes', 'No'}, ... 'Value', 'Auto', ... @@ -98,7 +98,7 @@ ddHasHeader.Layout.Row = 3; ddHasHeader.Layout.Column = 2; - [lblTimeColumn, edtTimeColumn] = labkit.ui.createLabeledEditField(importGrid, ... + [lblTimeColumn, edtTimeColumn] = labkit.ui.view.form(importGrid, 'edit', ... 'Time column:', 'text', 'Value', '', ... 'ValueChangedFcn', @onImportOptionChanged); lblTimeColumn.Layout.Row = 4; @@ -106,7 +106,7 @@ edtTimeColumn.Layout.Row = 4; edtTimeColumn.Layout.Column = 2; - [lblTimeUnit, ddTimeUnit] = labkit.ui.createLabeledDropdown(importGrid, ... + [lblTimeUnit, ddTimeUnit] = labkit.ui.view.form(importGrid, 'dropdown', ... 'Time unit:', ... 'Items', {'Auto', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'}, ... 'Value', 'Auto', ... @@ -116,7 +116,7 @@ ddTimeUnit.Layout.Row = 5; ddTimeUnit.Layout.Column = 2; - [lblSignalColumns, edtSignalColumns] = labkit.ui.createLabeledEditField(importGrid, ... + [lblSignalColumns, edtSignalColumns] = labkit.ui.view.form(importGrid, 'edit', ... 'Signal columns:', 'text', 'Value', '', ... 'ValueChangedFcn', @onImportOptionChanged); lblSignalColumns.Layout.Row = 6; @@ -124,7 +124,7 @@ edtSignalColumns.Layout.Row = 6; edtSignalColumns.Layout.Column = 2; - [lblFallbackFs, edtFallbackFs] = labkit.ui.createLabeledSpinner(importGrid, ... + [lblFallbackFs, edtFallbackFs] = labkit.ui.view.form(importGrid, 'spinner', ... 'Fallback Fs:', 'Value', 2000, 'Limits', [0 Inf], 'Step', 100, ... 'ValueChangedFcn', @onImportOptionChanged); lblFallbackFs.Layout.Row = 7; @@ -137,52 +137,52 @@ btnRefreshImport.Layout.Row = 8; btnRefreshImport.Layout.Column = [1 2]; - channelPanel = labkit.ui.createPanelGrid(layFA, 'Channel + ROI', 3, [3 2], ... + channelPanel = labkit.ui.view.section(layFA, 'Channel + ROI', 3, [3 2], ... struct('rowHeight', {repmat({'fit'}, 1, 3)}, ... 'columnWidth', {{135, '1x'}})); channelGrid = channelPanel.grid; - [lblChannel, ddChannel] = labkit.ui.createLabeledDropdown(channelGrid, 'Channel:', ... + [lblChannel, ddChannel] = labkit.ui.view.form(channelGrid, 'dropdown', 'Channel:', ... 'Items', {'(none)'}, 'Value', '(none)', 'ValueChangedFcn', @onChannelChanged); lblChannel.Layout.Row = 1; lblChannel.Layout.Column = 1; ddChannel.Layout.Row = 1; ddChannel.Layout.Column = 2; - [lblStart, edtStart] = labkit.ui.createLabeledSpinner(channelGrid, ... + [lblStart, edtStart] = labkit.ui.view.form(channelGrid, 'spinner', ... '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.createLabeledSpinner(channelGrid, ... + [lblEnd, edtEnd] = labkit.ui.view.form(channelGrid, 'spinner', ... 'ROI end (s):', 'Value', 0, 'Limits', [0 Inf], 'Step', 1); lblEnd.Layout.Row = 3; lblEnd.Layout.Column = 1; edtEnd.Layout.Row = 3; edtEnd.Layout.Column = 2; - procPanel = labkit.ui.createPanelGrid(layFA, 'Signal Processing + SNR', 4, [9 2], ... + procPanel = labkit.ui.view.section(layFA, 'Signal Processing + SNR', 4, [9 2], ... struct('rowHeight', {repmat({'fit'}, 1, 9)}, ... 'columnWidth', {{135, '1x'}})); procGrid = procPanel.grid; - [lblLow, edtLow] = labkit.ui.createLabeledSpinner(procGrid, ... + [lblLow, edtLow] = labkit.ui.view.form(procGrid, 'spinner', ... '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.createLabeledSpinner(procGrid, ... + [lblHigh, edtHigh] = labkit.ui.view.form(procGrid, 'spinner', ... '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.createLabeledDropdown(procGrid, ... + [lblPeakMethod, ddPeakMethod] = labkit.ui.view.form(procGrid, 'dropdown', ... 'Peak method:', ... 'Items', {'QRS streaming', 'Pan-Tompkins', 'Local peaks'}, ... 'Value', 'QRS streaming'); @@ -191,28 +191,28 @@ ddPeakMethod.Layout.Row = 3; ddPeakMethod.Layout.Column = 2; - [lblPeakDist, edtPeakDist] = labkit.ui.createLabeledSpinner(procGrid, ... + [lblPeakDist, edtPeakDist] = labkit.ui.view.form(procGrid, 'spinner', ... '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.createLabeledSpinner(procGrid, ... + [lblWin, edtWin] = labkit.ui.view.form(procGrid, 'spinner', ... '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.createLabeledSpinner(procGrid, ... + [lblTopN, edtTopN] = labkit.ui.view.form(procGrid, 'spinner', ... '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.createLabeledSpinner(procGrid, ... + [lblSmooth, edtSmooth] = labkit.ui.view.form(procGrid, 'spinner', ... 'Smooth beats:', 'Value', 15, 'Limits', [1 Inf], 'Step', 1, ... 'ValueChangedFcn', @(~,~) refreshPlots()); lblSmooth.Layout.Row = 7; @@ -220,7 +220,7 @@ edtSmooth.Layout.Row = 7; edtSmooth.Layout.Column = 2; - [lblView, ddTemplateView] = labkit.ui.createLabeledDropdown(procGrid, ... + [lblView, ddTemplateView] = labkit.ui.view.form(procGrid, 'dropdown', ... 'Template plot:', ... 'Items', {'Template + residual band', 'Template + segments'}, ... 'Value', 'Template + residual band', ... @@ -235,7 +235,7 @@ btnAnalyze.Layout.Row = 9; btnAnalyze.Layout.Column = [1 2]; - exportPanel = labkit.ui.createPanelGrid(layFA, 'Exports', 5, [2 1], ... + exportPanel = labkit.ui.view.section(layFA, 'Exports', 5, [2 1], ... struct('rowHeight', {{'fit','fit'}})); exportGrid = exportPanel.grid; btnExportSegments = uibutton(exportGrid, 'Text', 'Export segment SNR CSV', ... @@ -245,20 +245,20 @@ 'ButtonPushedFcn', @onExportWaveform); btnExportOverlay.Layout.Row = 2; - labkit.ui.createReadOnlyTextPanel(layFA, 'Workflow Notes', 6, { ... + labkit.ui.view.panel(layFA, 'text', 'Workflow Notes', 6, { ... '1. Open MAT/CSV data, select a numeric channel, and optionally set a time ROI.', ... '2. Use File Header Preview and Import Parsing only when CSV/text auto-detection needs correction.', ... '3. Analysis filters the selected channel with edge padding, then crops the filtered signal to the ROI for peak/SNR measurement.'}); summaryTable = uitable(laySR, 'ColumnName', {'Metric','Value'}, ... 'Data', initialSummaryRows()); - summaryTable.Layout.Row = labkit.ui.layoutRow(laySR, 1); + labkit.ui.view.place(summaryTable, laySR, 1); - previewUi = labkit.ui.createReadOnlyTextPanel(laySR, 'File Header Preview', 2, ... + previewUi = labkit.ui.view.panel(laySR, 'text', 'File Header Preview', 2, ... {'Open a CSV/text file, then use Preview file header.'}); txtFilePreview = previewUi.textArea; - logUi = labkit.ui.createLogPanel(layLog, 1, {'Ready.'}); + logUi = labkit.ui.view.panel(layLog, 'log', 1, {'Ready.'}); txtLog = logUi.textArea; ui.waveAxes = uiaxes(ui.rightGrid); @@ -564,7 +564,7 @@ function updateSummary() function refreshTemplatePlot() ax = ui.templateAxes; - labkit.ui.hardResetAxis(ax, 'Template + Residual Band'); + labkit.ui.view.draw(ax, 'reset', 'Template + Residual Band'); xlabel(ax, 'Time from peak (s)'); ylabel(ax, 'Amplitude'); if isempty(S.segments) || isempty(S.template) || isempty(S.segments.values) @@ -710,22 +710,22 @@ function shadeMeasurementWindows(ax) end function resetAxes() - labkit.ui.hardResetAxis(ui.waveAxes, 'Waveform + Peaks'); + labkit.ui.view.draw(ui.waveAxes, 'reset', 'Waveform + Peaks'); xlabel(ui.waveAxes, 'Time (s)'); ylabel(ui.waveAxes, 'Amplitude'); - labkit.ui.hardResetAxis(ui.noiseAxes, 'Template Noise RMS Over Time'); + labkit.ui.view.draw(ui.noiseAxes, 'reset', 'Template Noise RMS Over Time'); xlabel(ui.noiseAxes, 'Time (s)'); ylabel(ui.noiseAxes, 'Noise RMS'); - labkit.ui.hardResetAxis(ui.snrAxes, 'Template SNR Over Time'); + labkit.ui.view.draw(ui.snrAxes, 'reset', 'Template SNR Over Time'); xlabel(ui.snrAxes, 'Time (s)'); ylabel(ui.snrAxes, 'SNR (dB)'); - labkit.ui.hardResetAxis(ui.templateAxes, 'Template + Residual Band'); + labkit.ui.view.draw(ui.templateAxes, 'reset', 'Template + Residual Band'); xlabel(ui.templateAxes, 'Time from peak (s)'); ylabel(ui.templateAxes, 'Amplitude'); end function addLog(message) - labkit.ui.appendLog(txtLog, message); + labkit.ui.view.update(txtLog, 'appendLog', message); debugLog.append(message); end diff --git a/docs/apps.md b/docs/apps.md index b3a4e5c..98e834c 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -59,7 +59,7 @@ The app owns: - failed-row behavior - callback ordering, alerts, and log wording -Every public app entry point should preserve its launch name, route internal test/debug requests through `labkit.ui.dispatchAppRequest`, build the GUI with `labkit.ui.createAppShell`, and keep visible debug trace wired into the Log tab during debug launches. Image apps with drawing, scale bars, ROI, or preview scroll should pass a `labkit.ui.createInteractionRuntime` result into reusable tools instead of owning figure pointer callbacks directly. +Every public app entry point should preserve its launch name, route internal test/debug requests through `labkit.ui.app.dispatchRequest`, build the GUI with `labkit.ui.app.createShell`, and keep visible debug trace wired into the Log tab during debug launches. Image apps with drawing, scale bars, ROI, or preview scroll should pass a `labkit.ui.tool.createRuntime` result into reusable tools instead of owning figure pointer callbacks directly. Move code into `+labkit` only when it is reusable without app vocabulary, testable independently, and useful beyond one workflow. When a documented UI tool owns app-neutral interaction mechanics, the app should consume that tool and keep workflow meaning, summaries, and exports app-local. @@ -100,7 +100,7 @@ Define these before adding controls or helpers: 10. GUI shell spec, debug trace behavior, and file-selection mode ``` -Start from the closest existing app, reduce it to the needed workflow, and preserve ownership boundaries. Prefer `labkit.ui.createAppShell` even for small apps so daily interaction stays consistent across app families. +Start from the closest existing app, reduce it to the needed workflow, and preserve ownership boundaries. Prefer `labkit.ui.app.createShell` even for small apps so daily interaction stays consistent across app families. ## Validation diff --git a/docs/architecture.md b/docs/architecture.md index 4327730..7c23995 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -20,7 +20,7 @@ apps/ category folders containing public app entry points or app subfolders Short version: ```text -labkit.ui layered GUI foundation for shells, controls, axes, runtime, tools, diagnostics +labkit.ui layered GUI foundation split into app/view/tool/diag facades labkit.dta current electrochemistry/Gamry DTA file and session facade labkit.biosignal current wearable/physiological time-series facade apps/ experiment-specific workflow apps @@ -52,7 +52,7 @@ labkit_ECGPrint_app | Area | Responsibility | | --- | --- | | `apps/` | Public app entry points and app-specific workflow code, including app-owned private helpers. | -| `+labkit/+ui` | Reusable GUI shell, panels, controls, axes, interaction runtime, tools, diagnostics, logs, and app-neutral UI helpers. | +| `+labkit/+ui` | Reusable GUI app/view/tool/diagnostics facades plus private implementation helpers. | | `+labkit/+dta` | GUI-free DTA discovery, loading, session, pulse, and parsed curve/table facade. | | `+labkit/+biosignal` | GUI-free recording loading, channel extraction, waveform processing, events, segments, templates, measurements, and group comparisons. | | `private/` helpers | Parser, normalization, item/session construction, pulse, and implementation details hidden behind the owning facade. | @@ -67,18 +67,16 @@ Biosignal code should not depend on GUI state, DTA, or app entry points. Low-lev UI helpers should build or update generic controls and draw prepared data. Apps pass labels, callbacks, prepared vectors, tables, debug contexts, and option values into UI helpers. UI helpers should not call DTA parsers, own formulas, define result fields, or decide export schemas. -Reusable image-interaction tools may own app-neutral UI state when the interaction itself is generic. Image apps with custom axes behavior should register default scroll or interaction hooks through `labkit.ui.createInteractionRuntime`; direct app ownership of image-tool figure/axes pointer callbacks is outside the app boundary. Apps remain responsible for image loading/redrawing, edit-mode coordination, scientific calculations, summaries, and exports. +Reusable image-interaction tools may own app-neutral UI state when the interaction itself is generic. Image apps with custom axes behavior should register default scroll or interaction hooks through `labkit.ui.tool.createRuntime`; direct app ownership of image-tool figure/axes pointer callbacks is outside the app boundary. Apps remain responsible for image loading/redrawing, edit-mode coordination, scientific calculations, summaries, and exports. The app-facing UI API is intentionally layered: | Layer | Responsibility | App-facing API | | --- | --- | --- | -| Shell | Figure shell, tabs, split panes, left/right layout. | `createAppShell`, `tabSpec`, layout helpers. | -| Controls | Labeled controls, read-only fields, tables, log panels, file panels. | Control/panel helpers under `labkit.ui.*`. | -| Axes | Axes creation/reset, image display, prepared plotting, popout. | `createAxes`, `hardResetAxis`, `showImageAxes`, `plotXY`, `enableAxesPopout`. | -| Runtime | Exclusive image interaction sessions, callback ownership, busy state. | `createInteractionRuntime`, `runWithBusyState`. | -| Tools | App-neutral composed tools such as anchor editing and scale bars. | `createAnchorCurveEditor`, `createScaleBarTool`, `createScaleBarPanel`. | -| Diagnostics | Debug launch, visible trace, callback instrumentation, request dispatch. | `dispatchAppRequest`, `createDebugContext`. | +| App | Figure shell, tabs, request dispatch, busy state. | `labkit.ui.app.createShell`, `tab`, `dispatchRequest`, `runBusy`. | +| View | Sections, forms, reusable panels, axes, rendering actions, and UI state updates. | `labkit.ui.view.section`, `form`, `panel`, `axes`, `draw`, `update`, `place`. | +| Tool | Exclusive interaction runtime and composed tools. | `labkit.ui.tool.createRuntime`, `anchorEditor`, `scaleBar`, `scaleBarCalibration`. | +| Diagnostics | Debug launch, visible trace, callback instrumentation. | `labkit.ui.diag.createContext`. | App-specific analysis, plotting annotations, result summaries, CSV schemas, failed-row behavior, and workflow wording belong in the owning app file or app-owned private helpers. The default private-helper location for a large app is `apps///private/`; `apps//private/` should be reserved for helpers shared by multiple apps in that family. @@ -115,7 +113,10 @@ GUI launch/layout checks live in source-aligned suites and are enabled with `--g ## Current Package Surface -- `labkit.ui`: app shell specs, tab specs, file-selection panel, interaction runtime, scale-bar panel/tool, scale-bar calibration, log panel, panel grids, row resizing, axes creation/reset, axes popout, image display, anchor curve editing, prepared-X/Y plotting, result tables, plot controls, listbox state, busy-state feedback, labeled controls, read-only fields, request dispatch, and debug/trace support for app maintainers. +- `labkit.ui.app`: shell specs, tab specs, internal request dispatch, and busy-state feedback. +- `labkit.ui.view`: sections, unified form controls, file panels, logs, tables, listbox state, axes reset/popout, image display, and prepared-X/Y plotting. +- `labkit.ui.tool`: interaction runtime, anchor editing, scale-bar tool, and scale-bar calibration. +- `labkit.ui.diag`: debug context, visible trace, callback instrumentation, and log mirroring. - `labkit.dta`: DTA file discovery, type detection, single/batch/folder loading, pulse detection, item construction behind the facade, parsed table/curve access, session save/load, and session add/remove/select operations. - `labkit.biosignal`: MAT timetable and delimited table recording loading, channel extraction, time ROI cropping, filtering, ECG/QRS peak detection, event-centered segmentation, template construction, template-residual SNR-style measurements, and group comparisons. diff --git a/docs/testing.md b/docs/testing.md index fe04b2d..afa93ac 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -90,7 +90,7 @@ UI framework changes should cover the affected layer rather than only the change | UI layer | Automated coverage | | --- | --- | -| Public surface | `project` suite checks stable public API, deprecated compatibility API, and private implementation packages. | +| Public surface | `project` suite checks the layered `labkit.ui.app/view/tool/diag` API and private implementation packages. | | Shell/layout | `labkit/ui --gui` and affected app-family `--gui` suites. | | Runtime/tools | `labkit/ui --gui` runtime, anchor-editor, and scale-bar tool tests. | | Diagnostics | `labkit/ui --gui` debug instrumentation tests plus `apps/smoke --gui` debug launch trace checks. | diff --git a/docs/ui.md b/docs/ui.md index c95f942..3a5ff6c 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -1,34 +1,30 @@ # UI Library -`labkit.ui.*` is the reusable MATLAB GUI foundation. It provides a standard app-shell shape and domain-neutral UI helpers for lab-internal tools. +`labkit.ui` is the reusable MATLAB GUI foundation. It is now split into four app-facing facade packages: -The UI library should stay small and app-neutral. It owns reusable layout and interaction mechanics; apps own scientific workflow, wording, result definitions, plotting choices, and exports. - -## Layer Map - -`labkit.ui` is organized around app-facing layers: - -| Layer | Owns | Main APIs | +| Facade | Owns | Main APIs | | --- | --- | --- | -| Shell | Figure shell, tabs, split panes, left/right layout, row resizing. | `createAppShell`, `tabSpec`, `layoutRow`, `addRowResizeHandle`. | -| Controls | Labeled controls, read-only fields, file panels, tables, log panels. | `createFileSelectionPanel`, `createPanelGrid`, labeled/read-only helpers, `createResultTablePanel`, `createLogPanel`. | -| Axes | Axes creation/reset, prepared image/plot display, popout. | `createAxes`, `hardResetAxis`, `showImageAxes`, `plotXY`, `enableAxesPopout`, `popoutAxes`. | -| Runtime | Exclusive interaction sessions, callback ownership, busy state. | `createInteractionRuntime`, `runWithBusyState`. | -| Tools | App-neutral composed tools. | `createAnchorCurveEditor`, `createScaleBarTool`, `createScaleBarPanel`, `scaleBarCalibration`. | -| Diagnostics | Debug launch, visible trace, request dispatch, callback instrumentation. | `dispatchAppRequest`, `createDebugContext`. | +| `labkit.ui.app` | Figure shell, tabs, request dispatch, busy state. | `createShell`, `tab`, `dispatchRequest`, `runBusy`. | +| `labkit.ui.view` | Sections, forms, component panels, axes rendering, and app-neutral UI state updates. | `section`, `form`, `panel`, `axes`, `draw`, `update`, `place`. | +| `labkit.ui.tool` | Reusable composed image tools and interaction runtime. | `createRuntime`, `anchorEditor`, `scaleBar`, `scaleBarCalibration`. | +| `labkit.ui.diag` | Debug launch context, visible trace, callback instrumentation. | `createContext`. | -Stable app-facing APIs are documented in this file. `createWorkbench`, `createImageAxesRuntime`, `createAppDebugLog`, and `handleAppRequest` remain as deprecated compatibility surface for one migration cycle; new app code should not call them. +The root `labkit.ui.*` flat helper surface has been removed. Apps should call the facade that owns the behavior they need. Private implementation details live under each facade's `private/` folder. -## Standard App Shell +## Standard Shell -Every app should start from the same basic shell: +Every app should start from `labkit.ui.app.createShell`: -```text -left side: resizable tabbed controls -right side: live plots, images, tables, or primary output +```matlab +opts = struct('rightKind', 'dualPlot'); +ui = labkit.ui.app.createShell(struct( ... + 'title', 'Example App', ... + 'position', [90 70 1200 800], ... + 'leftWidth', 380, ... + 'options', opts)); ``` -The default left tabs are: +Default left tabs are: ```text Files + Analysis @@ -36,210 +32,106 @@ Summary + Results Log ``` -Apps may pass custom tab specs when a workflow needs different pages. The app still owns the controls inside each tab. -The left tab host and each tab content grid are scrollable, so app-specific sections can extend below the visible window without hiding controls. -Tabs can also declare draggable row boundaries through `resizeRows`; this is a shell-level behavior, and app code should continue to use logical grid row numbers. - -## Core Entry Point - -Use `labkit.ui.createAppShell` for both small and large apps: +Custom tabs use `labkit.ui.app.tab`: ```matlab -opts = struct(); -opts.rightTitle = 'Plots'; -opts.rightGridSize = [1 1]; -opts.rightRowHeight = {'1x'}; -ui = labkit.ui.createAppShell(struct( ... - 'title', titleText, ... - 'position', position, ... - 'leftWidth', leftWidth, ... - 'options', opts)); - -dualOpts = struct('rightKind', 'dualPlot'); -ui = labkit.ui.createAppShell(struct( ... - 'title', titleText, ... - 'position', position, ... - 'leftWidth', leftWidth, ... - 'options', dualOpts)); - -imageOpts = struct('rightKind', 'dualPlot', 'showPlotControls', false); -ui = labkit.ui.createAppShell(struct( ... - 'title', titleText, ... - 'position', position, ... - 'leftWidth', leftWidth, ... - 'options', imageOpts)); -``` - -Use `opts.rightKind = 'dualPlot'` for the common top/bottom live-plot layout. By default it includes small top/bottom control panels for axis selectors and plot options. Set `opts.showPlotControls = false` for image/overlay apps that only need the two output axes; this avoids empty control rows compressing the plot area. - -For custom right-side arrangements, pass `rightGridSize`, `rightRowHeight`, and `rightRowSpacing`. - -App files should not rebuild split-pane layout plumbing, own their own separator-drag behavior, or introduce compatibility shell wrappers around `createAppShell`. - -### `createAppShell` Spec - -| Field | Type | Default | Valid values / meaning | -| --- | --- | --- | --- | -| `title` | char/string | required | Figure title. | -| `position` | numeric 1-by-4 | required | MATLAB figure position `[x y width height]`. | -| `leftWidth` | scalar | required | Initial left controls width in pixels. | -| `options` | struct | empty | Shell options below. | - -### Shell Options - -| Option | Type | Default | Valid values / meaning | -| --- | --- | --- | --- | -| `rightKind` | string | `custom` | `custom` or `dualPlot`. | -| `rightGridSize` | numeric row vector | `[1 1]` | Right output grid size for custom right panes. | -| `rightRowHeight` | cell row | `{'1x'}` | Right output grid row heights for custom right panes. | -| `rightRowSpacing` | scalar | `8`, or `10` for `dualPlot` | Right output grid row spacing. | -| `showPlotControls` | logical | `true` | `dualPlot` only; false uses two plot rows without option panels. | -| `controlsTitle` | char/string | `Controls` | Left panel title. | -| `rightTitle` | char/string | `Plots` | Right panel title. | -| `topPlotTitle` | char/string | `Top Plot` | `dualPlot` top controls/axes title. | -| `bottomPlotTitle` | char/string | `Bottom Plot` | `dualPlot` bottom controls/axes title. | -| `tabs` | tabSpec array | standard three tabs | Custom left-tab definitions. | - -Custom left-tab sizing is declared in the tab spec: - -```matlab -opts.tabs = labkit.ui.tabSpec( ... +opts.tabs = labkit.ui.app.tab( ... 'filesAnalysis', 'Files + Analysis', [4 1], ... - {240, 210, 330, 170}, ... + {180, 220, 260, 140}, ... struct('resizeRows', [1 2 3])); ``` -Here `resizeRows = [1 2 3]` means the user can drag the boundaries after logical rows 1, 2, and 3. The framework may create internal handle rows, but app code still places sections in rows 1 through 4 through LabKit layout helpers. - -### `tabSpec` Options +The shell owns split panes, scrollable tab grids, and row resize handles. Apps own the controls placed inside returned grids. -| Option | Type | Default | Valid values / meaning | -| --- | --- | --- | --- | -| `columnWidth` | cell row | all `{'1x'}` | Column widths for the tab content grid. | -| `resizeRows` | numeric row vector | `[]` | Logical rows after which draggable height handles are inserted. | -| `resizeOptions` | struct | empty | Passed to row resize handle creation. | -| `padding` | numeric 1-by-4 | `[8 8 8 8]` | Tab content grid padding. | -| `rowSpacing` | scalar | `10` | Tab content grid row spacing. | -| `columnSpacing` | scalar | MATLAB default | Tab content grid column spacing. | +## Views And Forms -## Common Helpers - -Construction helpers: +Use `labkit.ui.view.section` for titled app-defined sections: ```matlab -labkit.ui.createFileSelectionPanel(parent, labels, callbacks, opts); -labkit.ui.createPanelGrid(parent, titleText, row, gridSize, opts); -labkit.ui.createPlotOptionsPanel(parent, numRows, row); -labkit.ui.createTopBottomPlotControls(topPanel, bottomPanel, xItems, yItems, topDefaults, bottomDefaults, onChange); -labkit.ui.createLabeledDropdown(parent, labelText, ...); -labkit.ui.createLabeledEditField(parent, labelText, style, ...); -labkit.ui.createLabeledSpinner(parent, labelText, ...); -labkit.ui.createReadOnlyTextField(parent, ...); -labkit.ui.createReadOnlyTextPanel(parent, titleText, row, lines, opts); -labkit.ui.createResultTablePanel(parent, titleText, row, columnNames, initialData); -labkit.ui.createLogPanel(parent, row, initialValue); -runtime = labkit.ui.createInteractionRuntime(ax, opts); -labkit.ui.createAnchorCurveEditor(runtime, imageSize, opts); -labkit.ui.createScaleBarPanel(parent, row, opts); -labkit.ui.createScaleBarTool(parent, row, runtime, opts); -labkit.ui.runWithBusyState(fig, workFcn, opts); -labkit.ui.dispatchAppRequest(appName, args, nout, handlers); -labkit.ui.createDebugContext(appName, opts); +section = labkit.ui.view.section(layFA, 'Analysis Settings', 2, [3 2]); +grid = section.grid; ``` -State and rendering helpers: +Use `labkit.ui.view.form` as the single public control entry point. It replaces separate labeled spinner/dropdown/edit/read-only helpers: ```matlab -labkit.ui.appendLog(txtLog, message); -[value, idx] = labkit.ui.refreshListboxSelection(lbFiles, names, preferredSelection, opts); -info = labkit.ui.plotXY(ax, x, y, labels, opts); -cal = labkit.ui.scaleBarCalibration(referencePixels, referenceLength, unitName); -labkit.ui.enableAxesPopout(ax); -fig = labkit.ui.popoutAxes(ax); -hImage = labkit.ui.showImageAxes(ax, imageData, titleText); -``` - -Use `createPanelGrid` for app-defined sections that only need the standard panel/grid styling. Fixed-height parent tab rows are automatically grown when the declared height is smaller than the section's estimated control height, so default app startup layouts should avoid clipping controls while still allowing user row resizing and scrolling. Use `createLabeledSpinner` for numeric settings that should support click/step adjustment, `createReadOnlyTextField` for single-line status or path display, and `createReadOnlyTextPanel` for app-owned usage notes, file previews, or other read-only multiline text. Use `refreshListboxSelection` for generic single- or multi-select listbox state updates. +[lblMode, ddMode] = labkit.ui.view.form(grid, 'dropdown', ... + 'Mode:', 'Items', {'Auto', 'Manual'}, 'Value', 'Auto', ... + 'ValueChangedFcn', @onModeChanged); -### `createPanelGrid` Options +[lblN, edN] = labkit.ui.view.form(grid, 'spinner', ... + 'Samples:', 'Value', 10, 'Limits', [1 Inf], 'Step', 1); -| Option | Type | Default | Valid values / meaning | -| --- | --- | --- | --- | -| `rowHeight` | cell row | all `{'fit'}` | Child grid row heights. | -| `columnWidth` | cell row | all `{'1x'}` | Child grid column widths. | -| `padding` | numeric 1-by-4 | `[8 8 8 8]` | Child grid padding. | -| `rowSpacing` | scalar | `8` | Child grid row spacing. | -| `columnSpacing` | scalar | `8` | Child grid column spacing. | -| `autoGrowParentRow` | logical | `true` | Grows undersized fixed parent tab rows. | -| `minPanelHeight` | scalar | estimated from child grid | Minimum height used by parent-row auto growth. | +txtStatus = labkit.ui.view.form(grid, 'readonly', ... + 'Value', 'No file loaded'); -Axes created through `labkit.ui.createAxes`, app-shell dual-plot panes, or reset with `labkit.ui.hardResetAxis` get a standard right-click context action named `Open axes in new figure`. The same context menu is attached to plotted child objects such as images, lines, overlays, and ROI previews so image-heavy apps do not block the action. MATLAB does not reliably propagate axes context menus to graphics children created later, so app-local renderers that create new image or overlay objects should call `labkit.ui.enableAxesPopout(ax)` after drawing. It copies the current axes contents, labels, scales, grid state, and basic styling into a separate MATLAB figure for manual editing or export. The copied axes use automatic data and plot-box aspect-ratio modes so the standalone figure can be freely resized. Apps should not implement their own plot-popout behavior unless they need a domain-specific export workflow. +txtMetric = labkit.ui.view.form(grid, 'info', 3, 'Current value:'); +``` -Use `showImageAxes` for app-neutral image display boilerplate: it draws an image, uses image-style axes limits, hides ticks, enables standard image navigation, and refreshes the axes popout menu onto the image object. Apps still own how image arrays, overlays, masks, and annotations are computed. +`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. -Use `createInteractionRuntime` before attaching interactive image tools to an axes. The runtime owns figure/axes callback lifecycle for image tools: app-default scroll behavior, exclusive tool sessions, temporary drag callbacks, hit testing, and restoration when a tool deactivates. Apps with special preview scroll or other axes behavior register those callbacks in the runtime spec instead of setting `WindowScrollWheelFcn`, `WindowButtonMotionFcn`, `WindowButtonUpFcn`, or `ButtonDownFcn` directly. +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 `runWithBusyState` around long synchronous callbacks that should give immediate feedback and prevent repeat button clicks. The helper sets a busy pointer, optionally shows an indeterminate progress dialog, disables the controls supplied in `opts.controls`, runs the callback, then restores the prior control states even if the callback errors. Apps still own which controls are passed in and should refresh any final enable/disable state after the helper returns when a computation changes available actions. +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 `tabSpec(..., struct('resizeRows', ...))` when a left tab contains several stacked app-defined sections that may need manual height adjustment. When manually placing a component directly into an app-shell tab grid, map the logical row through `labkit.ui.layoutRow(parentGrid, row)`. Most app code should use helpers such as `createPanelGrid`, `createResultTablePanel`, `createLogPanel`, and `createAxes`, which apply that mapping for their parent row. `labkit.ui.addRowResizeHandle` remains a lower-level helper for unusual app-local grids that intentionally reserve a physical handle row. +```matlab +fileUi = labkit.ui.view.panel(layFA, 'files', labels, callbacks); +logUi = labkit.ui.view.panel(layLog, 'log', 1, {'Ready.'}); +tableUi = labkit.ui.view.panel(laySR, 'table', 'Batch Results', 2, columns); +``` -Use `createAnchorCurveEditor` when an app or higher-level UI tool needs image anchor editing: double-click blank image space to add or insert anchors, drag anchors to move them, double-click anchors to delete them, switch between curve and straight-line preview, constrain the maximum point count for tools such as two-endpoint reference lines, and optionally install scroll-wheel zoom through the runtime while active. For open paths, new anchors near either endpoint usually extend that endpoint for sequential tracing, while clicks close to an existing visible segment insert correction anchors into the middle. Endpoint extensions that would self-intersect the visible path also become insertions when there is a nearby visible segment. The helper owns generic interaction, runtime sessions, hit testing, and preview graphics. Callers own the higher-level workflow that consumes the edited points. +Use `labkit.ui.view.update` for state changes on existing component handles: -Use `createScaleBarTool` when an image app needs the common scale-bar workflow. The tool owns the fixed controls, unit normalization, typed or two-endpoint reference-pixel calibration, pixels-per-unit readout, final scale-bar placement, black/white overlay drawing, and reference-edit mode state. The default units are `m`, `cm`, `mm`, `um`, and `nm`; the default position is `Bottom right`; the default color is `Black`; the default reference length and scale-bar length are both `1`. +```matlab +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'); +``` -Apps should pass the interaction runtime into the tool, call `setImageSize` after loading a new image, call `setBackground` with the image graphics handle after redrawing, call `renderOverlay` from the app-local image renderer, and read `calibration()` before app-owned measurements. Pass `opts.onTrace` to capture verbose scale-bar state changes and reference-editor lifecycle messages during debug launches. The calibration struct has `referencePixels`, `referenceLength`, `unit`, `pixelsPerUnit`, `isCalibrated`, and `referenceLine`. Apps still own image loading/redrawing, edit-mode coordination, scientific calculations, result summaries, alerts/log wording, exports, and CSV/table schemas. +## Axes And Rendering -`createScaleBarPanel` remains the lower-level reusable control panel for callers that need to own reference drawing or overlay rendering themselves. The returned scale-bar spec includes a two-point `line`, `label`, RGB `color`, `labelPosition`, `verticalAlignment`, `pixelsPerUnit`, `unit`, `barLength`, `position`, and `colorName`. +Use view helpers for app-neutral rendering boilerplate: -### `createInteractionRuntime` Options +```matlab +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'); +``` -| Option | Type | Default | Valid values / meaning | -| --- | --- | --- | --- | -| `figure` | figure handle | `ancestor(ax,'figure')` | Owning figure for scroll and drag callbacks. | -| `defaultScrollFcn` | function handle or empty | `[]` | App-default scroll behavior restored when no scroll-owning tool session is active. | -| `onInteractionChanged` | function handle or empty | `[]` | Called as `callback(active, name)` when a runtime session activates or deactivates. | -| `onTrace` | function handle or empty | `[]` | Called as `callback(message)` for verbose debug messages about default scroll ownership and session lifecycle. | +`draw(..., 'popout')` installs the standard right-click action `Open axes in new figure` and attaches it to axes children such as images and plotted lines. Apps should call it after custom redraws that create new graphics children. -The returned runtime struct exposes `axes`, `figure`, `setDefaultScrollFcn`, `setTraceCallback`, `installDefaultCallbacks`, `createSession`, `isInteractionActive`, and `delete`. App code normally passes the runtime to public tools rather than creating sessions directly. +## Interaction Tools -### `createAnchorCurveEditor` Options +Image apps that need scroll, drag, hit-test, anchor editing, ROI-style drawing, or scale bars should create a runtime: -| Option | Type | Default | Valid values / meaning | -| --- | --- | --- | --- | -| `closed` | logical | `false` | True for closed ROI boundaries. | -| `style` | string | `Curve` | `Curve` or `Straight lines`. | -| `installScrollWheel` | logical | `true` | True temporarily uses editor zoom while active; false preserves runtime default scroll behavior. | -| `maxPoints` | positive integer or `Inf` | `Inf` | Maximum number of anchors. | -| `onChanged` | function handle | `[]` | Called after point edits. | -| `onTrace` | function handle or empty | `[]` | Called as `callback(message)` for verbose debug messages about editor lifecycle and pointer interactions. | +```matlab +runtime = labkit.ui.tool.createRuntime(ax, struct( ... + 'figure', fig, ... + 'defaultScrollFcn', @onPreviewScroll, ... + 'onTrace', debug.trace)); +``` -The returned editor struct exposes `start`, `setActive`, `setPoints`, `getPoints`, `clearPoints`, `undoLast`, `insertPoint`, `setStyle`, `setImageSize`, `setBackground`, `refresh`, `curvePoints`, and `delete`. +The runtime owns exclusive sessions, pointer callbacks, drag capture, scroll ownership, and restoration. Apps should not set `WindowScrollWheelFcn`, `WindowButtonMotionFcn`, `WindowButtonUpFcn`, or image-tool `ButtonDownFcn` directly. -## Callback Lifecycle Policy +Use `labkit.ui.tool.anchorEditor(runtime, imageSize, opts)` for generic anchor editing. Use `labkit.ui.tool.scaleBar(parent, row, runtime, opts)` for calibration controls, reference-pixel editing, unit normalization, final scale-bar placement, and overlay drawing. Apps still own image loading, redraw order, scientific calculations, result summaries, alerts, logs, and exports. -Reusable UI helpers and composed tools must keep three callback classes separate: +`labkit.ui.tool.scaleBarCalibration(referencePixels, referenceLength, unitName, opts)` is the GUI-free calibration struct helper used by apps and app-private calculations. -| Callback class | Purpose | -| --- | --- | -| User semantic callbacks | Notify the app that the user changed app-relevant state. | -| Internal refresh callbacks | Keep controls, graphics, and derived readouts synchronized without re-entering app semantics. | -| Programmatic callbacks | Apply app-initiated state changes and report source as programmatic when exposed through trace. | - -All `setX(value)` style APIs should no-op when the requested value is already current. Internal synchronization should not fire app-facing semantic callbacks. Composed tools should trace callback reason/source as `user`, `internal`, or `programmatic` when the event crosses the app/tool boundary. Tools that own pointer, drag, scroll, or hit testing must acquire that ownership through a `createInteractionRuntime` session and restore callbacks when the session ends. - -## Internal App Hooks +## Diagnostics -Apps may use the shared internal hook helpers for tests and maintenance debug logging. These hooks are not user-facing launch APIs. - -Canonical test calls use: +Apps route internal test/debug launch through: ```matlab -appName("__labkit_test__", "commandName", arg1, arg2, ...) +[handled, outputs, debug] = labkit.ui.app.dispatchRequest( ... + appName, varargin, nargout, handlers); ``` -Apps pass a handler struct array to `labkit.ui.dispatchAppRequest(appName, varargin, nargout, handlers)`. Each handler has `command`, `minArgs`, `maxArgs`, `maxOutputs`, and `run`. The `run` function receives command arguments as a cell array and returns outputs as a cell array. Unsupported commands and invalid requests use app-scoped error IDs such as `:UnknownTestCommand` and `:InvalidTestArguments`. +Debug contexts are created by dispatch for normal app entry points. Apps with nonstandard request paths may call `labkit.ui.diag.createContext(appName, opts)` directly. -Debug calls use either the compatibility hook or a maintainer-friendly debug alias: +Debug launches support: ```matlab [fig, debug] = appName("__labkit_debug__", opts); @@ -247,49 +139,35 @@ Debug calls use either the compatibility hook or a maintainer-friendly debug ali [fig, debug] = appName("--debug", opts); ``` -`opts.logFile` optionally mirrors appended and trace log lines to a text file, `opts.logCallback` optionally receives each captured line, `opts.traceCallback` optionally receives trace lines, and `opts.traceEnabled` controls verbose trace logging. App-local `addLog` functions should append to the visible UI log and then call `debug.append(message)`. Apps that want visible verbose debug output call `debug.attachTextLog(txtLog)` after creating their Log tab text area, emit a startup trace line, pass `debug.trace` into reusable image-interaction tools through `onTrace`, and call `debug.instrumentFigure(fig)` after controls are built to trace common component callbacks. Trace lines include timestamp plus stable `app=...`, `component=...`, `event=...`, and `reason=...` fields. One-argument `debug.trace(message)` calls use `component=app` and `reason=internal`; composed tools may call `debug.trace(component, event, reason)` with reason/source such as `user`, `internal`, or `programmatic`. The default instrumentation intentionally skips low-level pointer, drag, and scroll callbacks so reading or scrolling the Log tab does not generate more log lines; callers can pass `callbackProperties` explicitly for a narrow low-level trace. Normal `appName()` launches receive a disabled debug log internally and keep existing behavior. +App-local `addLog` functions should append to the visible UI log with `labkit.ui.view.update(txtLog, 'appendLog', message)` and then call `debug.append(message)`. Debug-mode apps attach the Log tab text area, emit a startup trace line, pass `debug.trace` into reusable tools through `onTrace`, and call `debug.instrumentFigure(fig)` after controls are built. -## Ownership Boundary +Trace lines include timestamp plus stable `app=...`, `component=...`, `event=...`, and `reason=...` fields. Default instrumentation skips low-level pointer, drag, and scroll callbacks. + +## Callback Policy -`labkit.ui.*` may provide: +Reusable helpers and tools keep three callback classes separate: -- app shell creation -- tab specification helpers -- file-selection panels -- log panels and log append helpers -- internal app test/debug hook dispatch and visible trace diagnostics -- panel/grid construction -- row-resize handles for stacked app-defined sections -- interaction runtime ownership for default scroll, exclusive tool modes, drag callbacks, and hit testing -- anchor-curve editing on image axes -- image scale-bar calibration, reference editing, and overlay placement -- plot axes creation, reset, prepared-X/Y plotting, and app-neutral axes popout -- app-neutral image display boilerplate for prepared image arrays -- result table panels -- listbox selection refresh -- busy-state feedback for long synchronous callbacks -- small labeled controls, read-only text surfaces, and domain-neutral state helpers +| Callback class | Purpose | +| --- | --- | +| User semantic callbacks | Notify the app that the user changed app-relevant state. | +| Internal refresh callbacks | Keep controls, graphics, and derived readouts synchronized without re-entering app semantics. | +| Programmatic callbacks | Apply app-initiated state changes and report source as programmatic when exposed through trace. | -`labkit.ui.*` should not own: +All `setX(value)` style APIs should no-op when the requested value is already current. Internal synchronization should not fire app-facing semantic callbacks. Composed tools should trace callback reason/source as `user`, `internal`, or `programmatic` when the event crosses the app/tool boundary. -- experiment names -- formulas, thresholds, or analysis definitions -- parser calls or file-format assumptions -- result field definitions -- export schemas -- app-specific callback choreography +## Ownership Boundary -Apps pass labels, callbacks, prepared vectors, table data, and option values into the GUI helpers. Reusable GUI helpers exist to remove MATLAB UI boilerplate, not to hide the domain workflow. +`labkit.ui` may provide app-neutral GUI shell, view construction, axes rendering, interaction lifecycle, composed tools, diagnostics, and reusable control state mechanics. -Do not add a UI helper only because one app got large. Extract UI code when the behavior is generic, stable across real apps, and easier to test as a domain-neutral helper than as app-local code. +`labkit.ui` should not own experiment names, formulas, thresholds, parser calls, result fields, export schemas, plotting annotations, or app-specific callback choreography. Apps pass labels, callbacks, prepared vectors, tables, debug contexts, and option values into UI helpers. -## UI Validation +## Validation -Reusable UI helper and shell contracts are covered by the `labkit/ui` suite. Add `--gui` when checking noninteractive layout and callback wiring: +Reusable UI contracts are covered by: ```bash scripts/run_matlab_tests.sh --suite labkit/ui --gui -scripts/run_matlab_tests.sh --suite gui +scripts/run_matlab_tests.sh --suite project ``` -These checks require MATLAB graphics/uifigure support and are not part of the default GitHub Actions job. The CI job runs the non-GUI suite; final interactive behavior is validated manually in the app windows. +Automated GUI tests validate launch, layout, callback wiring, and trace plumbing. Full interactive drawing, file selection, visual inspection, and workflow feel still require manual MATLAB GUI validation. diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 60d4a42..d4b47e0 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -15,7 +15,7 @@ Tests mirror source ownership. Do not create a parallel runner framework unless - Use `tests/helpers/` only for setup, lookup, assertion, cleanup, and fixture-building helpers. - Do not move app-specific formulas, expected scientific values, result schemas, or export columns into shared test helpers. - Boundary tests may require app-owned logic to stay under the owning app tree, but should not require GUI-free helpers to remain inside the public app entry-point file or assert exact app-private helper file lists. -- UI public-surface tests should distinguish stable public API, deprecated compatibility API, and private implementation candidates instead of treating every top-level helper as equally recommended. +- UI public-surface tests should assert the layered `labkit.ui.app/view/tool/diag` facade and keep low-level controls, row resize, panel internals, and popout implementation private. - GUI smoke/debug tests may assert that every app supports debug launch and visible startup trace, but should not claim full interactive workflow validation. - When one test file grows too broad, add new focused `test_*.m` files instead of appending unrelated coverage. - GUI tests are structural launch/layout/callback checks; do not claim full interactive workflow validation from automated GUI tests. diff --git a/tests/helpers/architectureTestHelpers.m b/tests/helpers/architectureTestHelpers.m index fb9e796..355ff54 100644 --- a/tests/helpers/architectureTestHelpers.m +++ b/tests/helpers/architectureTestHelpers.m @@ -50,20 +50,40 @@ [appName ' should not call internal analysis APIs directly.']); assert(~contains(appSource, 'labkit.util.'), ... [appName ' should not call utility APIs directly.']); - assert(contains(appSource, 'labkit.ui.createAppShell'), ... - [appName ' should build its GUI from the app-facing shell helper.']); + assert(contains(appSource, 'labkit.ui.app.createShell'), ... + [appName ' should build its GUI from the layered app shell facade.']); + assert(~contains(appSource, 'labkit.ui.create'), ... + [appName ' should not call removed flat UI create* helpers.']); + assert(~contains(appSource, 'labkit.ui.appendLog'), ... + [appName ' should not call removed flat UI log helpers.']); + assert(~contains(appSource, 'labkit.ui.tabSpec'), ... + [appName ' should not call removed flat UI tab helpers.']); + assert(~contains(appSource, 'labkit.ui.layoutRow'), ... + [appName ' should not call removed flat UI layout helpers.']); + assert(~contains(appSource, 'labkit.ui.runWithBusyState'), ... + [appName ' should not call removed flat UI busy-state helpers.']); assert(~contains(appSource, 'labkit.ui.createWorkbench'), ... - [appName ' should not call deprecated UI shell helpers.']); + [appName ' should not call removed flat UI shell helpers.']); assert(~contains(appSource, 'labkit.ui.handleAppRequest'), ... - [appName ' should not call deprecated app request helpers.']); + [appName ' should not call removed flat UI request helpers.']); assert(~contains(appSource, 'labkit.ui.createAppDebugLog'), ... - [appName ' should not call deprecated debug helpers.']); + [appName ' should not call removed flat UI debug helpers.']); assert(~contains(appSource, 'labkit.ui.createImageAxesRuntime'), ... - [appName ' should not call deprecated image runtime helpers.']); + [appName ' should not call removed flat UI runtime helpers.']); assert(~contains(appSource, 'labkit.ui.createStandardWorkbenchShell'), ... [appName ' should not use compatibility shell wrappers directly.']); assert(~contains(appSource, 'labkit.ui.createTabbedDualPlotShell'), ... [appName ' should not use compatibility shell wrappers directly.']); + forbiddenViewHelpers = {'appendLog', 'clearAxes', 'enablePopout', ... + 'fileSelectionPanel', 'logPanel', 'plotOptionsPanel', 'plotXY', ... + 'refreshListboxItems', 'refreshListboxSelection', 'resetAxes', ... + 'resultTable', 'setTopBottomPlotSelections', 'showImage', ... + 'swapTopBottomPlotSelections', 'textPanel', 'topBottomPlotControls'}; + for iHelper = 1:numel(forbiddenViewHelpers) + oldViewCall = ['labkit.ui.view.' forbiddenViewHelpers{iHelper}]; + assert(~contains(appSource, oldViewCall), ... + [appName ' should use the unified view panel/draw/update facade instead of ' oldViewCall '.']); + end source = readAppOwnedSource(appFile); assert(~contains(source, 'labkit.io.'), ... @@ -108,7 +128,7 @@ function assertDTAFacadeUsage(source, appName, expectedKind, expectsFolderDiscov function assertDICAppBoundary(source, appName) assert(~contains(source, 'labkit.dta.'), ... [appName ' should not use the electrochemistry DTA facade.']); - assert(contains(source, 'labkit.ui.createAppShell'), ... + assert(contains(source, 'labkit.ui.app.createShell'), ... [appName ' should build from the reusable GUI foundation.']); assert(~contains(source, '+labkit/+dic'), ... [appName ' should keep DIC workflow code app-local.']); @@ -118,7 +138,7 @@ function assertDICAppBoundary(source, appName) function assertImageMeasurementAppBoundary(source, appName) assert(~contains(source, 'labkit.dta.'), ... [appName ' should not use the electrochemistry DTA facade.']); - assert(contains(source, 'labkit.ui.createAppShell'), ... + assert(contains(source, 'labkit.ui.app.createShell'), ... [appName ' should build from the reusable GUI foundation.']); assert(~contains(source, '+labkit/+dic'), ... [appName ' should not depend on DIC implementation packages.']); @@ -130,7 +150,7 @@ function assertImageMeasurementAppBoundary(source, appName) function assertWearableAppBoundary(source, appName) assert(~contains(source, 'labkit.dta.'), ... [appName ' should not use the electrochemistry DTA facade.']); - assert(contains(source, 'labkit.ui.createAppShell'), ... + assert(contains(source, 'labkit.ui.app.createShell'), ... [appName ' should build from the reusable GUI foundation.']); assert(contains(source, 'labkit.biosignal.'), ... [appName ' should use the GUI-free biosignal facade for signal operations.']); @@ -181,7 +201,7 @@ function assertPackageSourcesDoNotContain(packageDir, forbiddenWords, label) function assertAppUsesManagedImageInteractions(source, appName) assert(~contains(source, 'WindowScrollWheelFcn'), ... - [appName ' should register image scroll behavior through labkit.ui.createInteractionRuntime.']); + [appName ' should register image scroll behavior through labkit.ui.tool.createRuntime.']); assert(~contains(source, 'WindowButtonMotionFcn') && ~contains(source, 'WindowButtonUpFcn'), ... [appName ' should not own image-tool drag callbacks directly.']); assert(~contains(source, '.ButtonDownFcn'), ... diff --git a/tests/suites/labkit/ui/test_appHookHelpers.m b/tests/suites/labkit/ui/test_appHookHelpers.m index e35c5a9..52c25a2 100644 --- a/tests/suites/labkit/ui/test_appHookHelpers.m +++ b/tests/suites/labkit/ui/test_appHookHelpers.m @@ -12,7 +12,7 @@ function checkDebugLog() cleaner = onCleanup(@() cleanupFile(logFile)); %#ok callbackLines = {}; traceLines = {}; - debug = labkit.ui.createDebugContext('probe_app', ... + debug = labkit.ui.diag.createContext('probe_app', ... struct('logFile', logFile, ... 'logCallback', @captureLine, ... 'traceCallback', @captureTraceLine)); @@ -42,12 +42,12 @@ function checkDebugLog() contains(fileText, 'event=details'), ... 'Debug log should mirror appended and trace messages to the log file.'); - disabled = labkit.ui.createDebugContext('probe_app', struct('enabled', false)); + disabled = labkit.ui.diag.createContext('probe_app', struct('enabled', false)); disabled.append('ignored'); disabled.trace('ignored trace'); assert(isempty(disabled.getLog()), 'Disabled debug logs should ignore appended messages.'); - appendOnly = labkit.ui.createDebugContext('probe_app', struct('traceEnabled', false)); + appendOnly = labkit.ui.diag.createContext('probe_app', struct('traceEnabled', false)); appendOnly.append('kept'); appendOnly.trace('hidden'); appendOnlyLines = appendOnly.getLog(); @@ -66,7 +66,7 @@ function captureTraceLine(line) function checkCallbackWrapper() callbackCalls = 0; - debug = labkit.ui.createDebugContext('probe_app', struct()); + debug = labkit.ui.diag.createContext('probe_app', struct()); wrapped = debug.wrapCallback('sample callback', @sampleCallback); wrapped('source', 'event'); @@ -100,26 +100,26 @@ function checkRequestDispatch() 'maxOutputs', {2}, ... 'run', {@runEcho}); - [handled, outputs, debug] = labkit.ui.dispatchAppRequest('probe_app', {}, 0, handlers); + [handled, outputs, debug] = labkit.ui.app.dispatchRequest('probe_app', {}, 0, handlers); assert(~handled && isempty(outputs) && ~debug.enabled, ... 'Empty app input should not be handled and should return a disabled debug log.'); - [handled, outputs] = labkit.ui.dispatchAppRequest( ... + [handled, outputs] = labkit.ui.app.dispatchRequest( ... 'probe_app', {'__labkit_test__', 'echo', 'one', 'two'}, 2, handlers); assert(handled && isequal(outputs, {'one', 'two'}), ... 'Test hook dispatch should return requested handler outputs.'); - [handled, outputs, debug] = labkit.ui.dispatchAppRequest( ... + [handled, outputs, debug] = labkit.ui.app.dispatchRequest( ... 'probe_app', {'__labkit_debug__', struct()}, 2, handlers); assert(~handled && isempty(outputs) && debug.enabled && debug.traceEnabled, ... 'Debug hook dispatch should enable debug logging without consuming app launch.'); - [handled, outputs, debug] = labkit.ui.dispatchAppRequest( ... + [handled, outputs, debug] = labkit.ui.app.dispatchRequest( ... 'probe_app', {'debug'}, 2, handlers); assert(~handled && isempty(outputs) && debug.enabled && debug.traceEnabled, ... 'Debug launch dispatch should accept the user-facing debug alias.'); - [handled, outputs, debug] = labkit.ui.dispatchAppRequest( ... + [handled, outputs, debug] = labkit.ui.app.dispatchRequest( ... 'probe_app', {'--debug', struct('traceEnabled', false)}, 2, handlers); assert(~handled && isempty(outputs) && debug.enabled && ~debug.traceEnabled, ... 'Debug launch dispatch should preserve explicit traceEnabled=false.'); @@ -133,21 +133,21 @@ function checkRequestErrors() 'maxOutputs', {1}, ... 'run', {@runEcho}); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {42}, 0, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {42}, 0, handlers), ... 'probe_app:UnsupportedInput', 'Nonstrings should be unsupported app input.'); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {'__labkit_test__'}, 0, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {'__labkit_test__'}, 0, handlers), ... 'probe_app:InvalidTestRequest', 'Test hooks require a command name.'); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {'__labkit_test__', 'missing'}, 0, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {'__labkit_test__', 'missing'}, 0, handlers), ... 'probe_app:UnknownTestCommand', 'Unknown test commands should fail with the canonical id.'); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {'__labkit_test__', 'echo'}, 0, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {'__labkit_test__', 'echo'}, 0, handlers), ... 'probe_app:InvalidTestArguments', 'Invalid test argument counts should fail with the canonical id.'); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {'__labkit_test__', 'echo', 'one'}, 2, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {'__labkit_test__', 'echo', 'one'}, 2, handlers), ... 'probe_app:TooManyOutputs', 'Too many requested outputs should fail with the canonical id.'); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {'debug'}, 3, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {'debug'}, 3, handlers), ... 'probe_app:TooManyOutputs', 'Too many debug outputs should fail with the canonical id.'); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {'debug', 42}, 0, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {'debug', 42}, 0, handlers), ... 'probe_app:InvalidTestRequest', 'Debug options should be a struct.'); - assertThrows(@() labkit.ui.dispatchAppRequest('probe_app', {'debug', struct(), struct()}, 0, handlers), ... + assertThrows(@() labkit.ui.app.dispatchRequest('probe_app', {'debug', struct(), struct()}, 0, handlers), ... 'probe_app:InvalidTestRequest', 'Debug requests should accept at most one options struct.'); end diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_anchor_curve_editor.m b/tests/suites/labkit/ui/test_gui_layout_ui_anchor_curve_editor.m index 030e2a8..31670cf 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_anchor_curve_editor.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_anchor_curve_editor.m @@ -10,10 +10,10 @@ function test_gui_layout_ui_anchor_curve_editor() ax = uiaxes(fig); image(ax, zeros(40, 60, 3, 'uint8')); axis(ax, 'image'); - runtime = labkit.ui.createInteractionRuntime(ax, struct('figure', fig)); + runtime = labkit.ui.tool.createRuntime(ax, struct('figure', fig)); changed = false; - editor = labkit.ui.createAnchorCurveEditor(runtime, [40 60 3], ... + editor = labkit.ui.tool.anchorEditor(runtime, [40 60 3], ... struct('closed', true, ... 'style', 'Curve', ... 'onChanged', @(~,~) markChanged())); @@ -33,27 +33,27 @@ function test_gui_layout_ui_anchor_curve_editor() points = editor.getPoints(); assert(isequal(size(points), [5 2]) && isequal(points(2, :), [25 10]), ... 'Anchor curve editor should insert new anchors into the nearest displayed curve segment.'); - twoPointEditor = labkit.ui.createAnchorCurveEditor(runtime, [40 60 3], ... + twoPointEditor = labkit.ui.tool.anchorEditor(runtime, [40 60 3], ... struct('closed', false, 'style', 'Straight lines', 'maxPoints', 2)); twoPointEditor.start([5 5; 20 5]); twoPointEditor.insertPoint([30 5]); assert(isequal(size(twoPointEditor.getPoints()), [2 2]), ... 'Anchor curve editor should enforce maxPoints for two-anchor tools.'); - openEditor = labkit.ui.createAnchorCurveEditor(runtime, [40 60 3], ... + openEditor = labkit.ui.tool.anchorEditor(runtime, [40 60 3], ... struct('closed', false, 'style', 'Straight lines')); openEditor.start([10 10; 40 30]); openEditor.insertPoint([25 20]); points = openEditor.getPoints(); assert(isequal(points(2, :), [25 20]), ... 'Open anchor editor should insert points that are close to an existing segment.'); - spiralEditor = labkit.ui.createAnchorCurveEditor(runtime, [60 70 3], ... + spiralEditor = labkit.ui.tool.anchorEditor(runtime, [60 70 3], ... struct('closed', false, 'style', 'Straight lines')); spiralEditor.start([20 20; 55 20; 55 55; 35 55; 35 35; 48 35]); spiralEditor.insertPoint([48 45]); points = spiralEditor.getPoints(); assert(isequal(points(end, :), [48 45]), ... 'Open anchor editor should extend nearby endpoints instead of inserting into an inner spiral segment.'); - crossingEditor = labkit.ui.createAnchorCurveEditor(runtime, [100 120 3], ... + crossingEditor = labkit.ui.tool.anchorEditor(runtime, [100 120 3], ... struct('closed', false, 'style', 'Straight lines')); crossingEditor.start([10 10; 60 10; 60 80; 80 80; 80 70]); ax.XLim = [0.5 500.5]; @@ -74,8 +74,8 @@ function test_gui_layout_ui_anchor_curve_editor() axis(ax2, 'image'); baseScroll2 = @(~,~) setappdata(fig2, 'baseScrollCalled', true); fig2.WindowScrollWheelFcn = baseScroll2; - runtime2 = labkit.ui.createInteractionRuntime(ax2, struct('figure', fig2)); - scrollEditor = labkit.ui.createAnchorCurveEditor(runtime2, [40 60 3], ... + runtime2 = labkit.ui.tool.createRuntime(ax2, struct('figure', fig2)); + scrollEditor = labkit.ui.tool.anchorEditor(runtime2, [40 60 3], ... struct('closed', false, 'style', 'Straight lines')); scrollEditor.start([5 5; 20 20]); assert(~isempty(fig2.WindowScrollWheelFcn), ... @@ -96,10 +96,10 @@ function test_gui_layout_ui_anchor_curve_editor() axis(ax3, 'image'); baseScroll3 = @(~,~) setappdata(fig3, 'baseScrollCalled', true); fig3.WindowScrollWheelFcn = baseScroll3; - runtime3 = labkit.ui.createInteractionRuntime(ax3, struct('figure', fig3)); - firstEditor = labkit.ui.createAnchorCurveEditor(runtime3, [40 60 3], ... + runtime3 = labkit.ui.tool.createRuntime(ax3, struct('figure', fig3)); + firstEditor = labkit.ui.tool.anchorEditor(runtime3, [40 60 3], ... struct('closed', false, 'style', 'Straight lines')); - secondEditor = labkit.ui.createAnchorCurveEditor(runtime3, [40 60 3], ... + secondEditor = labkit.ui.tool.anchorEditor(runtime3, [40 60 3], ... struct('closed', false, 'style', 'Straight lines')); firstEditor.start([5 5; 20 20]); secondEditor.start([8 8; 24 24]); diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_axes_workbench.m b/tests/suites/labkit/ui/test_gui_layout_ui_axes_workbench.m index 978944d..9079948 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_axes_workbench.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_axes_workbench.m @@ -6,7 +6,6 @@ function test_gui_layout_ui_axes_workbench() cleanup = onCleanup(@() h.closeAllFigures()); %#ok checkCreateAxesHelper(h); - checkRowResizeHandleHelper(h); checkCreateAppShellHelper(h); checkTopBottomPlotControlsHelper(h); checkTopBottomPlotStateHelpers(h); @@ -17,16 +16,21 @@ function checkCreateAxesHelper(h) cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 1]); - ax = labkit.ui.createAxes(grid, 2, 'Probe Title', 'Probe X', 'Probe Y'); + ax = labkit.ui.view.axes(grid, 2, 'Probe Title', 'Probe X', 'Probe Y'); plot(ax, 1:3, [1 4 2], 'DisplayName', 'probe'); - labkit.ui.enableAxesPopout(ax); + labkit.ui.view.draw(ax, 'popout'); assert(ax.Layout.Row == 2, 'Axes helper should set the requested layout row.'); assert(strcmp(char(ax.Title.String), 'Probe Title'), 'Axes helper should preserve the title.'); assert(strcmp(char(ax.XLabel.String), 'Probe X'), 'Axes helper should preserve the x label.'); assert(strcmp(char(ax.YLabel.String), 'Probe Y'), 'Axes helper should preserve the y label.'); h.assertAxesPopoutEnabled(ax, 'Axes helper should install the LabKit popout context action.'); - popoutFig = labkit.ui.popoutAxes(ax); + menuItem = findall(ax.ContextMenu, 'Type', 'uimenu', 'Tag', 'labkitAxesPopoutMenu'); + h.invokeCallback(menuItem, 'MenuSelectedFcn'); + drawnow; + popoutFig = findall(groot, 'Type', 'figure', 'Name', 'Probe Title'); + assert(~isempty(popoutFig), 'Axes popout menu should create a standalone figure.'); + popoutFig = popoutFig(1); popoutCleaner = onCleanup(@() delete(popoutFig)); %#ok popoutAxes = findobj(popoutFig, 'Type', 'axes'); assert(numel(popoutAxes) >= 1, 'Axes popout should create an editable figure axes.'); @@ -40,46 +44,21 @@ function checkCreateAxesHelper(h) h.assertAxesChildrenUsePopoutMenu(ax, ... 'Axes helper should attach the popout menu to plotted child objects.'); - imgAx = labkit.ui.createAxes(grid, 1, 'Image Probe', '', ''); - hImage = labkit.ui.showImageAxes(imgAx, zeros(12, 16, 3, 'uint8'), 'Image Probe'); + imgAx = labkit.ui.view.axes(grid, 1, 'Image Probe', '', ''); + hImage = labkit.ui.view.draw(imgAx, 'image', zeros(12, 16, 3, 'uint8'), 'Image Probe'); assert(strcmp(char(imgAx.Title.String), 'Image Probe'), ... 'Image axes helper should preserve the supplied title.'); assert(isequal(hImage.ContextMenu, imgAx.ContextMenu), ... 'Image axes helper should attach the popout menu to the image object.'); end -function checkRowResizeHandleHelper(h) - fig = uifigure('Visible', 'off', 'Name', 'labkit_row_resize_probe'); - cleaner = onCleanup(@() delete(fig)); %#ok - grid = uigridlayout(fig, [3 1]); - grid.RowHeight = {120, 6, 140}; - - top = uipanel(grid, 'Title', 'Top'); - top.Layout.Row = 1; - bottom = uipanel(grid, 'Title', 'Bottom'); - bottom.Layout.Row = 3; - - handle = labkit.ui.addRowResizeHandle(fig, grid, 2, ... - struct('minTopHeight', 80, 'minBottomHeight', 90)); - assert(handle.Layout.Row == 2, 'Row-resize handle should live in the requested row.'); - assert(isequal(grid.RowHeight, {120, 6, 140}), ... - 'Row-resize handle should preserve existing adjacent row heights before dragging.'); - h.assertCallbackPresent(handle, 'ButtonDownFcn', 'row-resize handle'); - h.invokeCallback(handle, 'ButtonDownFcn'); - assert(~isempty(fig.WindowButtonMotionFcn) && ~isempty(fig.WindowButtonUpFcn), ... - 'Row-resize drag should install temporary figure motion callbacks.'); - h.invokeCallback(fig, 'WindowButtonUpFcn'); - assert(isempty(fig.WindowButtonMotionFcn) && isempty(fig.WindowButtonUpFcn), ... - 'Row-resize drag should clear temporary figure callbacks after release.'); -end - function checkCreateAppShellHelper(h) opts = struct(); opts.rightTitle = 'Preview'; opts.rightGridSize = [2 1]; opts.rightRowHeight = {'1x', 'fit'}; opts.rightRowSpacing = 7; - ui = labkit.ui.createAppShell(struct( ... + ui = labkit.ui.app.createShell(struct( ... 'title', 'labkit_create_app_shell_probe', ... 'position', [40 30 1200 760], ... 'leftWidth', 330, ... @@ -97,6 +76,14 @@ function checkCreateAppShellHelper(h) 'Standard Summary + Results tab should expose one row-resize handle.'); assert(isequal(ui.filesAnalysisGrid.UserData.LabKitLogicalRowMap, [1 3 5]), ... 'App shell should keep standard app rows mapped behind the shell.'); + resizeHandle = ui.filesAnalysisResizeHandles(1); + h.assertCallbackPresent(resizeHandle, 'ButtonDownFcn', 'row-resize handle'); + h.invokeCallback(resizeHandle, 'ButtonDownFcn'); + assert(~isempty(ui.fig.WindowButtonMotionFcn) && ~isempty(ui.fig.WindowButtonUpFcn), ... + 'Shell row-resize drag should install temporary figure motion callbacks.'); + h.invokeCallback(ui.fig, 'WindowButtonUpFcn'); + assert(isempty(ui.fig.WindowButtonMotionFcn) && isempty(ui.fig.WindowButtonUpFcn), ... + 'Shell row-resize drag should clear temporary figure callbacks after release.'); assert(strcmp(ui.rightPanel.Title, 'Preview'), ... 'App shell helper should preserve the requested right panel title.'); assert(isequal(ui.rightGrid.RowHeight, {'1x', 'fit'}), ... @@ -104,9 +91,9 @@ function checkCreateAppShellHelper(h) customOpts = struct(); customOpts.rightTitle = 'Custom'; - customOpts.tabs = labkit.ui.tabSpec( ... + customOpts.tabs = labkit.ui.app.tab( ... 'probe', 'Probe Controls', [2 1], {'fit', '1x'}); - custom = labkit.ui.createAppShell(struct( ... + custom = labkit.ui.app.createShell(struct( ... 'title', 'labkit_custom_tab_app_shell_probe', ... 'position', [40 30 1200 760], ... 'leftWidth', 330, ... @@ -120,7 +107,7 @@ function checkCreateAppShellHelper(h) assert(isempty(custom.probeResizeHandles), ... 'Custom tabs without resizeRows should not create resize handles.'); - dual = labkit.ui.createAppShell(struct( ... + dual = labkit.ui.app.createShell(struct( ... 'title', 'labkit_create_dual_plot_app_shell_probe', ... 'position', [40 30 1200 760], ... 'leftWidth', 330, ... @@ -133,7 +120,7 @@ function checkCreateAppShellHelper(h) 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.createAppShell(struct( ... + dualNoControls = labkit.ui.app.createShell(struct( ... 'title', 'labkit_create_dual_plot_no_controls_probe', ... 'position', [40 30 1200 760], ... 'leftWidth', 330, ... @@ -153,8 +140,9 @@ function checkTopBottomPlotControlsHelper(h) 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.createTopBottomPlotControls( ... + ui = labkit.ui.view.panel( ... shell.topControlsPanel, ... + 'topBottomPlotControls', ... shell.bottomControlsPanel, ... {'Time (s)', 'Sample #'}, ... {'VT: Vf vs time', 'IT: Im vs time'}, ... @@ -192,8 +180,9 @@ function checkTopBottomPlotStateHelpers(h) 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.createTopBottomPlotControls( ... + ui = labkit.ui.view.panel( ... shell.topControlsPanel, ... + 'topBottomPlotControls', ... shell.bottomControlsPanel, ... {'Time (s)', 'Sample #'}, ... {'VT: Vf vs time', 'IT: Im vs time'}, ... @@ -201,14 +190,13 @@ function checkTopBottomPlotStateHelpers(h) bottomDefaults, ... []); - labkit.ui.setTopBottomPlotSelections(ui.topX, ui.topY, ui.bottomX, ui.bottomY, ... - bottomDefaults, topDefaults); + 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.swapTopBottomPlotSelections(ui.topX, ui.topY, ui.bottomX, ui.bottomY); + 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'), ... @@ -216,8 +204,8 @@ function checkTopBottomPlotStateHelpers(h) shell.topAxes.XScale = 'log'; shell.bottomAxes.YScale = 'log'; - labkit.ui.hardResetAxis(shell.topAxes, 'Top Plot', true); - labkit.ui.hardResetAxis(shell.bottomAxes, 'Bottom Plot', true); + 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'), ... @@ -235,7 +223,7 @@ function checkTopBottomPlotStateHelpers(h) opts.rightTitle = 'Plots'; opts.topPlotTitle = 'Top Plot'; opts.bottomPlotTitle = 'Bottom Plot'; - shell = labkit.ui.createAppShell(struct( ... + shell = labkit.ui.app.createShell(struct( ... 'title', figName, ... 'position', [40 30 1680 980], ... 'leftWidth', 430, ... diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_basic_controls.m b/tests/suites/labkit/ui/test_gui_layout_ui_basic_controls.m index 83c3374..1472b2b 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_basic_controls.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_basic_controls.m @@ -22,20 +22,20 @@ function checkListboxItemsRefreshHelper(h) cleaner = onCleanup(@() delete(fig)); %#ok lb = uilistbox(fig, 'Items', {}, 'Multiselect', 'on'); - labkit.ui.refreshListboxItems(lb, {'a.DTA', 'b.DTA'}); + labkit.ui.view.update(lb, 'listItems', {'a.DTA', 'b.DTA'}); assert(h.sameStringCell(lb.Items, {'a.DTA', 'b.DTA'}), ... 'File listbox helper should populate item display names.'); assert(h.sameStringCell(lb.Value, {'a.DTA', 'b.DTA'}), ... 'File listbox helper should select all items when there is no prior selection.'); lb.Value = {'b.DTA'}; - labkit.ui.refreshListboxItems(lb, {'b.DTA', 'c.DTA'}); + labkit.ui.view.update(lb, 'listItems', {'b.DTA', 'c.DTA'}); assert(h.sameStringCell(lb.Items, {'b.DTA', 'c.DTA'}), ... 'File listbox helper should update item display names.'); assert(h.sameStringCell(lb.Value, {'b.DTA'}), ... 'File listbox helper should preserve valid prior selections.'); - labkit.ui.refreshListboxItems(lb, {}); + labkit.ui.view.update(lb, 'listItems', {}); assert(isempty(lb.Items) && isempty(lb.Value), ... 'File listbox helper should clear listbox items and values for empty sessions.'); end @@ -46,22 +46,23 @@ function checkListboxSelectionHelper(h) grid = uigridlayout(fig, [2 1]); singleList = uilistbox(grid, 'Items', {}, 'Multiselect', 'off'); - [value, idx] = labkit.ui.refreshListboxSelection(singleList, {'a.DTA', 'b.DTA'}, []); + [value, idx] = labkit.ui.view.update(singleList, 'listSelection', {'a.DTA', 'b.DTA'}, []); assert(strcmp(value, 'a.DTA') && idx == 1, ... 'Listbox selection helper should select the first single-select item by default.'); - [value, idx] = labkit.ui.refreshListboxSelection(singleList, {'b.DTA', 'c.DTA'}, 2); + [value, idx] = labkit.ui.view.update(singleList, 'listSelection', {'b.DTA', 'c.DTA'}, 2); assert(strcmp(value, 'c.DTA') && idx == 2, ... 'Listbox selection helper should accept a preferred single-select index.'); multiList = uilistbox(grid, 'Items', {}, 'Multiselect', 'on'); - [value, idx] = labkit.ui.refreshListboxSelection( ... - multiList, {'x.DTA', 'y.DTA'}, {}, struct('defaultSelection', 'all')); + [value, idx] = labkit.ui.view.update( ... + multiList, 'listSelection', {'x.DTA', 'y.DTA'}, {}, ... + struct('defaultSelection', 'all')); assert(h.sameStringCell(value, {'x.DTA', 'y.DTA'}) && isequal(idx, [1 2]), ... 'Listbox selection helper should support selecting all multi-select items by default.'); - [value, idx] = labkit.ui.refreshListboxSelection( ... - multiList, {'y.DTA', 'z.DTA'}, {'y.DTA', 'missing.DTA'}, ... + [value, idx] = labkit.ui.view.update( ... + multiList, 'listSelection', {'y.DTA', 'z.DTA'}, {'y.DTA', 'missing.DTA'}, ... struct('defaultSelection', 'all')); assert(h.sameStringCell(value, {'y.DTA'}) && isequal(idx, 1), ... 'Listbox selection helper should preserve only valid multi-select choices.'); @@ -72,7 +73,7 @@ function checkLogPanelHelper(h) cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 1]); - ui = labkit.ui.createLogPanel(grid, 2, {'Started.'}); + ui = labkit.ui.view.panel(grid, 'log', 2, {'Started.'}); assert(strcmp(ui.panel.Title, 'Log'), 'Log panel helper should preserve the panel title.'); assert(ui.panel.Layout.Row == 2, 'Log panel helper should place the panel in the requested row.'); assert(isequal(ui.grid.Padding, [8 8 8 8]), 'Log panel helper should preserve grid padding.'); @@ -86,7 +87,7 @@ function checkLabeledSpinnerHelper() cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [1 2]); - [lbl, spinner] = labkit.ui.createLabeledSpinner(grid, 'Probe value:', ... + [lbl, spinner] = labkit.ui.view.form(grid, 'spinner', 'Probe value:', ... 'Value', 2, 'Limits', [0 10], 'Step', 0.5); assert(strcmp(lbl.Text, 'Probe value:'), ... 'Labeled spinner helper should preserve label text.'); @@ -101,12 +102,12 @@ function checkReadOnlyTextHelpers(h) cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 1]); - field = labkit.ui.createReadOnlyTextField(grid, 'Value', 'Status'); + field = labkit.ui.view.form(grid, '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.'); - panelUi = labkit.ui.createReadOnlyTextPanel(grid, 'Notes', 2, {'one', 'two'}); + panelUi = labkit.ui.view.panel(grid, 'text', 'Notes', 2, {'one', 'two'}); assert(strcmp(panelUi.panel.Title, 'Notes'), ... 'Read-only text panel helper should preserve the panel title.'); assert(strcmp(panelUi.textArea.Editable, 'off') && ... @@ -119,7 +120,7 @@ function checkReadOnlyInfoRowHelper() cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 2]); - [field, lbl] = labkit.ui.createReadOnlyInfoRow(grid, 2, 'Probe:'); + [field, lbl] = labkit.ui.view.form(grid, 'info', 2, '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.'); @@ -138,7 +139,7 @@ function checkResultTablePanelHelper(h) cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 1]); - ui = labkit.ui.createResultTablePanel(grid, 'Batch Results', 2, ... + ui = labkit.ui.view.panel(grid, 'table', 'Batch Results', 2, ... {'File', 'Value'}, cell(0, 2)); assert(strcmp(ui.panel.Title, 'Batch Results'), ... 'Result table panel helper should preserve the panel title.'); @@ -157,7 +158,7 @@ function checkPanelGridHelper(h) cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 1]); - ui = labkit.ui.createPanelGrid(grid, 'Probe Panel', 2, [3 2]); + ui = labkit.ui.view.section(grid, 'Probe Panel', 2, [3 2]); assert(strcmp(ui.panel.Title, 'Probe Panel'), ... 'Panel-grid helper should preserve the requested panel title.'); assert(ui.panel.Layout.Row == 2, ... @@ -170,7 +171,7 @@ function checkPanelGridHelper(h) 'Panel-grid helper should preserve standard padding.'); opts = struct('columnWidth', {{'1x', '1x'}}, 'padding', [0 0 0 0]); - ui2 = labkit.ui.createPanelGrid(grid, 'Actions', 1, [2 2], opts); + ui2 = labkit.ui.view.section(grid, 'Actions', 1, [2 2], opts); assert(h.sameStringCell(ui2.grid.ColumnWidth, {'1x', '1x'}), ... 'Panel-grid helper should support explicit action-column widths.'); assert(isequal(ui2.grid.Padding, [0 0 0 0]), ... @@ -178,7 +179,7 @@ function checkPanelGridHelper(h) growGrid = uigridlayout(fig, [1 1]); growGrid.RowHeight = {50}; - labkit.ui.createPanelGrid(growGrid, 'Tall Controls', 1, [5 2]); + labkit.ui.view.section(growGrid, 'Tall Controls', 1, [5 2]); assert(growGrid.RowHeight{1} > 50, ... 'Panel-grid helper should grow undersized fixed parent rows to avoid clipped controls.'); end @@ -188,7 +189,7 @@ function checkPlotOptionsPanelHelper(h) cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [3 1]); - ui = labkit.ui.createPlotOptionsPanel(grid, 3); + 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'}), ... @@ -199,7 +200,7 @@ function checkPlotOptionsPanelHelper(h) assert(ui.grid.RowSpacing == 8 && ui.grid.ColumnSpacing == 8, ... 'Plot-options helper should preserve row and column spacing.'); - ui2 = labkit.ui.createPlotOptionsPanel(grid, 2, 2); + 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 @@ -223,7 +224,7 @@ function checkFileSelectionPanelHelper(h) 'clearAll', 'Clear all', ... 'export', 'Export results CSV', ... 'loadedText', 'No files loaded'); - ui = labkit.ui.createFileSelectionPanel(grid, labels, callbacks); + ui = labkit.ui.view.panel(grid, 'files', labels, callbacks); assert(strcmp(ui.panel.Title, 'Files'), 'File-selection panel should preserve the panel title.'); assert(ui.panel.Layout.Row == 1, 'File-selection panel should place the panel in row 1.'); assert(h.sameStringCell(ui.grid.RowHeight, {'fit', '1x', 'fit'}), ... @@ -260,7 +261,7 @@ function checkFileSelectionPanelHelper(h) multiCallbacks.onRemoveSelected = @(~,~) []; multiLabels = labels; multiLabels.removeSelected = 'Remove selected'; - multiUi = labkit.ui.createFileSelectionPanel(grid, multiLabels, multiCallbacks, ... + multiUi = labkit.ui.view.panel(grid, 'files', multiLabels, multiCallbacks, ... struct('showRemoveSelected', true, 'multiselect', 'on', 'row', 2)); assert(strcmp(multiUi.listbox.Multiselect, 'on'), ... 'File-selection panel should support multi-select listboxes.'); diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_busy_state.m b/tests/suites/labkit/ui/test_gui_layout_ui_busy_state.m index 9fe7701..a4ad027 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_busy_state.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_busy_state.m @@ -16,7 +16,7 @@ function test_gui_layout_ui_busy_state() opts = struct(); opts.showDialog = false; opts.controls = {btnRun, btnExport}; - result = labkit.ui.runWithBusyState(fig, @probeWork, opts); + result = labkit.ui.app.runBusy(fig, @probeWork, opts); assert(result == 42, ... 'Busy-state helper should return the work callback output.'); @@ -29,7 +29,7 @@ function test_gui_layout_ui_busy_state() assert(strcmp(fig.Pointer, 'arrow'), ... 'Busy-state helper should restore the figure pointer.'); - assertThrows(@() labkit.ui.runWithBusyState(fig, @failingWork, opts), ... + assertThrows(@() labkit.ui.app.runBusy(fig, @failingWork, opts), ... 'labkit:ui:test:BusyFailure', ... 'Busy-state helper should rethrow callback errors.'); assert(strcmp(btnRun.Enable, 'on') && strcmp(btnExport.Enable, 'off'), ... diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_debug_trace.m b/tests/suites/labkit/ui/test_gui_layout_ui_debug_trace.m index a2d275d..033b1cb 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_debug_trace.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_debug_trace.m @@ -20,7 +20,7 @@ function checkDefaultInstrumentationSkipsScroll(h) btnAction = uibutton(grid, 'Text', 'Default action', ... 'ButtonPushedFcn', @onAction); - debug = labkit.ui.createDebugContext('probe_app', struct()); + debug = labkit.ui.diag.createContext('probe_app', struct()); count = debug.instrumentFigure(fig); assert(count >= 1, 'Default debug instrumentation should wrap component callbacks.'); assert(isequal(fig.WindowScrollWheelFcn, scrollFcn), ... @@ -53,7 +53,7 @@ function checkExplicitInstrumentation(h) 'ButtonPushedFcn', {@onCellAction, 'extra'}); btnCell.Layout.Row = 2; - debug = labkit.ui.createDebugContext('probe_app', struct()); + debug = labkit.ui.diag.createContext('probe_app', struct()); count = debug.instrumentFigure(fig, ... struct('callbackProperties', {{'ButtonPushedFcn'}})); assert(count == 2, 'Debug instrumentation should wrap both button callbacks.'); @@ -77,7 +77,7 @@ function checkExplicitInstrumentation(h) assert(any(contains(lines, 'END ButtonPushedFcn') & contains(lines, '"Cell action"')), ... 'Instrumented cell callbacks should trace END messages.'); - disabled = labkit.ui.createDebugContext('probe_app', struct('traceEnabled', false)); + disabled = labkit.ui.diag.createContext('probe_app', struct('traceEnabled', false)); disabledCount = disabled.instrumentFigure(fig, ... struct('callbackProperties', {{'ButtonPushedFcn'}})); assert(disabledCount == 0, 'traceEnabled=false should skip GUI instrumentation.'); @@ -86,7 +86,7 @@ function checkExplicitInstrumentation(h) cleaner2 = onCleanup(@() delete(fig2)); %#ok scrollCalls = 0; fig2.WindowScrollWheelFcn = @onScroll; - explicitDebug = labkit.ui.createDebugContext('probe_app', struct()); + explicitDebug = labkit.ui.diag.createContext('probe_app', struct()); scrollCount = explicitDebug.instrumentFigure(fig2, ... struct('callbackProperties', "WindowScrollWheelFcn")); assert(scrollCount == 1, ... diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_image_axes_runtime.m b/tests/suites/labkit/ui/test_gui_layout_ui_image_axes_runtime.m index c16574d..ed6b1ec 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_image_axes_runtime.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_image_axes_runtime.m @@ -13,7 +13,7 @@ function test_gui_layout_ui_image_axes_runtime() interactionEvents = {}; traceMessages = {}; - runtime = labkit.ui.createInteractionRuntime(ax, ... + runtime = labkit.ui.tool.createRuntime(ax, ... struct('figure', fig, ... 'defaultScrollFcn', defaultScroll, ... 'onInteractionChanged', @onInteractionChanged, ... diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_panel.m b/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_panel.m index 1cb6edc..5ef5e7d 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_panel.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_panel.m @@ -8,13 +8,18 @@ function test_gui_layout_ui_scale_bar_panel() fig = uifigure('Visible', 'off', 'Name', 'labkit_scale_bar_panel_probe'); cleaner = onCleanup(@() delete(fig)); %#ok grid = uigridlayout(fig, [2 1]); + ax = uiaxes(grid); + ax.Layout.Row = 1; + runtime = labkit.ui.tool.createRuntime(ax, struct('figure', fig)); - calls = struct('measure', 0, 'calibration', 0, 'bar', 0, 'place', 0); - ui = labkit.ui.createScaleBarPanel(grid, 2, ... - struct('onMeasureReference', @onMeasure, ... + calls = struct('beforeEdit', 0, 'referenceEdit', 0, 'calibration', 0, 'bar', 0, 'place', 0); + ui = labkit.ui.tool.scaleBar(grid, 2, runtime, ... + struct('imageSize', [600 1000 3], ... + 'onBeforeReferenceEdit', @onBeforeEdit, ... + 'onReferenceEditChanged', @onReferenceEditChanged, ... 'onCalibrationChanged', @onCalibration, ... 'onScaleBarChanged', @onBar, ... - 'onPlaceScaleBar', @onPlace)); + 'onScaleBarPlaced', @onPlace)); assert(strcmp(ui.panel.Title, 'Scale Bar'), ... 'Scale-bar panel should preserve the default panel title.'); @@ -48,7 +53,7 @@ function test_gui_layout_ui_scale_bar_panel() assert(strcmp(ui.controls.pixelsPerUnitReadout.Value, '4 px/m'), ... 'Scale-bar panel should update the pixels/unit readout.'); - spec = ui.scaleBarSpec([600 1000 3]); + spec = ui.scaleBarSpec(); assert(isequal(spec.color, [0 0 0]) && strcmp(spec.colorName, 'Black'), ... 'Default scale-bar color should be black.'); assert(strcmp(spec.label, '1 m') && spec.pixelsPerUnit == 4, ... @@ -60,7 +65,7 @@ function test_gui_layout_ui_scale_bar_panel() ui.controls.positionDropdown.Value = 'Top left'; ui.controls.barLengthSpinner.Value = 50; h.invokeCallback(ui.controls.barLengthSpinner, 'ValueChangedFcn'); - whiteSpec = ui.scaleBarSpec([600 1000 3]); + whiteSpec = ui.scaleBarSpec(); assert(isequal(whiteSpec.color, [1 1 1]) && strcmp(whiteSpec.colorName, 'White'), ... 'Scale-bar panel should map the White option to a white drawing color.'); assert(strcmp(whiteSpec.position, 'Top left') && strcmp(whiteSpec.verticalAlignment, 'top'), ... @@ -71,22 +76,28 @@ function test_gui_layout_ui_scale_bar_panel() h.invokeCallback(ui.controls.referenceLengthSpinner, 'ValueChangedFcn'); h.invokeCallback(ui.controls.measureReferenceButton, 'ButtonPushedFcn'); h.invokeCallback(ui.controls.placeButton, 'ButtonPushedFcn'); - assert(calls.calibration == 1 && calls.measure == 1 && calls.place == 1, ... - 'Scale-bar panel should wire calibration, measure, and place callbacks.'); + assert(calls.calibration == 1 && calls.beforeEdit == 1 && ... + calls.referenceEdit == 1 && calls.place == 1, ... + 'Scale-bar tool should wire calibration, reference-edit, and place callbacks.'); ui.setEnabled(struct('hasImage', false)); assert(strcmp(ui.controls.measureReferenceButton.Enable, 'off') && ... strcmp(ui.controls.placeButton.Enable, 'off'), ... 'Scale-bar controls should disable image-dependent actions when no image is loaded.'); - ui.setEnabled(struct('hasImage', true, 'referenceEditActive', true, ... + h.invokeCallback(ui.controls.measureReferenceButton, 'ButtonPushedFcn'); + ui.setEnabled(struct('hasImage', true, ... 'blockInputs', false, 'blockPlacement', true)); assert(strcmp(ui.controls.measureReferenceButton.Text, 'Finish reference edit') && ... strcmp(ui.controls.referencePixelsSpinner.Enable, 'off') && ... strcmp(ui.controls.placeButton.Enable, 'off'), ... 'Scale-bar controls should reflect reference-edit mode.'); - function onMeasure(~, ~) - calls.measure = calls.measure + 1; + function onBeforeEdit(~, ~) + calls.beforeEdit = calls.beforeEdit + 1; + end + + function onReferenceEditChanged(~, ~) + calls.referenceEdit = calls.referenceEdit + 1; end function onCalibration(~, ~) diff --git a/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_tool.m b/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_tool.m index 102f51b..8ce213a 100644 --- a/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_tool.m +++ b/tests/suites/labkit/ui/test_gui_layout_ui_scale_bar_tool.m @@ -14,9 +14,9 @@ function test_gui_layout_ui_scale_bar_tool() calls = struct('beforeEdit', 0, 'edit', 0, 'calibration', 0, ... 'bar', 0, 'placed', 0, 'error', 0); traceMessages = {}; - runtime = labkit.ui.createInteractionRuntime(ax, ... + runtime = labkit.ui.tool.createRuntime(ax, ... struct('figure', fig, 'onTrace', @captureTrace)); - tool = labkit.ui.createScaleBarTool(grid, 2, runtime, ... + tool = labkit.ui.tool.scaleBar(grid, 2, runtime, ... struct('onBeforeReferenceEdit', @onBeforeEdit, ... 'onReferenceEditChanged', @onEdit, ... 'onCalibrationChanged', @onCalibration, ... @@ -72,8 +72,8 @@ function test_gui_layout_ui_scale_bar_tool() ax2 = uiaxes(grid2); ax2.Layout.Row = 1; bg2 = imagesc(ax2, rand(40, 80)); - runtime2 = labkit.ui.createInteractionRuntime(ax2, struct('figure', fig2)); - tool2 = labkit.ui.createScaleBarTool(grid2, 2, runtime2, ... + runtime2 = labkit.ui.tool.createRuntime(ax2, struct('figure', fig2)); + tool2 = labkit.ui.tool.scaleBar(grid2, 2, runtime2, ... struct('onError', @onError)); tool2.setImageSize([40 80 1]); h.invokeCallback(tool2.controls.measureReferenceButton, 'ButtonPushedFcn'); @@ -128,8 +128,8 @@ function checkReferenceEditRestartDoesNotReenterRefresh(h) calibrationCalls = 0; refreshCalls = 0; - runtime = labkit.ui.createInteractionRuntime(ax, struct('figure', fig)); - tool = labkit.ui.createScaleBarTool(grid, 2, runtime, ... + runtime = labkit.ui.tool.createRuntime(ax, struct('figure', fig)); + tool = labkit.ui.tool.scaleBar(grid, 2, runtime, ... struct('onCalibrationChanged', @onCalibration, ... 'onError', @onError)); tool.setImageSize([60 80 1]); diff --git a/tests/suites/labkit/ui/test_plotXY.m b/tests/suites/labkit/ui/test_plotXY.m index 0d4fa6c..bc22335 100644 --- a/tests/suites/labkit/ui/test_plotXY.m +++ b/tests/suites/labkit/ui/test_plotXY.m @@ -22,7 +22,7 @@ function test_plotXY() opts = struct('holdPlot', false, 'showGrid', true, 'lineWidth', 1.2); labels = struct('title', curve.name, 'x', xname, 'y', yname); - info = labkit.ui.plotXY(ax, x, y, labels, opts); + 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.'); @@ -35,7 +35,7 @@ function test_plotXY() 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.plotXY(ax, [], y, labels, opts); + 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 diff --git a/tests/suites/labkit/ui/test_scaleBarCalibration.m b/tests/suites/labkit/ui/test_scaleBarCalibration.m index 1d94042..0df80da 100644 --- a/tests/suites/labkit/ui/test_scaleBarCalibration.m +++ b/tests/suites/labkit/ui/test_scaleBarCalibration.m @@ -7,7 +7,7 @@ function test_scaleBarCalibration() end function checkTypedCalibration() - cal = labkit.ui.scaleBarCalibration(80, 20, "mm"); + cal = labkit.ui.tool.scaleBarCalibration(80, 20, "mm"); assert(cal.isCalibrated, 'Positive reference pixels and length should calibrate.'); assert(cal.pixelsPerUnit == 4, 'Pixels per unit calculation changed.'); assert(strcmp(cal.unit, 'mm'), 'Selected scale unit should be preserved.'); @@ -16,7 +16,7 @@ function checkTypedCalibration() end function checkReferenceLineCalibration() - cal = labkit.ui.scaleBarCalibration(NaN, 2, "cm", ... + cal = labkit.ui.tool.scaleBarCalibration(NaN, 2, "cm", ... struct('referenceLine', [0 0; 3 4])); assert(cal.isCalibrated, 'Two reference endpoints should provide reference pixels.'); assert(cal.referencePixels == 5, 'Reference line pixel distance changed.'); @@ -26,7 +26,7 @@ function checkReferenceLineCalibration() end function checkFallbackUnitAndMissingScale() - cal = labkit.ui.scaleBarCalibration(NaN, 0, "inch"); + cal = labkit.ui.tool.scaleBarCalibration(NaN, 0, "inch"); assert(~cal.isCalibrated, 'Missing reference scale should remain uncalibrated.'); assert(cal.pixelsPerUnit == 0, 'Missing reference scale should produce zero pixels/unit.'); assert(strcmp(cal.unit, 'm'), 'Unsupported units should fall back to the default unit.'); diff --git a/tests/suites/project/test_app_owned_workflow_boundaries.m b/tests/suites/project/test_app_owned_workflow_boundaries.m index 7b26ffa..fb9a9b4 100644 --- a/tests/suites/project/test_app_owned_workflow_boundaries.m +++ b/tests/suites/project/test_app_owned_workflow_boundaries.m @@ -52,14 +52,14 @@ function test_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.plotXY'), ... - 'CSC plotting should use the reusable prepared-X/Y GUI helper.'); + assert(contains(cscSource, 'labkit.ui.view.draw'), ... + 'CSC plotting should use the reusable prepared-X/Y GUI render facade.'); 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', 'plotXY.m'), 'file') == 2, ... - 'Reusable prepared-X/Y plotting should live in +labkit/+ui.'); + 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', '+analysis', 'computeCSC.m'), 'file') ~= 2, ... 'CSC-specific analysis should not live in reusable +labkit analysis.'); @@ -147,17 +147,17 @@ function test_app_owned_workflow_boundaries() assert(exist(fullfile(root, '+labkit', '+ui', 'refreshSingleSelectFileListbox.m'), 'file') ~= 2, ... 'Item-schema-specific single-select file listbox refresh should stay in the owning app.'); assert(exist(fullfile(root, '+labkit', '+ui', 'resetTopBottomAxes.m'), 'file') ~= 2, ... - 'Title-specific top/bottom axes reset should stay in the owning app or call hardResetAxis directly.'); + 'Title-specific top/bottom axes reset should stay in the owning app or call view.draw reset.'); assert(exist(fullfile(root, '+labkit', '+ui', 'createTwoPaneShell.m'), 'file') ~= 2, ... - 'The old two-pane shell name should not be reintroduced; use createAppShell.'); + 'The old two-pane shell name should not be reintroduced; use app.createShell.'); assert(exist(fullfile(root, '+labkit', '+ui', 'createStandardWorkbenchShell.m'), 'file') ~= 2, ... - 'Compatibility shell wrappers should not be reintroduced; use createAppShell.'); + 'Compatibility shell wrappers should not be reintroduced; use app.createShell.'); assert(exist(fullfile(root, '+labkit', '+ui', 'createTabbedDualPlotShell.m'), 'file') ~= 2, ... - 'Compatibility shell wrappers should not be reintroduced; use createAppShell.'); + 'Compatibility shell wrappers should not be reintroduced; use app.createShell.'); assert(exist(fullfile(root, '+labkit', '+ui', 'createSingleTabWorkbenchShell.m'), 'file') ~= 2, ... 'Single-tab app shells should not be reintroduced; use the standard three-tab workbench shell.'); assert(exist(fullfile(root, '+labkit', '+ui', 'createFilePanel.m'), 'file') ~= 2, ... - 'Separate file-button-only panels should not be reintroduced; use createFileSelectionPanel.'); + 'Separate file-button-only panels should not be reintroduced; use view.panel files.'); assert(exist(fullfile(root, '+labkit', '+ui', 'createSingleSelectFilePanel.m'), 'file') ~= 2, ... - 'Separate single-select file panels should not be reintroduced; use createFileSelectionPanel.'); + 'Separate single-select file panels should not be reintroduced; use view.panel files.'); end diff --git a/tests/suites/project/test_package_dependency_boundaries.m b/tests/suites/project/test_package_dependency_boundaries.m index 24e1a65..e2fbbe5 100644 --- a/tests/suites/project/test_package_dependency_boundaries.m +++ b/tests/suites/project/test_package_dependency_boundaries.m @@ -19,14 +19,32 @@ function test_package_dependency_boundaries() h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+biosignal', 'private'), ... [guiWords {'apps/', 'labkit.ui', 'labkit.dta'} appWords], ... 'Biosignal private implementation'); + uiForbidden = [{'DTA', 'Gamry', 'labkit.dta', 'labkit.io', ... + 'labkit.data', 'labkit.analysis', 'apps/'} appWords]; h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui'), ... - [{'DTA', 'Gamry', 'labkit.dta', 'labkit.io', 'labkit.data', 'labkit.analysis', 'apps/'} ... - appWords], ... - 'Reusable +labkit UI'); - h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', 'private'), ... - [{'DTA', 'Gamry', 'labkit.dta', 'labkit.io', 'labkit.data', 'labkit.analysis', 'apps/'} ... - appWords], ... - 'Reusable +labkit UI private implementation'); + uiForbidden, ... + 'Reusable +labkit UI root'); + h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', '+app'), ... + uiForbidden, ... + 'Reusable +labkit UI app facade'); + h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', '+app', 'private'), ... + uiForbidden, ... + 'Reusable +labkit UI app private implementation'); + h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', '+view'), ... + uiForbidden, ... + 'Reusable +labkit UI view facade'); + h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', '+view', 'private'), ... + uiForbidden, ... + 'Reusable +labkit UI view private implementation'); + h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', '+tool'), ... + uiForbidden, ... + 'Reusable +labkit UI tool facade'); + h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', '+tool', 'private'), ... + uiForbidden, ... + 'Reusable +labkit UI tool private implementation'); + h.assertPackageSourcesDoNotContain(fullfile(root, '+labkit', '+ui', '+diag'), ... + uiForbidden, ... + 'Reusable +labkit UI diagnostics facade'); assert(exist(fullfile(root, '+labkit', '+ui', 'loadFilesIntoSession.m'), 'file') ~= 2, ... 'GUI-free session loading should live in +labkit/+dta, not +ui.'); diff --git a/tests/suites/project/test_package_public_surface.m b/tests/suites/project/test_package_public_surface.m index 8f1d8c2..bc08b14 100644 --- a/tests/suites/project/test_package_public_surface.m +++ b/tests/suites/project/test_package_public_surface.m @@ -17,35 +17,47 @@ function test_package_public_surface() h.assertNoPackageMFiles(fullfile(root, '+labkit', '+util'), ... 'Reusable +labkit utility'); - uiStablePublic = {'addRowResizeHandle.m', 'appendLog.m', 'clearAxisObjects.m', ... - 'createAnchorCurveEditor.m', 'createAppShell.m', 'createAxes.m', ... - 'createDebugContext.m', 'createFileSelectionPanel.m', ... - 'createInteractionRuntime.m', 'createLabeledDropdown.m', ... + h.assertTopLevelMFiles(fullfile(root, '+labkit', '+ui'), {}, ... + 'Layered +labkit UI root'); + h.assertTopLevelMFiles(fullfile(root, '+labkit', '+ui', '+app'), ... + {'createShell.m', 'dispatchRequest.m', 'runBusy.m', 'tab.m'}, ... + 'UI app facade'); + h.assertPackageMFiles(fullfile(root, '+labkit', '+ui', '+app', 'private'), ... + {'addRowResizeHandle.m', 'attachColumnResize.m', ... + 'createTabbedWorkbenchShell.m', 'disableAxesInteractivity.m'}, ... + 'UI app private implementation'); + h.assertPackageMFiles(fullfile(root, '+labkit', '+ui', '+diag'), ... + {'createContext.m'}, ... + 'UI diagnostics facade'); + h.assertTopLevelMFiles(fullfile(root, '+labkit', '+ui', '+view'), ... + {'axes.m', 'draw.m', 'form.m', 'panel.m', 'place.m', ... + 'section.m', 'update.m'}, ... + 'UI view facade'); + h.assertPackageMFiles(fullfile(root, '+labkit', '+ui', '+view', 'private'), ... + {'appendLog.m', 'clearAxes.m', 'createLabeledDropdown.m', ... 'createLabeledEditField.m', 'createLabeledSpinner.m', ... - 'createLogPanel.m', 'createPanelGrid.m', 'createPlotOptionsPanel.m', ... 'createReadOnlyInfoRow.m', 'createReadOnlyTextField.m', ... - 'createReadOnlyTextPanel.m', 'createResultTablePanel.m', ... - 'createScaleBarPanel.m', 'createScaleBarTool.m', ... - 'createTopBottomPlotControls.m', 'dispatchAppRequest.m', ... - 'enableAxesPopout.m', 'hardResetAxis.m', 'layoutRow.m', ... - 'plotXY.m', 'popoutAxes.m', 'refreshListboxItems.m', ... - 'refreshListboxSelection.m', 'setTopBottomPlotSelections.m', ... - 'runWithBusyState.m', 'scaleBarCalibration.m', 'showImageAxes.m', ... - 'swapTopBottomPlotSelections.m', 'tabSpec.m'}; - uiDeprecatedPublic = {'createAppDebugLog.m', 'createImageAxesRuntime.m', ... - 'createWorkbench.m', 'handleAppRequest.m'}; - h.assertTopLevelMFiles(fullfile(root, '+labkit', '+ui'), ... - [uiStablePublic, uiDeprecatedPublic], ... - 'Public reusable +labkit UI facade'); + 'enablePopout.m', 'fileSelectionPanel.m', 'layoutRow.m', ... + 'logPanel.m', 'plotOptionsPanel.m', 'plotXY.m', 'popoutAxes.m', ... + 'refreshListboxItems.m', 'refreshListboxSelection.m', ... + 'resetAxes.m', 'resultTable.m', 'setTopBottomPlotSelections.m', ... + 'showImage.m', 'swapTopBottomPlotSelections.m', 'textPanel.m', ... + 'topBottomPlotControls.m'}, ... + 'UI view private implementation'); + h.assertTopLevelMFiles(fullfile(root, '+labkit', '+ui', '+tool'), ... + {'anchorEditor.m', 'createRuntime.m', 'scaleBar.m', ... + 'scaleBarCalibration.m'}, ... + 'UI tool facade'); + h.assertPackageMFiles(fullfile(root, '+labkit', '+ui', '+tool', 'private'), ... + {'addOrInsertAnchor.m', 'anchorCurvePoints.m', ... + 'createLabeledDropdown.m', 'createLabeledEditField.m', ... + 'createLabeledSpinner.m', 'createReadOnlyInfoRow.m', ... + 'createReadOnlyTextField.m', 'defaultScaleBarUnits.m', ... + 'drawScaleBarOverlay.m', 'normalizeScaleBarUnit.m', ... + 'scaleBarPanel.m'}, ... + 'UI tool private implementation'); assertNoPublicPackage(fullfile(root, '+labkit', '+ui', '+control'), ... 'UI control helpers should stay private instead of becoming a public helper-dump package.'); - assertDeprecatedUiSurface(root, uiDeprecatedPublic); - h.assertPackageMFiles(fullfile(root, '+labkit', '+ui', 'private'), ... - {'addOrInsertAnchor.m', 'anchorCurvePoints.m', 'attachColumnResize.m', ... - 'createTabbedWorkbenchShell.m', 'defaultScaleBarUnits.m', ... - 'disableAxesInteractivity.m', 'drawScaleBarOverlay.m', ... - 'normalizeScaleBarUnit.m'}, ... - 'UI private implementation'); h.assertTopLevelMFiles(fullfile(root, '+labkit', '+dta'), ... {'addFilesToSession.m', 'detectPulses.m', 'detectType.m', 'findFiles.m', ... @@ -96,13 +108,3 @@ function test_package_public_surface() function assertNoPublicPackage(packageDir, message) assert(exist(packageDir, 'dir') ~= 7, message); end - -function assertDeprecatedUiSurface(root, deprecatedFiles) - uiDir = fullfile(root, '+labkit', '+ui'); - for iFile = 1:numel(deprecatedFiles) - filepath = fullfile(uiDir, deprecatedFiles{iFile}); - source = fileread(filepath); - assert(contains(source, 'Deprecated') || contains(source, 'deprecated'), ... - ['Deprecated UI surface should be documented in ' deprecatedFiles{iFile} '.']); - end -end