4848 hSearchLbl_ = [] % "Search:" label
4949 hHeaderLbl_ = [] % "Tags" right-side header label
5050 hLastRefreshLbl_ = [] % "Last refreshed: HH:MM:SS" label (260519-bs4-04 patch)
51+ hPauseBtn_ = [] % "Pause polling"/"Resume polling" uicontrol pushbutton (260519-bs4-05 patch)
52+ PollingActive_ = true % true = RefreshTimer_ running + markTagsDirty live ; false = frozen (260519 - bs4- 05 patch)
5153 hChipsType_ = [] % 1x5 array of uicontrol pushbuttons (Sensor/Monitor/Composite/State/Derived)
5254 hChipsCrit_ = [] % 1x4 array of uicontrol pushbuttons (Low/Medium/High/Safety)
5355 hChipsActivity_ = [] % 1x2 array of uicontrol pushbuttons (Live/Inactive)
@@ -140,17 +142,34 @@ function openWith(obj, registry, theme, companion)
140142 % --- Last-refreshed label (top-left, small muted text). ---
141143 % Style mirrors EventsLogPane.setLastUpdated convention:
142144 % small font, Menlo monospace, PlaceholderTextColor.
145+ % Width reduced to leave room for the Pause/Resume polling
146+ % button on the right edge of the same row (260519-bs4-05).
143147 obj.hLastRefreshLbl_ = uicontrol(obj .hFig_ , ...
144148 ' Style' , ' text' , ...
145149 ' Units' , ' normalized' , ...
146- ' Position' , [0.01 0.945 0.98 0.04 ], ...
150+ ' Position' , [0.01 0.945 0.85 0.04 ], ...
147151 ' String' , ' Last refreshed: --:--:--' , ...
148152 ' HorizontalAlignment' , ' left' , ...
149153 ' BackgroundColor' , t .WidgetBackground , ...
150154 ' ForegroundColor' , t .PlaceholderTextColor , ...
151155 ' FontName' , ' Menlo' , ...
152156 ' FontSize' , 10 );
153157
158+ % --- Pause/Resume polling button (right edge, same row). ---
159+ % Same widget family as the other controls (uicontrol pushbutton)
160+ % so the classical-figure window stays consistent. Initial label
161+ % matches the default PollingActive_=true state ("Pause polling").
162+ % 260519-bs4-05 patch.
163+ obj.hPauseBtn_ = uicontrol(obj .hFig_ , ...
164+ ' Style' , ' pushbutton' , ...
165+ ' Units' , ' normalized' , ...
166+ ' Position' , [0.87 0.945 0.12 0.04 ], ...
167+ ' String' , ' Pause polling' , ...
168+ ' BackgroundColor' , t .WidgetBackground , ...
169+ ' ForegroundColor' , t .ForegroundColor , ...
170+ ' FontSize' , 10 , ...
171+ ' Callback' , @(~,~) obj .setPollingActive(~obj .PollingActive_ ));
172+
154173 % --- Search strip ---
155174 obj.hSearchLbl_ = uicontrol(obj .hFig_ , ...
156175 ' Style' , ' text' , ...
@@ -244,9 +263,13 @@ function openWith(obj, registry, theme, companion)
244263
245264 function markTagsDirty(obj , keys )
246265 % MARKTAGSDIRTY Refresh only rows for the listed tag keys.
247- % keys -- cellstr or single char. No-op when ~IsOpen. Whole body
248- % wrapped in try/catch so a live tick can never crash via this path.
266+ % keys -- cellstr or single char. No-op when ~IsOpen or when
267+ % PollingActive_ is false (paused -> table is frozen, mirroring
268+ % the user's "polling off = nothing moves" mental model;
269+ % 260519-bs4-05 patch). Whole body wrapped in try/catch so a
270+ % live tick can never crash via this path.
249271 if ~obj .IsOpen ; return ; end
272+ if ~obj .PollingActive_ ; return ; end
250273 if isempty(keys ); return ; end
251274 if ischar(keys ); keys = {keys }; end
252275 if ~iscell(keys ); return ; end
@@ -304,6 +327,10 @@ function applyTheme(obj, theme)
304327 obj.hLastRefreshLbl_.BackgroundColor = t .WidgetBackground ;
305328 obj.hLastRefreshLbl_.ForegroundColor = t .PlaceholderTextColor ;
306329 end
330+ if ~isempty(obj .hPauseBtn_ ) && isvalid(obj .hPauseBtn_ )
331+ obj.hPauseBtn_.BackgroundColor = t .WidgetBackground ;
332+ obj.hPauseBtn_.ForegroundColor = t .ForegroundColor ;
333+ end
307334 % Re-apply chip active/inactive styling -- pulls Accent
308335 % from the freshly-stored theme.
309336 obj .applyChipStyles_();
@@ -317,6 +344,74 @@ function applyTheme(obj, theme)
317344 end
318345 end
319346
347+ function setPollingActive(obj , tf )
348+ % SETPOLLINGACTIVE Pause or resume the window's refresh polling.
349+ % setPollingActive(true) -> starts RefreshTimer_ (if not running),
350+ % fires one immediate synchronous
351+ % onRefreshTick_ so the user sees fresh
352+ % data right away, sets the button
353+ % label to 'Pause polling' and drops
354+ % the '(paused)' suffix from the
355+ % header label.
356+ % setPollingActive(false) -> stops RefreshTimer_ (without deleting
357+ % it -- close() still cleans up via
358+ % stopRefreshTimer_), sets the button
359+ % label to 'Resume polling' and adds
360+ % the '(paused)' suffix to the header
361+ % label so the user sees WHEN the
362+ % polling stopped.
363+ %
364+ % While paused, markTagsDirty() is a no-op: the table is frozen.
365+ % No-op when ~IsOpen. Whole body wrapped in try/catch so a stray
366+ % click cannot crash the window. 260519-bs4-05 patch.
367+ if ~obj .IsOpen ; return ; end
368+ if ~islogical(tf ) || ~isscalar(tf )
369+ error(' FastSenseCompanion:tagStatusTableInvalidPollingFlag' , ...
370+ ' setPollingActive requires a scalar logical argument.' );
371+ end
372+ try
373+ obj.PollingActive_ = tf ;
374+ if tf
375+ % Resume: restart timer (if it died, recreate via
376+ % startRefreshTimer_; if it just stopped, start() it).
377+ if ~isempty(obj .RefreshTimer_ ) && isvalid(obj .RefreshTimer_ )
378+ try
379+ if strcmp(get(obj .RefreshTimer_ , ' Running' ), ' off' )
380+ start(obj .RefreshTimer_ );
381+ end
382+ catch
383+ % If start() fails (e.g. timer in a weird state),
384+ % rebuild it cleanly.
385+ obj .startRefreshTimer_();
386+ end
387+ else
388+ obj .startRefreshTimer_();
389+ end
390+ % Immediate one-shot refresh on resume so the user sees
391+ % freshness right away rather than waiting up to
392+ % RefreshPeriod_ seconds for the timer tick.
393+ obj .onRefreshTick_();
394+ else
395+ % Pause: stop the timer but DO NOT delete it -- we want
396+ % to be able to re-start the same timer on resume.
397+ % Close-path teardown still runs stopRefreshTimer_
398+ % regardless of paused state.
399+ if ~isempty(obj .RefreshTimer_ ) && isvalid(obj .RefreshTimer_ )
400+ try
401+ if strcmp(get(obj .RefreshTimer_ , ' Running' ), ' on' )
402+ stop(obj .RefreshTimer_ );
403+ end
404+ catch
405+ % Best-effort; never throw out of a UI click.
406+ end
407+ end
408+ end
409+ obj .refreshPauseUi_();
410+ catch
411+ % UI click handler must never throw.
412+ end
413+ end
414+
320415 function close(obj )
321416 % CLOSE Programmatic close; routes through onCloseRequest_ for parity.
322417 if obj .IsOpen && ~isempty(obj .hFig_ ) && isvalid(obj .hFig_ )
@@ -370,6 +465,26 @@ function tickForTest(obj)
370465 obj .onRefreshTick_();
371466 end
372467
468+ function s = pauseBtnLabelForTest(obj )
469+ % PAUSEBTNLABELFORTEST Test helper: read the Pause/Resume button text.
470+ % Returns '' when the window is detached or the button is invalid.
471+ % 260519-bs4-05 patch.
472+ s = ' ' ;
473+ if ~isempty(obj .hPauseBtn_ ) && isvalid(obj .hPauseBtn_ )
474+ s = obj .hPauseBtn_ .String ;
475+ end
476+ end
477+
478+ function t = refreshTimerForTest(obj )
479+ % REFRESHTIMERFORTEST Test helper: return the underlying RefreshTimer_.
480+ % Returns [] when the window is detached or the timer is invalid.
481+ % 260519-bs4-05 patch.
482+ t = [];
483+ if ~isempty(obj .RefreshTimer_ ) && isvalid(obj .RefreshTimer_ )
484+ t = obj .RefreshTimer_ ;
485+ end
486+ end
487+
373488 end
374489
375490 methods (Access = private )
@@ -410,6 +525,7 @@ function onCloseRequest_(obj)
410525 obj.hSearchLbl_ = [];
411526 obj.hHeaderLbl_ = [];
412527 obj.hLastRefreshLbl_ = [];
528+ obj.hPauseBtn_ = [];
413529 obj.hChipsType_ = [];
414530 obj.hChipsCrit_ = [];
415531 obj.hChipsActivity_ = [];
@@ -611,7 +727,12 @@ function onActivityChip_(obj, key)
611727 function setLastRefreshedNow_(obj )
612728 % SETLASTREFRESHEDNOW_ Update the "Last refreshed: HH:MM:SS" label to now.
613729 % 24h clock, second precision, local time. No-op when the label
614- % is invalid (window detached). 260519-bs4-04 patch.
730+ % is invalid (window detached). When paused, appends " (paused)"
731+ % suffix so the user sees the freshness state -- but the timer
732+ % does not tick while paused, so this branch is only reached
733+ % from the synchronous resume path (where the suffix is dropped
734+ % right after by refreshPauseUi_) and from defensive callers.
735+ % 260519-bs4-04 patch; paused-suffix added in 260519-bs4-05.
615736 if isempty(obj .hLastRefreshLbl_ ) || ~isvalid(obj .hLastRefreshLbl_ )
616737 return ;
617738 end
@@ -621,7 +742,44 @@ function setLastRefreshedNow_(obj)
621742 % Octave / stripped MATLAB fallback.
622743 ts = datestr(now , ' HH:MM:SS' ); % #ok<DATST,TNOW1>
623744 end
624- obj.hLastRefreshLbl_.String = sprintf(' Last refreshed: %s ' , ts );
745+ if obj .PollingActive_
746+ obj.hLastRefreshLbl_.String = sprintf(' Last refreshed: %s ' , ts );
747+ else
748+ obj.hLastRefreshLbl_.String = sprintf(' Last refreshed: %s (paused)' , ts );
749+ end
750+ end
751+
752+ function refreshPauseUi_(obj )
753+ % REFRESHPAUSEUI_ Sync the Pause/Resume button label and header suffix.
754+ % Called from setPollingActive after PollingActive_ flips. Does
755+ % NOT update the "Last refreshed" timestamp -- it only rewrites
756+ % the suffix in-place so the previous HH:MM:SS is preserved
757+ % (the user can see WHEN the polling stopped, per the spec).
758+ % 260519-bs4-05 patch.
759+ % --- Button label ---
760+ if ~isempty(obj .hPauseBtn_ ) && isvalid(obj .hPauseBtn_ )
761+ if obj .PollingActive_
762+ obj.hPauseBtn_.String = ' Pause polling' ;
763+ else
764+ obj.hPauseBtn_.String = ' Resume polling' ;
765+ end
766+ end
767+ % --- Header label "(paused)" suffix maintenance ---
768+ if isempty(obj .hLastRefreshLbl_ ) || ~isvalid(obj .hLastRefreshLbl_ )
769+ return ;
770+ end
771+ cur = obj .hLastRefreshLbl_ .String ;
772+ if ~ischar(cur )
773+ return ;
774+ end
775+ hasSuffix = ~isempty(regexp(cur , ' \(paused\)\s*$' , ' once' ));
776+ if obj .PollingActive_ && hasSuffix
777+ % Drop the trailing " (paused)".
778+ obj.hLastRefreshLbl_.String = regexprep(cur , ' \s*\(paused\)\s*$' , ' ' );
779+ elseif ~obj .PollingActive_ && ~hasSuffix
780+ % Add the " (paused)" suffix to the existing timestamp.
781+ obj.hLastRefreshLbl_.String = [strtrim(cur ), ' (paused)' ];
782+ end
625783 end
626784
627785 function startRefreshTimer_(obj )
0 commit comments