Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions +labkit/+app/+internal/+contract/CompiledDefinition.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
interactionIds = string(cellfun(@(value) value.Id, ...
interactions, "UniformOutput", false));
assertUnique([ids interactionIds], "Layout and interaction");
validateLayoutContracts(nodes);
targetMask = cellfun(@(value) ...
~isempty(value.Capabilities), nodes);
obj.TargetNodes = [nodes(targetMask) interactions];
Expand Down Expand Up @@ -70,6 +71,13 @@
"Target %s does not declare a renderer.", ...
operation.Target);
end
if operation.Kind == "tableData" && ...
any(operation.Value.ColumnEditable) && ...
~hasSignal(node, "cellEdited")
error("labkit:app:contract:InvalidValue", ...
"Editable dataTable %s must declare OnCellEdited.", ...
operation.Target);
end
covered(index) = true;
end
missing = obj.TargetIds(~covered);
Expand Down Expand Up @@ -97,6 +105,57 @@
end
end

function validateLayoutContracts(nodes)
for k = 1:numel(nodes)
node = nodes{k};
config = node.configurationForCompiler();
switch node.Kind
case "field"
if config.Kind ~= "readonly"
requireValueOwner(node, config);
end
case {"rangeField", "slider"}
requireValueOwner(node, config);
case "fileList"
if strlength(config.Bind) == 0
error("labkit:app:contract:InvalidValue", ...
"fileList %s must declare Bind.", node.Id);
end
case "plotArea"
if ~isempty(config.ViewModes) && ...
~hasSignal(node, "valueChanged")
error("labkit:app:contract:InvalidValue", ...
"plotArea %s with ViewModes must declare OnValueChanged.", ...
node.Id);
end
case "dataTable"
if any(config.ColumnEditable) && ...
~hasSignal(node, "cellEdited")
error("labkit:app:contract:InvalidValue", ...
"Editable dataTable %s must declare OnCellEdited.", ...
node.Id);
end
case "workspace"
if hasSignal(node, "pageChanged") && isempty(node.PageIds)
error("labkit:app:contract:InvalidValue", ...
"Workspace OnPageChanged requires named pages.");
end
end
end
end

function requireValueOwner(node, config)
if strlength(config.Bind) == 0 && ~hasSignal(node, "valueChanged")
error("labkit:app:contract:InvalidValue", ...
"Interactive %s %s must declare Bind or OnValueChanged.", ...
node.Kind, node.Id);
end
end

function tf = hasSignal(node, signal)
tf = any(cellfun(@(value) value.Signal == signal, node.Signals));
end

