5151 hLeftPanel_ = [] % left pane uipanel
5252 hMidPanel_ = [] % middle pane uipanel
5353 hRightPanel_ = [] % right pane uipanel
54+ hLogPanel_ = [] % bottom log uipanel (full-width)
55+ hLogText_ = [] % uitextarea inside hLogPanel_ (newest line first)
5456 Theme_ = [] % resolved CompanionTheme struct
5557 Listeners_ = {} % all addlistener return values ; deleted on close
5658 CatalogPane_ = [] % TagCatalogPane instance
132134 ' Visible' , ' off' );
133135 obj.hFig_.Color = obj .Theme_ .DashboardBackground ;
134136
135- % Step 8 — Root grid
136- obj.hLayout_ = uigridlayout(obj .hFig_ , [1 3 ]);
137+ % Step 8 — Root grid (2 rows: top = 3 panes, bottom = log strip)
138+ obj.hLayout_ = uigridlayout(obj .hFig_ , [2 3 ]);
137139 obj.hLayout_.ColumnWidth = {220 , ' 1x' , 280 };
138- obj.hLayout_.RowHeight = {' 1x' };
140+ obj.hLayout_.RowHeight = {' 1x' , 140 };
139141 obj.hLayout_.Padding = [24 24 24 24 ];
140142 obj.hLayout_.ColumnSpacing = 16 ;
141- obj.hLayout_.RowSpacing = 0 ;
143+ obj.hLayout_.RowSpacing = 12 ;
142144 obj.hLayout_.BackgroundColor = obj .Theme_ .DashboardBackground ;
143145
144- % Step 9 — Three uipanels (order matters: grid assigns col 1, 2, 3)
146+ % Step 9 — Three uipanels in row 1 + log panel spanning row 2.
145147 obj.hLeftPanel_ = uipanel(obj .hLayout_ );
148+ obj.hLeftPanel_.Layout.Row = 1 ; obj.hLeftPanel_.Layout.Column = 1 ;
146149 obj.hMidPanel_ = uipanel(obj .hLayout_ );
150+ obj.hMidPanel_.Layout.Row = 1 ; obj.hMidPanel_.Layout.Column = 2 ;
147151 obj.hRightPanel_ = uipanel(obj .hLayout_ );
152+ obj.hRightPanel_.Layout.Row = 1 ; obj.hRightPanel_.Layout.Column = 3 ;
153+ obj.hLogPanel_ = uipanel(obj .hLayout_ );
154+ obj.hLogPanel_.Layout.Row = 2 ; obj.hLogPanel_.Layout.Column = [1 3 ];
148155
149156 % Apply panel styling from theme
150- for hp = {obj .hLeftPanel_ , obj .hMidPanel_ , obj .hRightPanel_ }
157+ for hp = {obj .hLeftPanel_ , obj .hMidPanel_ , obj .hRightPanel_ , obj . hLogPanel_ }
151158 hp{1 }.BackgroundColor = obj .Theme_ .WidgetBackground ;
152159 hp{1 }.BorderColor = obj .Theme_ .WidgetBorderColor ;
153160 hp{1 }.BorderType = ' line' ;
154161 hp{1 }.BorderWidth = 1 ;
155162 end
156163
164+ % Build log strip (Header + uitextarea in a 2-row inner grid)
165+ obj .buildLogStrip_();
166+
157167 % Step 10 — Instantiate pane objects and attach
158168 obj.CatalogPane_ = TagCatalogPane();
159169 obj.ListPane_ = DashboardListPane();
@@ -376,6 +386,31 @@ function removeDashboard(obj, key)
376386 end
377387 end
378388
389+ function addLogEntry(obj , level , msg )
390+ % ADDLOGENTRY Append a timestamped log line to the bottom log strip.
391+ % level — 'info' | 'warn' | 'error' (any short tag accepted)
392+ % msg — char/string. Anything else is sprintf'd through %s.
393+ % Newest line is at the top so the user always sees the latest
394+ % without scrolling. Buffer capped at 500 lines.
395+ if isempty(obj .hLogText_ ) || ~isvalid(obj .hLogText_ ); return ; end
396+ try
397+ ts = char(datetime(' now' , ' Format' , ' HH:mm:ss' ));
398+ if isstring(msg ) && isscalar(msg ); msg = char(msg ); end
399+ if ~ischar(msg ); msg = sprintf(' %s ' , msg ); end
400+ line = sprintf(' [%s ] %-5s %s ' , ts , upper(char(level )), msg );
401+ cur = obj .hLogText_ .Value ;
402+ if isempty(cur ) || (iscell(cur ) && numel(cur )==1 && isempty(cur{1 }))
403+ cur = {};
404+ end
405+ if ~iscell(cur ); cur = {cur }; end
406+ cur = [{line }, reshape(cur , 1 , [])];
407+ if numel(cur ) > 500 ; cur = cur(1 : 500 ); end
408+ obj.hLogText_.Value = cur ;
409+ catch
410+ % Logging must never crash the UI.
411+ end
412+ end
413+
379414 function refreshCatalog(obj )
380415 % REFRESHCATALOG Re-snapshot tags from registry and rebuild the tag catalog.
381416 % Call after externally mutating TagRegistry to update the visible catalog.
@@ -391,6 +426,31 @@ function refreshCatalog(obj)
391426
392427 methods (Access = private )
393428
429+ function buildLogStrip_(obj )
430+ % BUILDLOGSTRIP_ Construct header label + uitextarea inside hLogPanel_.
431+ t = obj .Theme_ ;
432+ g = uigridlayout(obj .hLogPanel_ , [2 1 ]);
433+ g.RowHeight = {18 , ' 1x' };
434+ g.ColumnWidth = {' 1x' };
435+ g.Padding = [8 4 8 4 ];
436+ g.RowSpacing = 4 ;
437+ g.BackgroundColor = t .WidgetBackground ;
438+ hLbl = uilabel(g );
439+ hLbl.Layout.Row = 1 ; hLbl.Layout.Column = 1 ;
440+ hLbl.Text = ' Log' ; hLbl.FontWeight = ' bold' ; hLbl.FontSize = 11 ;
441+ hLbl.FontColor = t .ForegroundColor ;
442+ hLbl.HorizontalAlignment = ' left' ; hLbl.VerticalAlignment = ' center' ;
443+ obj.hLogText_ = uitextarea(g );
444+ obj.hLogText_.Layout.Row = 2 ; obj.hLogText_.Layout.Column = 1 ;
445+ obj.hLogText_.Editable = ' off' ;
446+ obj.hLogText_.FontName = ' Menlo' ;
447+ obj.hLogText_.FontSize = 10 ;
448+ obj.hLogText_.BackgroundColor = t .DashboardBackground ;
449+ obj.hLogText_.FontColor = t .ForegroundColor ;
450+ obj.hLogText_.Value = {sprintf(' [%s ] INFO Companion ready.' , ...
451+ char(datetime(' now' , ' Format' , ' HH:mm:ss' )))};
452+ end
453+
394454 function applyPlaceholderColors_(obj )
395455 % APPLYPLACEHOLDERCOLORS_ Set FontColor on all placeholder uilabels.
396456 % Called after attach() on each pane so the theme color is applied.
@@ -414,7 +474,10 @@ function onDashboardSelected_(obj, ~, ed)
414474 obj.SelectedDashboardIdx_ = ed .Index ;
415475 obj.LastInteraction_ = ' dashboard' ;
416476 obj .resolveInspectorState_();
477+ obj .addLogEntry(' info' , sprintf(' Selected dashboard: %s ' , ...
478+ char(ed .Engine .Name )));
417479 catch err
480+ obj .addLogEntry(' error' , sprintf(' Dashboard select failed: %s ' , err .message ));
418481 uialert(obj .hFig_ , err .message , ' FastSense Companion' );
419482 end
420483 end
@@ -428,7 +491,10 @@ function onOpenDashboardRequested_(obj, ~, ed)
428491 obj.SelectedDashboardIdx_ = ed .Index ;
429492 obj.LastInteraction_ = ' dashboard' ;
430493 obj .resolveInspectorState_();
494+ obj .addLogEntry(' info' , sprintf(' Opened dashboard: %s ' , ...
495+ char(ed .Engine .Name )));
431496 catch err
497+ obj .addLogEntry(' error' , sprintf(' Open dashboard failed: %s ' , err .message ));
432498 uialert(obj .hFig_ , err .message , ' FastSense Companion' );
433499 end
434500 end
@@ -441,7 +507,18 @@ function onTagSelectionChanged_(obj, ~, ~)
441507 obj.SelectedTagKeys_ = obj .CatalogPane_ .getSelectedKeys();
442508 obj.LastInteraction_ = ' tags' ;
443509 obj .resolveInspectorState_();
510+ if isempty(obj .SelectedTagKeys_ )
511+ obj .addLogEntry(' info' , ' Tag selection cleared' );
512+ elseif numel(obj .SelectedTagKeys_ ) == 1
513+ obj .addLogEntry(' info' , sprintf(' Selected tag: %s ' , ...
514+ char(obj.SelectedTagKeys_{1 })));
515+ else
516+ obj .addLogEntry(' info' , sprintf(' Selected %d tags: %s ' , ...
517+ numel(obj .SelectedTagKeys_ ), ...
518+ strjoin(obj .SelectedTagKeys_ , ' , ' )));
519+ end
444520 catch err
521+ obj .addLogEntry(' error' , sprintf(' Tag select failed: %s ' , err .message ));
445522 uialert(obj .hFig_ , err .message , ' FastSense Companion' );
446523 end
447524 end
@@ -530,14 +607,21 @@ function onOpenAdHocPlotRequested_(obj, ~, evt)
530607 end
531608 end
532609 [~ , skipped ] = openAdHocPlot(tags , mode , obj .Theme );
610+ obj .addLogEntry(' info' , sprintf( ...
611+ ' Opened ad-hoc plot: %d tag(s) [%s ]' , ...
612+ numel(tags ), char(mode )));
533613 if ~isempty(skipped )
614+ obj .addLogEntry(' warn' , sprintf( ...
615+ ' Ad-hoc plot skipped %d tag(s): %s ' , ...
616+ numel(skipped ), strjoin(skipped , ' , ' )));
534617 msg = sprintf( ...
535618 ' Plot opened, but some tags were skipped:\n - %s ' , ...
536619 strjoin(skipped , sprintf(' \n - ' )));
537620 uialert(obj .hFig_ , msg , ' FastSense Companion' , ...
538621 ' Icon' , ' warning' );
539622 end
540623 catch ME
624+ obj .addLogEntry(' error' , sprintf(' Ad-hoc plot failed: %s ' , ME .message ));
541625 if ~isempty(obj .hFig_ ) && isvalid(obj .hFig_ )
542626 uialert(obj .hFig_ , ...
543627 sprintf(' Failed to open plot: %s ' , ME .message ), ...
0 commit comments