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
41 changes: 37 additions & 4 deletions +labkit/+app/+internal/+launcher/dispatch.m
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ function launchSelected()
addPathIfMissing(app.folder, "-end");
reportLaunchStage(app, 2, ...
"initializing app window via " + app.command);
feval(app.command);
invokeDiscoveredApp(app);
setStatus("Finishing startup for " + app.name + "...");
drawnow;
setStatus("Opened " + app.command + ".");
Expand Down Expand Up @@ -516,7 +516,7 @@ function updateInfo()
info = struct( ...
"name", "labkit_launcher", ...
"displayName", "LabKit App Launcher", ...
"version", "1.8.1", ...
"version", "1.8.2", ...
"updated", "2026-07-30");
end

Expand Down Expand Up @@ -696,14 +696,47 @@ function addPathIfMissing(folder, varargin)
addpath(folder, "-begin");
cleanup = onCleanup(@() rmpath(folder));
end
switch string(name)
case "manageLabKitVersions"
callable = @manageLabKitVersions;
case "cleanLabKitArtifacts"
callable = @cleanLabKitArtifacts;
case "renderLabKitDocs"
callable = @renderLabKitDocs;
case "runCodecheckReport"
callable = @runCodecheckReport;
case "profileLabKitTarget"
callable = @profileLabKitTarget;
case "packageLabKitApp"
callable = @packageLabKitApp;
otherwise
error("labkit:app:internal:launcher:UnknownTool", ...
"Launcher tool is not allowlisted: %s", name);
end
if nargout > 0
[varargout{1:nargout}] = feval(name, varargin{:});
[varargout{1:nargout}] = callable(varargin{:});
else
feval(name, varargin{:});
callable(varargin{:});
end
clear cleanup
end

function invokeDiscoveredApp(app)
command = string(app.command);
resolved = string(which(char(command)));
expected = fullfile(string(app.folder), command + [".m", ".p"]);
available = arrayfun(@(candidate) exist(candidate, "file") == 2, expected);
expected = expected(available);
if strlength(resolved) == 0 || isempty(expected) || ...
~any(normalizePathEntry(resolved) == normalizePathEntry(expected))
error("labkit:app:internal:launcher:AppEntryMismatch", ...
"Discovered App entry does not resolve from its owning folder: %s", command);
end
% Dynamic extension boundary: the command is derived from and revalidated
% against one discovered labkit_*_app.m or .p file before invocation.
feval(char(command));
end

function apps = discoverApps(root)
apps = emptyApps();
roots = [string(fullfile(root, "apps")); privateAppRoots(root)];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,24 +263,46 @@ function alert(obj, message, title)
function result = chooseInputFile(~, filters, startPath)
filters = labkit.app.internal.NativeAdapterValues.dialogFilters(filters);
[name, folder] = uigetfile(filters, "Choose input file", ...
labkit.app.internal.NativeAdapterValues.safeStartPath(startPath));
labkit.app.internal.NativeAdapterValues.dialogStartFolder( ...
"input", startPath));
if ~isequal(name, 0)
labkit.app.internal.NativeAdapterValues.rememberDialogFolder( ...
"input", folder);
end
result = labkit.app.internal.NativeAdapterValues.dialogPath(name, folder);
end

function result = chooseInputFolder(~, startPath)
folder = uigetdir(labkit.app.internal.NativeAdapterValues.safeStartPath(startPath), "Choose input folder");
folder = uigetdir( ...
labkit.app.internal.NativeAdapterValues.dialogStartFolder( ...
"input", startPath), "Choose input folder");
if ~isequal(folder, 0)
labkit.app.internal.NativeAdapterValues.rememberDialogFolder( ...
"input", folder);
end
result = labkit.app.internal.NativeAdapterValues.folderDialogPath(folder);
end

function result = chooseOutputFile(~, filters, startPath)
filters = labkit.app.internal.NativeAdapterValues.dialogFilters(filters);
[name, folder] = uiputfile(filters, "Choose output file", ...
labkit.app.internal.NativeAdapterValues.safeStartPath(startPath));
labkit.app.internal.NativeAdapterValues.dialogStartFolder( ...
"output", startPath));
if ~isequal(name, 0)
labkit.app.internal.NativeAdapterValues.rememberDialogFolder( ...
"output", folder);
end
result = labkit.app.internal.NativeAdapterValues.dialogPath(name, folder);
end

function result = chooseOutputFolder(~, startPath)
folder = uigetdir(labkit.app.internal.NativeAdapterValues.safeStartPath(startPath), "Choose output folder");
folder = uigetdir( ...
labkit.app.internal.NativeAdapterValues.dialogStartFolder( ...
"output", startPath), "Choose output folder");
if ~isequal(folder, 0)
labkit.app.internal.NativeAdapterValues.rememberDialogFolder( ...
"output", folder);
end
result = labkit.app.internal.NativeAdapterValues.folderDialogPath(folder);
end
end
Expand Down Expand Up @@ -458,7 +480,7 @@ function popoutAllPlots(obj)