function plan = compilePlatformPlan(nodes)
compiled = repmat(struct( ...
"Kind", "", "Id", "", "ChildIds", strings(1, 0), ...
Expand Down
29 changes: 21 additions & 8 deletions +labkit/+app/+internal/+diagnostics/RuntimeDiagnostics.m
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function setTraceCapture(obj, enabled)
function destination = exportBundle( ...
obj, destination, state, stateMode)
if nargin < 4
stateMode = "exact";
stateMode = "compact";
end
stateMode = validateStateMode(stateMode);
operation = obj.Recorder.begin( ...
Expand All @@ -80,24 +80,37 @@ function setTraceCapture(obj, enabled)
end
end

function destination = exportAfterErrorOnClose(obj, state)
destination = "";
if ~obj.Recorder.hasErrorOrCriticalEvent()
return
end
try
destination = obj.exportBundle( ...
obj.automaticDestination("compact"), state, "compact");
catch
% Diagnostic persistence must not change Runtime close semantics.
end
end

function destination = exportInteractive(obj, state)
selection = obj.Context.chooseOption( ...
"Every bundle contains complete sensitive logs and App " + ...
"state. Compact MAT replaces supported state values over " + ...
"1 MiB with structural synthetic placeholders.", ...
["Complete bundle (exact MAT)", ...
"Complete bundle (compact synthetic MAT)", ...
["Complete bundle (compact synthetic MAT)", ...
"Complete bundle (exact MAT)", ...
"Cancel"], ...
Title="Export Diagnostic Bundle", ...
DefaultChoice="Complete bundle (exact MAT)", ...
DefaultChoice="Complete bundle (compact synthetic MAT)", ...
CancelChoice="Cancel");
if selection.Cancelled || selection.Value == "Cancel"
destination = "";
return
end
stateMode = "exact";
if selection.Value == "Complete bundle (compact synthetic MAT)"
stateMode = "compact";
stateMode = "compact";
if selection.Value == "Complete bundle (exact MAT)"
stateMode = "exact";
end
destination = "";
try
Expand Down Expand Up @@ -131,7 +144,7 @@ function setTraceCapture(obj, enabled)
function destination = exportTextFallback( ...
obj, preferredDestination, cause, stateMode)
if nargin < 4
stateMode = "exact";
stateMode = "compact";
end
stateMode = validateStateMode(stateMode);
obj.Recorder.log( ...
Expand Down
4 changes: 2 additions & 2 deletions +labkit/+app/+internal/+diagnostics/SessionDiagnosticBundle.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
privateState = [];
end
if nargin < 4
stateMode = "exact";
stateMode = "compact";
end
snapshot = validateSnapshot(snapshot);
stateMode = ...
Expand Down Expand Up @@ -79,7 +79,7 @@
function destination = writeFallback( ...
snapshot, preferredDestination, stateMode)
if nargin < 3
stateMode = "exact";
stateMode = "compact";
end
snapshot = validateFallbackSnapshot(snapshot);
stateMode = ...
Expand Down
8 changes: 6 additions & 2 deletions +labkit/+app/+internal/+diagnostics/SessionDiagnostics.m
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ function setTraceEnabled(obj, enabled)
obj.Stream.setTraceEnabled(enabled);
end

function observed = hasErrorOrCriticalEvent(obj)
observed = obj.Stream.hasErrorOrCriticalEvent();
end

function destination = exportBundle( ...
obj, destination, excludeOperationId, ...
privateState, stateMode)
Expand All @@ -110,7 +114,7 @@ function setTraceEnabled(obj, enabled)
privateState = [];
end
if nargin < 5
stateMode = "exact";
stateMode = "compact";
end
obj.Journal.flush();
streamSnapshot = obj.Stream.captureSnapshot();
Expand Down Expand Up @@ -154,7 +158,7 @@ function setTraceEnabled(obj, enabled)
function destination = exportTextFallback( ...
obj, preferredDestination, failure, stateMode)
if nargin < 4
stateMode = "exact";
stateMode = "compact";
end
% Keep this path independent of the journal and ZIP staging so a
% failure in either subsystem cannot consume the last evidence.
Expand Down
13 changes: 10 additions & 3 deletions +labkit/+app/+internal/+diagnostics/SessionEventStream.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ProjectionHealthHook = []
ProjectionHealthUnavailableReported (1, 1) logical = false
TraceEnabled (1, 1) logical = false
ErrorOrCriticalObserved (1, 1) logical = false
ConsumerSequence (1, 1) double = 0
Consumers (1, :) struct = struct( ...
"Id", strings(1, 0), "Callback", cell(1, 0))
Expand Down Expand Up @@ -182,12 +183,18 @@ function log(obj, severity, eventName, message, varargin)
end
record.exception = exception;
obj.retain(record);
if any(severity == ["error", "critical"]) && ...
~obj.TraceEnabled
obj.setTraceEnabled(true);
if any(severity == ["error", "critical"])
obj.ErrorOrCriticalObserved = true;
if ~obj.TraceEnabled
obj.setTraceEnabled(true);
end
end
end

function observed = hasErrorOrCriticalEvent(obj)
observed = obj.ErrorOrCriticalObserved;
end

function records = records(obj)
records = obj.Records;
end
Expand Down
4 changes: 2 additions & 2 deletions +labkit/+app/+internal/+launcher/launcherVersion.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
info = struct( ...
"name", "labkit_launcher", ...
"displayName", "LabKit App Launcher", ...
"version", "1.8.3", ...
"updated", "2026-08-03");
"version", "1.8.4", ...
"updated", "2026-08-06");
end
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
PriorPointer (1, 1) string = "arrow"
ClosePrompt
DialogFolders
ReadonlyHeights
Starting (1, 1) logical = false
StartupStarted
StartupPanel
Expand All @@ -45,9 +46,12 @@
"ValueType", "any");
obj.DialogFolders = containers.Map("KeyType", "char", ...
"ValueType", "char");
obj.ReadonlyHeights = containers.Map("KeyType", "char", ...
"ValueType", "double");
policy = labkit.app.internal.native.NativeAdapterValues.layoutPolicy();
obj.Figure = uifigure(Visible="off", ...
Name=char(string(title)), ...
Tag="labkitApp", ...
Position=policy.InitialFigurePosition);
obj.BusyLifecycle = ...
labkit.app.internal.native.BusyLifecycle( ...
Expand All @@ -60,6 +64,8 @@
obj.Figure.Pointer = "watch";
setappdata(obj.Figure, "labkitAppBusy", true);
obj.buildTree();
setappdata(obj.Figure, "labkitAppLayoutChanged", ...
@() obj.refreshReadonlySurfaces());
obj.createStartupSurface();
obj.startupUpdate("Preparing runtime...");
end
Expand Down Expand Up @@ -136,6 +142,8 @@ function show(obj, title)
return
end
obj.Figure.Visible = "on";
drawnow limitrate nocallbacks
obj.refreshReadonlySurfaces();
if mode == "minimized" && isprop(obj.Figure, "WindowState")
obj.Figure.WindowState = "minimized";
end
Expand Down Expand Up @@ -346,9 +354,13 @@ function createStartupSurface(obj)

applyTableData(~, component, model)

applyText(~, component, value)
applyText(obj, component, value)

applyValue(obj, component, value)

updateReadonlyHeight(obj, component, value)

applyValue(~, component, value)
refreshReadonlySurfaces(obj)

applyLimits(~, component, limits)

Expand Down Expand Up @@ -421,7 +433,9 @@ function runUtility(obj, callback, title)
try
callback();
catch cause
obj.alert(cause.message, title);
obj.alert( ...
labkit.app.internal.native.NativeAdapterValues.deepestCauseMessage( ...
cause), title);
end
end

Expand Down
4 changes: 4 additions & 0 deletions +labkit/+app/+internal/+native/@MatlabPlatformAdapter/apply.m
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ function apply(obj, operation)
case "workspacePage"
labkit.app.internal.native.NativeAdapterValues.setIfProperty(component, "Enable", labkit.app.internal.native.NativeAdapterValues.onOff(operation.Value.Enabled));
component.UserData = struct("Status", operation.Value.Status);
otherwise
error("labkit:app:runtime:InvariantFailure", ...
"No native presentation policy for operation %s.", ...
operation.Kind);
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function applyText(~, component, value)
function applyText(obj, component, value)
% Class-folder implementation of MatlabPlatformAdapter.applyText.
if isstruct(component.UserData) && ...
isfield(component.UserData, "Status") && ...
Expand All @@ -19,6 +19,15 @@ function applyText(~, component, value)
elseif isprop(component, "Title")
component.Title = value;
end
if isstruct(component.UserData) && ...
isfield(component.UserData, "Readonly") && ...
component.UserData.Readonly && isprop(component, "Tooltip")
component.Tooltip = char(join( ...
labkit.app.internal.native.NativeAdapterValues.readonlyLines(value), ...
newline));
obj.updateReadonlyHeight(component, value);
return
end
labkit.app.internal.native.NativeAdapterValues.fitText(component);
if isappdata(component, "labkitAppLogFollowLatest") && ...
getappdata(component, "labkitAppLogFollowLatest")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
function applyValue(~, component, value)
function applyValue(obj, component, value)
% Class-folder implementation of MatlabPlatformAdapter.applyValue.
if isstruct(component.UserData) && ...
isfield(component.UserData, "Readonly") && ...
component.UserData.Readonly
obj.applyText(component, value);
return
end
mode = labkit.app.internal.native.NativeAdapterValues.linkedPlotMode(component);
if ~isempty(mode)
mode.Value = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
Enable=labkit.app.internal.native.NativeAdapterValues.onOff(config.Enabled), ...
Tooltip=char(config.Tooltip));
labkit.app.internal.native.NativeAdapterValues.fitText(component, ...
CharsPerStep=18, MaxShrinkSteps=3);
CharsPerStep=28, MaxShrinkSteps=2);
if isprop(component, "WordWrap")
component.WordWrap = "off";
end
case "field"
component = obj.createField(parent, config, node.Id);
case "rangeField"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,31 @@
component = uidropdown(parent, Items=choices, ...
Value=value, Enable=labkit.app.internal.native.NativeAdapterValues.onOff(config.Enabled));
case "readonly"
component = uitextarea(parent, Editable="off", ...
Value=char(string(value)), ...
policy = labkit.app.internal.native.NativeAdapterValues.layoutPolicy();
valuePanel = uipanel(parent, BorderType="none", ...
AutoResizeChildren="off");
initialHeight = labkit.app.internal.native.NativeAdapterValues.readonlyHeight( ...
value, policy.ReadonlyDefaultWidth, policy.ReadonlyFontSize);
lines = labkit.app.internal.native.NativeAdapterValues.readonlyLines(value);
component = uitextarea(valuePanel, Editable="off", ...
Value=cellstr(lines), ...
WordWrap="on", ...
Position=[0 0 policy.ReadonlyDefaultWidth initialHeight], ...
Enable=labkit.app.internal.native.NativeAdapterValues.onOff(config.Enabled));
if isprop(component, "WordWrap")
component.WordWrap = "on";
end
otherwise
component = uieditfield(parent, "text", ...
Value=string(value), Enable=labkit.app.internal.native.NativeAdapterValues.onOff(config.Enabled));
end
component.UserData = struct( ...
"LayoutContainer", layoutContainer);
"LayoutContainer", layoutContainer, ...
"Readonly", config.Kind == "readonly", ...
"NodeId", string(id));
if config.Kind == "readonly" && isprop(component, "Tooltip")
component.Tooltip = char(join( ...
labkit.app.internal.native.NativeAdapterValues.readonlyLines(value), ...
newline));
end
end
Loading