6363 % Theme caching
6464 ThemeCache_ = [] % Cached DashboardTheme struct ; lazy -computed by getCachedTheme ()
6565 ThemeCachePreset_ = ' ' % Theme preset string that ThemeCache_ was built for
66- % Widget dispatch table
67- WidgetTypeMap_ = [] % containers.Map: type string -> constructor function handle
6866 % Time control
6967 TimePanelHeight = 0.085 % bumped from 0.06 to fit data-range labels below slider (260512-hrn-followup)
7068 DataTimeRange = [0 1 ] % [tMin tMax] across all widget data
162160 end
163161 obj.Layout = DashboardLayout();
164162 obj.Layout.EngineRef = obj ; % Phase 1032 PLOG-VIZ-05 — used by addPlantLogToggle callback
165- obj.WidgetTypeMap_ = containers .Map({ ...
166- ' fastsense' , ' number' , ' status' , ' text' , ...
167- ' gauge' , ' table' , ' rawaxes' , ' timeline' , ...
168- ' group' , ' heatmap' , ' barchart' , ' histogram' , ...
169- ' scatter' , ' image' , ' multistatus' , ' divider' , ...
170- ' iconcard' , ' chipbar' , ' sparkline' }, ...
171- {@FastSenseWidget , @NumberWidget , @StatusWidget , @TextWidget , ...
172- @GaugeWidget , @TableWidget , @RawAxesWidget , @EventTimelineWidget , ...
173- @GroupWidget , @HeatmapWidget , @BarChartWidget , @HistogramWidget , ...
174- @ScatterWidget , @ImageWidget , @MultiStatusWidget , @DividerWidget , ...
175- @IconCardWidget , @ChipBarWidget , @SparklineCardWidget });
163+ % Widget type->constructor dispatch now lives in DashboardWidgetRegistry
164+ % (the single source of truth shared with the serializer + detach paths).
176165 end
177166
178167 function pg = addPage(obj , name )
@@ -393,8 +382,8 @@ function switchPage(obj, pageIdx)
393382 type = ' number' ;
394383 end
395384
396- if isKey( obj .WidgetTypeMap_, type )
397- ctor = obj .WidgetTypeMap_ (type );
385+ if DashboardWidgetRegistry .isRegistered( DashboardWidgetRegistry .resolveAlias( type ) )
386+ ctor = DashboardWidgetRegistry .constructorFor (type );
398387 w = ctor(varargin{: });
399388 else
400389 error(' DashboardEngine:unknownType' , ...
@@ -1188,7 +1177,8 @@ function preview(obj, varargin)
11881177 width = 48 ;
11891178 end
11901179
1191- nWidgets = numel(obj .Widgets);
1180+ ws = obj .activePageWidgets();
1181+ nWidgets = numel(ws );
11921182
11931183 % Empty dashboard
11941184 if nWidgets == 0
@@ -1200,7 +1190,7 @@ function preview(obj, varargin)
12001190 cols = obj .Layout.Columns;
12011191 maxRow = 1 ;
12021192 for i = 1 : nWidgets
1203- p = obj.Widgets {i }.Position;
1193+ p = ws {i }.Position;
12041194 bottomRow = p(2 ) + p(4 ) - 1 ;
12051195 if bottomRow > maxRow
12061196 maxRow = bottomRow ;
@@ -1218,7 +1208,7 @@ function preview(obj, varargin)
12181208
12191209 % Render each widget
12201210 for i = 1 : nWidgets
1221- w = obj.Widgets {i };
1211+ w = ws {i };
12221212 p = w .Position; % [col, row, wCols, hRows]
12231213
12241214 % Character coordinates (1-based)
@@ -1501,13 +1491,44 @@ function removeWidget(obj, idx)
15011491 end
15021492 end
15031493
1494+ function removePage(obj , idx )
1495+ % REMOVEPAGE Remove the page at index idx, keeping ActivePage valid.
1496+ % Mirror of removeWidget for pages. Throws DashboardEngine:invalidIndex
1497+ % on a bad index. Deletes the page's widgets and the page, adjusts
1498+ % ActivePage (decrements when removing a page before it; clamps when
1499+ % removing the active page; resets to 0 when no pages remain), and
1500+ % re-renders when a figure is live.
1501+ if idx < 1 || idx > numel(obj .Pages)
1502+ error(' DashboardEngine:invalidIndex' , ...
1503+ ' Page index %d out of range [1, %d ].' , idx , numel(obj .Pages));
1504+ end
1505+ pg = obj.Pages{idx };
1506+ ws = pg .Widgets;
1507+ for i = 1 : numel(ws )
1508+ delete(ws{i });
1509+ end
1510+ obj .Pages(idx ) = [];
1511+ delete(pg );
1512+ if isempty(obj .Pages)
1513+ obj.ActivePage = 0 ;
1514+ elseif idx < obj .ActivePage
1515+ obj.ActivePage = obj .ActivePage - 1 ;
1516+ elseif idx == obj .ActivePage
1517+ obj.ActivePage = max(1 , min(obj .ActivePage, numel(obj .Pages)));
1518+ end
1519+ if ~isempty(obj .hFigure) && ishandle(obj .hFigure)
1520+ obj .rerenderWidgets();
1521+ end
1522+ end
1523+
15041524 function setWidgetPosition(obj , idx , pos )
15051525 % SETWIDGETPOSITION Set the grid position of a widget by index.
15061526 % Clamps width to grid columns and resolves overlaps with other
1507- % widgets.
1508- if idx < 1 || idx > numel(obj .Widgets)
1527+ % widgets. Operates on the active page in multi-page mode.
1528+ ws = obj .activePageWidgets();
1529+ if idx < 1 || idx > numel(ws )
15091530 error(' DashboardEngine:invalidIndex' , ...
1510- ' Widget index %d out of range [1, %d ].' , idx , numel(obj .Widgets ));
1531+ ' Widget index %d out of range [1, %d ].' , idx , numel(ws ));
15111532 end
15121533 % Clamp to grid bounds
15131534 cols = obj .Layout.Columns;
@@ -1516,25 +1537,28 @@ function setWidgetPosition(obj, idx, pos)
15161537 pos(2 ) = max(1 , pos(2 ));
15171538 pos(4 ) = max(1 , pos(4 ));
15181539 % Resolve overlaps against other widgets
1519- existingPositions = cell(1 , numel(obj .Widgets ) - 1 );
1540+ existingPositions = cell(1 , numel(ws ) - 1 );
15201541 k = 0 ;
1521- for i = 1 : numel(obj .Widgets )
1542+ for i = 1 : numel(ws )
15221543 if i ~= idx
15231544 k = k + 1 ;
1524- existingPositions{k } = obj.Widgets {i }.Position;
1545+ existingPositions{k } = ws {i }.Position;
15251546 end
15261547 end
15271548 pos = obj .Layout.resolveOverlap(pos , existingPositions );
1528- obj.Widgets{idx }.Position = pos ;
1549+ % ws{idx} is a handle, so this mutates the widget stored in the page.
1550+ ws{idx }.Position = pos ;
15291551 end
15301552
15311553 function w = getWidgetByTitle(obj , title )
15321554 % GETWIDGETBYTITLE Find a widget by its Title property.
1533- % Returns the widget object, or empty if not found.
1555+ % Searches every page in multi-page mode (active page or single-page
1556+ % Widgets otherwise). Returns the widget object, or empty if not found.
15341557 w = [];
1535- for i = 1 : numel(obj .Widgets)
1536- if strcmp(obj.Widgets{i }.Title, title )
1537- w = obj.Widgets{i };
1558+ ws = obj .allPageWidgets();
1559+ for i = 1 : numel(ws )
1560+ if strcmp(ws{i }.Title, title )
1561+ w = ws{i };
15381562 return ;
15391563 end
15401564 end
@@ -2293,8 +2317,10 @@ function onLiveTick(obj)
22932317 function markAllDirty(obj )
22942318 % MARKALLDIRTY Flag all widgets as needing refresh.
22952319 % Called on theme change, figure resize, or other global state changes.
2296- for i = 1 : numel(obj .Widgets)
2297- obj.Widgets{i }.markDirty();
2320+ % Covers every page in multi-page mode.
2321+ ws = obj .allPageWidgets();
2322+ for i = 1 : numel(ws )
2323+ ws{i }.markDirty();
22982324 end
22992325 end
23002326
@@ -4473,25 +4499,57 @@ function onFigureDestroyed_(obj)
44734499
44744500 methods (Static )
44754501 function types = widgetTypes()
4476- % WIDGETTYPES List supported widget type strings.
4477- types = {
4478- ' fastsense' , ' Time-series plot (FastSenseWidget)'
4479- ' number' , ' Single numeric value with trend (NumberWidget)'
4480- ' status' , ' Status indicator with dot and label (StatusWidget)'
4481- ' gauge' , ' Gauge display in arc/donut/bar/thermometer style (GaugeWidget)'
4482- ' table' , ' Data table from sensor (TableWidget)'
4483- ' text' , ' Static text block (TextWidget)'
4484- ' timeline' , ' Event timeline display (EventTimelineWidget)'
4485- ' rawaxes' , ' Raw MATLAB axes for custom plotting (RawAxesWidget)'
4486- ' group' , ' Widget container with panel/collapsible/tabbed modes (GroupWidget)'
4487- ' heatmap' , ' Heatmap color grid (HeatmapWidget)'
4488- ' barchart' , ' Bar chart for categories (BarChartWidget)'
4489- ' histogram' , ' Value distribution histogram (HistogramWidget)'
4490- ' scatter' , ' X vs Y scatter plot (ScatterWidget)'
4491- ' image' , ' Static image display (ImageWidget)'
4492- ' multistatus' , ' Multi-sensor status grid (MultiStatusWidget)'
4493- ' divider' , ' Horizontal divider line (DividerWidget)'
4494- };
4502+ % WIDGETTYPES Supported widget types + descriptions, as an Nx2 cell.
4503+ % Derived from DashboardWidgetRegistry.types() (the single source of
4504+ % truth), so it can no longer drift from what addWidget accepts, and
4505+ % user-registered types (registerWidgetType) appear automatically with
4506+ % a generic description.
4507+ descMap = containers .Map( ...
4508+ {' fastsense' , ' number' , ' status' , ' gauge' , ' table' , ' text' , ...
4509+ ' timeline' , ' rawaxes' , ' group' , ' heatmap' , ' barchart' , ...
4510+ ' histogram' , ' scatter' , ' image' , ' multistatus' , ' divider' , ...
4511+ ' iconcard' , ' chipbar' , ' sparkline' }, ...
4512+ {' Time-series plot (FastSenseWidget)' , ...
4513+ ' Single numeric value with trend (NumberWidget)' , ...
4514+ ' Status indicator with dot and label (StatusWidget)' , ...
4515+ ' Gauge display in arc/donut/bar/thermometer style (GaugeWidget)' , ...
4516+ ' Data table from sensor (TableWidget)' , ...
4517+ ' Static text block (TextWidget)' , ...
4518+ ' Event timeline display (EventTimelineWidget)' , ...
4519+ ' Raw MATLAB axes for custom plotting (RawAxesWidget)' , ...
4520+ ' Widget container with panel/collapsible/tabbed modes (GroupWidget)' , ...
4521+ ' Heatmap color grid (HeatmapWidget)' , ...
4522+ ' Bar chart for categories (BarChartWidget)' , ...
4523+ ' Value distribution histogram (HistogramWidget)' , ...
4524+ ' X vs Y scatter plot (ScatterWidget)' , ...
4525+ ' Static image display (ImageWidget)' , ...
4526+ ' Multi-sensor status grid (MultiStatusWidget)' , ...
4527+ ' Horizontal divider line (DividerWidget)' , ...
4528+ ' Icon + value card (IconCardWidget)' , ...
4529+ ' Chip/badge status bar (ChipBarWidget)' , ...
4530+ ' Sparkline value card (SparklineCardWidget)' });
4531+ names = DashboardWidgetRegistry .types();
4532+ types = cell(numel(names ), 2 );
4533+ for i = 1 : numel(names )
4534+ types{i , 1 } = names{i };
4535+ if descMap .isKey(names{i })
4536+ types{i , 2 } = descMap(names{i });
4537+ else
4538+ types{i , 2 } = sprintf(' %s widget' , names{i });
4539+ end
4540+ end
4541+ end
4542+
4543+ function registerWidgetType(type , ctorHandle )
4544+ % REGISTERWIDGETTYPE Register a custom widget type with the dashboard.
4545+ % DashboardEngine.registerWidgetType('mytype', @MyWidget) makes
4546+ % 'mytype' usable through addWidget, serialization, and detach — the
4547+ % documented extension point for third-party widgets. The widget class
4548+ % must subclass DashboardWidget and provide a static fromStruct.
4549+ % Errors DashboardWidgetRegistry:duplicateType on a name collision.
4550+ %
4551+ % See also DashboardWidgetRegistry.register, DashboardEngine.widgetTypes.
4552+ DashboardWidgetRegistry .register(type , ctorHandle );
44954553 end
44964554
44974555 function obj = load(filepath , varargin )
0 commit comments