function copyAllPlots(obj)
handles = obj.allAxes();
if numel(handles) == 1
if isscalar(handles)
copygraphics(handles(1), ContentType="image");
elseif ~isempty(handles)
copygraphics(obj.Figure, ContentType="image");
Expand Down Expand Up @@ -634,6 +656,8 @@ function chooseFiles(obj, target)
return;
end
obj.DialogFolders(char(target)) = char(folder);
labkit.app.internal.NativeAdapterValues.rememberDialogFolder( ...
"input", folder);
paths = string(folder) + filesep + string(names);
if config.SelectionMode == "single"
paths = paths(1);
Expand All @@ -653,6 +677,8 @@ function chooseFolderFiles(obj, target, recursive)
return
end
obj.DialogFolders(char(target)) = char(folder);
labkit.app.internal.NativeAdapterValues.rememberDialogFolder( ...
"input", folder);
paths = labkit.app.internal.NativeAdapterValues.filesInFolder(folder, config.Filters, recursive);
if recursive && ...
numel(paths) > config.FolderWarningThreshold
Expand Down Expand Up @@ -691,7 +717,8 @@ function removeSelectedFiles(obj, target, list)
end
folder = char(string(configured));
if isempty(folder) || ~isfolder(folder)
folder = labkit.app.internal.NativeAdapterValues.userDialogFolder();
folder = labkit.app.internal.NativeAdapterValues.dialogStartFolder( ...
"input", "");
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function applyFilePaths(~, component, paths)
if ~isempty(paths)
if component.UserData.Compact
text = string(paths(1));
elseif numel(paths) == 1
elseif isscalar(paths)
text = "1 file";
else
text = string(numel(paths)) + " files";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
function declarations = collectInteractionDeclarations(obj)
% Class-folder implementation of MatlabPlatformAdapter.collectInteractionDeclarations.
declarations = {};
chunks = cell(1, numel(obj.Plan.Nodes));
for k = 1:numel(obj.Plan.Nodes)
config = obj.Plan.Nodes(k).Configuration;
if isfield(config, "Interactions")
declarations = [declarations config.Interactions];
chunks{k} = config.Interactions;
end
end
populated = ~cellfun("isempty", chunks);
if any(populated)
declarations = [chunks{populated}];
else
declarations = cell(1, 0);
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function list = createFilePanel(obj, node, parent)
function list = createFilePanel(~, node, parent)
% Class-folder implementation of MatlabPlatformAdapter.createFilePanel.
config = node.Configuration;
panel = uipanel(parent, BorderType="line", ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function installContentGrid(obj, node, component)
RowSpacing=policy.ContentSpacing, ...
ColumnSpacing=policy.ContentSpacing);
heights = obj.childRowHeights(node.ChildIds);
singleGrowable = numel(node.ChildIds) == 1 && ...
singleGrowable = isscalar(node.ChildIds) && ...
obj.isGrowableTabChild(obj.node(node.ChildIds(1)));
if node.Kind == "workspacePage"
for k = 1:numel(node.ChildIds)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
function targets = interactionTargetAxes(obj)
% Class-folder implementation of MatlabPlatformAdapter.interactionTargetAxes.
targets = struct("id", {}, "axes", {});
capacity = sum(arrayfun(@(node) numel(node.AxisIds), obj.Plan.Nodes));
targets = repmat(struct("id", "", "axes", []), 1, capacity);
targetCount = 0;
for k = 1:numel(obj.Plan.Nodes)
node = obj.Plan.Nodes(k);
if node.Kind ~= "plotArea"
Expand All @@ -9,11 +11,13 @@
for axisId = node.AxisIds
key = labkit.app.internal.NativeAdapterValues.axisKey(node.Id, axisId);
targetId = key;
if numel(node.AxisIds) == 1
if isscalar(node.AxisIds)
targetId = node.Id;
end
targets(end + 1) = struct( ...
targetCount = targetCount + 1;
targets(targetCount) = struct( ...
"id", targetId, "axes", obj.Axes(char(key)));
end
end
targets = targets(1:targetCount);
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function tf = isGrowableTabChild(obj, node)
% Class-folder implementation of MatlabPlatformAdapter.isGrowableTabChild.
if node.Kind == "section" && numel(node.ChildIds) == 1
if node.Kind == "section" && isscalar(node.ChildIds)
tf = obj.isGrowableTabChild(obj.node(node.ChildIds(1)));
return
end
Expand Down
8 changes: 5 additions & 3 deletions +labkit/+app/+internal/@MatlabPlatformAdapter/nodes.m
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
function selected = nodes(obj, ids)
% Class-folder implementation of MatlabPlatformAdapter.nodes.
selected = repmat(obj.Plan.Nodes(1), 0, 1);
for id = string(ids)
ids = string(ids);
selected = repmat(obj.Plan.Nodes(1), numel(ids), 1);
for selectedIndex = 1:numel(ids)
id = ids(selectedIndex);
index = find(string({obj.Plan.Nodes.Id}) == id, 1);
if isempty(index)
error("labkit:app:runtime:InvariantFailure", ...
"Compiled Layout child is missing: %s.", id);
end
selected(end + 1, 1) = obj.Plan.Nodes(index);
selected(selectedIndex, 1) = obj.Plan.Nodes(index);
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
node.Configuration.Layout == "horizontal";
if node.Kind == "section" && ...
~obj.sectionDrawsOwnTitle(node) && ...
numel(children) == 1
isscalar(children)
height = childHeights(1) + ...
policy.UntitledSectionChromeHeight;
elseif obj.usesAdaptiveActionGrid(node)
Expand Down
31 changes: 22 additions & 9 deletions +labkit/+app/+internal/CompiledDefinition.m
Original file line number Diff line number Diff line change
Expand Up @@ -120,30 +120,41 @@
end

function bindings = collectSignalBindings(nodes, start)
bindings = {};
capacity = sum(cellfun(@(node) numel(node.Signals), nodes)) + ~isempty(start);
bindings = cell(1, capacity);
bindingCount = 0;
for k = 1:numel(nodes)
signals = nodes{k}.Signals;
for s = 1:numel(signals)
bindings{end + 1} = signals{s};
bindingCount = bindingCount + 1;
bindings{bindingCount} = signals{s};
end
end
if ~isempty(start)
bindings{end + 1} = start;
bindingCount = bindingCount + 1;
bindings{bindingCount} = start;
end
bindings = bindings(1:bindingCount);
bindings = uniqueBindings(bindings);
end

function interactions = collectInteractions(nodes)
interactions = {};
chunks = cell(1, numel(nodes));
for k = 1:numel(nodes)
if nodes{k}.Kind ~= "plotArea"
continue;
end
configuration = nodes{k}.configurationForCompiler();
if isfield(configuration, "Interactions")
interactions = [interactions configuration.Interactions];
chunks{k} = configuration.Interactions;
end
end
populated = ~cellfun("isempty", chunks);
if any(populated)
interactions = [chunks{populated}];
else
interactions = cell(1, 0);
end
end

function assertUnique(values, label)
Expand All @@ -154,17 +165,19 @@ function assertUnique(values, label)
end

function values = uniqueBindings(values)
uniqueValues = {};
uniqueValues = cell(size(values));
uniqueCount = 0;
for k = 1:numel(values)
value = values{k};
sameId = find(cellfun(@(candidate) candidate.Id == value.Id, ...
uniqueValues), 1);
uniqueValues(1:uniqueCount)), 1);
if isempty(sameId)
uniqueValues{end + 1} = value;
uniqueCount = uniqueCount + 1;
uniqueValues{uniqueCount} = value;
elseif ~isequaln(uniqueValues{sameId}, value)
error("labkit:app:contract:DuplicateId", ...
"Layout signal ID %s has conflicting callbacks.", value.Id);
end
end
values = uniqueValues;
values = uniqueValues(1:uniqueCount);
end
2 changes: 1 addition & 1 deletion +labkit/+app/+internal/InteractionSpec.m
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
obj.Id, missing(1));
end
result = obj;
if numel(axisIds) == 1
if isscalar(axisIds)
result.Targets = repmat(plotId, size(obj.AxisIds));
else
result.Targets = plotId + "." + obj.AxisIds;
Expand Down
8 changes: 2 additions & 6 deletions +labkit/+app/+internal/LayoutNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@
function obj = workspace(varargin)
content = {};
if ~isempty(varargin) && isa(varargin{1}, "labkit.app.internal.LayoutNode")
content = {varargin{1}};
content = varargin(1);
varargin = varargin(2:end);
end
options = labkit.app.internal.OptionParser.parse( ...
Expand Down Expand Up @@ -541,7 +541,7 @@
labkit.app.internal.LayoutNodeValues.validateChildKinds(content, labkit.app.internal.LayoutNodeValues.workspaceContentKinds(), ...
"workspace page");
pageNode = labkit.app.internal.LayoutNode("workspacePage", id, content, ...
["workspacePage"], {}, [], strings(1, 0), ...
"workspacePage", {}, [], strings(1, 0), ...
struct("Title", labkit.app.internal.LayoutNodeValues.nonemptyText(title, "workspace page title")));
obj.Children{end + 1} = pageNode;
obj.PageIds(end + 1) = id;
Expand Down Expand Up @@ -595,7 +595,3 @@
labkit.app.internal.LayoutNodeValues.optionValue( ...
options, name, defaultValue), name);
end

function state = runAnalysis(state, ~)
state.finished = true;
end
Loading