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
5 changes: 5 additions & 0 deletions demo/industrial_plant/private/buildDashboard.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
engine = DashboardEngine('FastSense Industrial Plant Demo', ...
'Theme', 'light', 'LiveInterval', 1.0);

% Wire EventStore so the per-widget "+" Create-Event button on every
% FastSenseWidget writes manual annotations into the same store the
% Events page (EventTimelineWidget) reads from (260513-snt).
engine.EventStore = ctx.store;

engine.addPage('Overview');
engine.addPage('Feed Line');
engine.addPage('Reactor');
Expand Down
548 changes: 548 additions & 0 deletions libs/Dashboard/CreateEventDialog.m

Large diffs are not rendered by default.

172 changes: 172 additions & 0 deletions libs/Dashboard/DashboardEngine.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
% shift down by BannerHeight. Single source of truth for the banner
% strip height (260508-jyh).
BannerHeight = 0.035
% 260513-snt — EventStore handle for manual-event creation via the
% per-FastSenseWidget '+Event' button. Defaults []; lazily
% auto-discovered from any EventTimelineWidget exposing
% EventStoreObj on the dashboard the first time the dialog is
% opened. Runtime handle only — NOT written through
% DashboardSerializer; serialized dashboards round-trip unchanged.
EventStore = []
end

properties (SetAccess = private)
Expand Down Expand Up @@ -422,6 +429,8 @@ function render(obj)
obj.Layout.ContentArea = [0, effTimeH, ...
1, 1 - obj.BannerHeight - effToolbarH - effPageBarH - effTimeH];
obj.Layout.DetachCallback = @(w) obj.detachWidget(w);
% 260513-snt — wire the per-FastSenseWidget '+Event' button.
obj.Layout.CreateEventCallback = @(w) obj.openCreateEventDialog_(w);
% Create viewport once up front — additive allocatePanels calls below
% will reuse it rather than destroying and recreating it each time.
obj.Layout.ensureViewport(obj.hFigure, themeStruct);
Expand Down Expand Up @@ -1340,6 +1349,8 @@ function rerenderWidgets(obj)
obj.Progress_ = [];
% Re-wire detach callback after panel recreation (Pitfall 3 in RESEARCH.md)
obj.Layout.DetachCallback = @(w) obj.detachWidget(w);
% 260513-snt — re-wire Create-Event callback for the same reason.
obj.Layout.CreateEventCallback = @(w) obj.openCreateEventDialog_(w);
end

function updateGlobalTimeRange(obj)
Expand Down Expand Up @@ -2210,6 +2221,65 @@ function broadcastTimeRangeNow(obj, tStart, tEnd)
ws = [ws, obj.Pages{i}.Widgets]; %#ok<AGROW>
end
end

function notifyEventsChanged(obj)
%NOTIFYEVENTSCHANGED Refresh all event-aware widgets after store mutation (260513-snt).
% Called after CreateEventDialog persists a new event. Walks the
% active page (recursing into GroupWidget children via
% getNestedWidgets) and refreshes every EventTimelineWidget and
% FastSenseWidget. Also re-aggregates the slider event-marker
% overlay via computeEventMarkers and the slider preview lines via
% computePreviewEnvelope so a freshly-added event becomes visible
% on the slider strip without waiting for the next live tick.
%
% Per-widget refresh() calls are wrapped in try/catch so a single
% broken widget does not kill the sweep. The outer call is also
% wrapped so the dialog's Save handler can swallow non-fatal
% failures without leaving the dialog in a half-saved state.
%
% Errors namespaced DashboardEngine:notifyEventsChangedFailed.
try
ws = obj.activePageWidgets();
flat = obj.flattenEventAwareWidgets_(ws);
for i = 1:numel(flat)
w = flat{i};
if ~isa(w, 'EventTimelineWidget') && ~isa(w, 'FastSenseWidget')
continue;
end
if ~w.Realized, continue; end
if isempty(w.hPanel) || ~ishandle(w.hPanel), continue; end
try
w.refresh();
catch ME
warning('DashboardEngine:notifyEventsChangedFailed', ...
'Widget "%s" refresh failed: %s', w.Title, ME.message);
end
end
% Re-aggregate slider event markers + preview lines so the
% new event shows up on the bottom strip. computeEventMarkers
% / computePreviewEnvelope are no-ops without a
% TimeRangeSelector, so safe to call before render too.
try
obj.computeEventMarkers();
catch ME
if obj.DebugPreview_
warning('DashboardEngine:notifyEventsChangedFailed', ...
'computeEventMarkers failed: %s', ME.message);
end
end
try
obj.computePreviewEnvelope();
catch ME
if obj.DebugPreview_
warning('DashboardEngine:notifyEventsChangedFailed', ...
'computePreviewEnvelope failed: %s', ME.message);
end
end
catch ME
warning('DashboardEngine:notifyEventsChangedFailed', ...
'notifyEventsChanged failed: %s', ME.message);
end
end
end

methods (Access = private)
Expand Down Expand Up @@ -2346,6 +2416,108 @@ function removeDetachedByRef(obj, mirrorHolder)
end
end

function flat = flattenEventAwareWidgets_(obj, widgets, depth)
%FLATTENEVENTAWAREWIDGETS_ Flatten widget tree for event-aware refresh sweep (260513-snt).
% Mirrors flattenWidgetsForPreview_ but kept as a separate helper
% for clarity at the call site — notifyEventsChanged iterates this
% flat list to call refresh() on EventTimelineWidget /
% FastSenseWidget instances regardless of GroupWidget nesting.
% Defensive depth cap of 10 mirrors flattenWidgetsForPreview_.
if nargin < 3, depth = 0; end
flat = {};
if depth >= 10
flat = widgets;
return;
end
for i = 1:numel(widgets)
w = widgets{i};
nested = {};
try
nested = w.getNestedWidgets();
catch
nested = {};
end
if isempty(nested)
flat = [flat, {w}]; %#ok<AGROW>
else
flat = [flat, obj.flattenEventAwareWidgets_(nested, depth + 1)]; %#ok<AGROW>
end
end
end

function store = resolveEventStore_(obj)
%RESOLVEEVENTSTORE_ Return the engine's EventStore, auto-discovering it if unset (260513-snt).
% Strategy: if obj.EventStore is already set, return it. Otherwise
% walk obj.allPageWidgets() (recursing into GroupWidget children
% via flattenEventAwareWidgets_) for the first
% EventTimelineWidget with a non-empty EventStoreObj, cache that
% handle onto obj.EventStore, and return it. Returns [] when
% nothing was found — caller is responsible for surfacing the
% no-store error to the user.
%
% Note: when multiple EventTimelineWidgets bind different stores,
% the first one walked wins. Setting engine.EventStore explicitly
% is the user's escape hatch.
if ~isempty(obj.EventStore)
store = obj.EventStore;
return;
end
store = [];
ws = obj.allPageWidgets();
flat = obj.flattenEventAwareWidgets_(ws);
for i = 1:numel(flat)
w = flat{i};
if isa(w, 'EventTimelineWidget') && ~isempty(w.EventStoreObj)
obj.EventStore = w.EventStoreObj;
store = obj.EventStore;
return;
end
end
end

function openCreateEventDialog_(obj, widget)
%OPENCREATEEVENTDIALOG_ Entry point invoked by the FastSenseWidget '+Event' button.
% 260513-snt shipped this as a modal dialog. 260513-v69 supersedes
% that trigger with a two-click pick-on-chart flow:
% 1. Resolve EventStore via resolveEventStore_ (auto-discovery
% from EventTimelineWidget if obj.EventStore is empty).
% 2. If no store: non-blocking errordlg, return.
% 3. Otherwise: hand off to widget.FastSenseObj.startEventPick_(obj).
% The FastSense instance owns the state machine; this engine
% method only gates on store availability and forwards.
% The CreateEventDialog class remains importable as a programmatic
% API (e.g., CreateEventDialog(widget, engine)) but is no longer the
% default '+' button entry point. The persistEventStatic helper is
% still the single source of truth for persistence and is reused by
% FastSense.completeEventPick_.
try
if ~isa(widget, 'FastSenseWidget')
warning('DashboardEngine:openCreateEventDialogFailed', ...
'openCreateEventDialog_ requires a FastSenseWidget; got %s.', class(widget));
return;
end
fs = widget.FastSenseObj;
if isempty(fs) || ~isa(fs, 'FastSense') || ~fs.IsRendered
warning('DashboardEngine:openCreateEventDialogFailed', ...
'FastSenseWidget has no rendered FastSense instance.');
return;
end
store = obj.resolveEventStore_();
if isempty(store)
msg = ['No EventStore is bound to this dashboard. ', ...
'Set engine.EventStore = EventStore(path) ', ...
'or add an EventTimelineWidget with ', ...
'EventStoreObj set to enable event creation.'];
errordlg(msg, 'Create Event');
return;
end
fs.startEventPick_(obj);
catch ME
warning('DashboardEngine:openCreateEventDialogFailed', ...
'openCreateEventDialog_ failed: %s', ME.message);
end
end

function renderPageBar(obj, themeStruct)
%RENDERPAGEBAR Create the PageBar uipanel with one button per page.
% Called from render() when numel(Pages) > 1. Y accounts for the
Expand Down
Loading
Loading