From 81f663e5839c9341f1bb0dc2cf35d75ce37df610 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:21:16 -0500 Subject: [PATCH 1/5] Allow adding widgets while disconnected from NT --- lib/pages/dashboard/add_widget_dialog.dart | 21 +++ lib/services/nt_widget_registry.dart | 16 +- .../dialog_widgets/layout_drag_tile.dart | 6 + .../dialog_widgets/nt_widget_drag_tile.dart | 155 ++++++++++++++++++ .../nt_widgets/multi_topic/accelerometer.dart | 13 +- .../multi_topic/basic_swerve_drive.dart | 68 +++++--- .../nt_widgets/multi_topic/camera_stream.dart | 11 +- .../multi_topic/combo_box_chooser.dart | 39 +++-- .../multi_topic/command_scheduler.dart | 25 ++- .../multi_topic/command_widget.dart | 24 ++- .../multi_topic/differential_drive.dart | 17 +- .../multi_topic/encoder_widget.dart | 14 +- .../field_widget/field_widget.dart | 3 +- .../field_widget/field_widget_model.dart | 13 +- .../nt_widgets/multi_topic/fms_info.dart | 46 ++++-- lib/widgets/nt_widgets/multi_topic/gyro.dart | 10 +- .../multi_topic/motor_controller.dart | 11 +- .../multi_topic/network_alerts.dart | 25 ++- .../multi_topic/pid_controller.dart | 32 ++-- .../multi_topic/power_distribution.dart | 34 ++-- .../multi_topic/profiled_pid_controller.dart | 32 ++-- .../nt_widgets/multi_topic/relay_widget.dart | 11 +- .../multi_topic/robot_preferences.dart | 2 +- .../multi_topic/split_button_chooser.dart | 39 +++-- .../multi_topic/subsystem_widget.dart | 20 ++- .../multi_topic/three_axis_accelerometer.dart | 25 ++- .../nt_widgets/multi_topic/ultrasonic.dart | 11 +- .../multi_topic/yagsl_swerve_drive.dart | 53 +++--- lib/widgets/nt_widgets/nt_widget.dart | 66 ++++---- .../nt_widgets/single_topic/boolean_box.dart | 3 +- .../single_topic/large_text_display.dart | 2 +- .../nt_widgets/single_topic/match_time.dart | 2 +- .../single_topic/multi_color_view.dart | 2 +- .../nt_widgets/single_topic/number_bar.dart | 2 +- .../single_topic/number_slider.dart | 4 +- .../nt_widgets/single_topic/radial_gauge.dart | 2 +- .../single_topic/single_color_view.dart | 2 +- .../nt_widgets/single_topic/text_display.dart | 10 +- .../single_topic/toggle_button.dart | 10 +- .../single_topic/toggle_switch.dart | 2 +- .../nt_widgets/single_topic/voltage_view.dart | 2 +- 41 files changed, 623 insertions(+), 262 deletions(-) create mode 100644 lib/widgets/dialog_widgets/nt_widget_drag_tile.dart diff --git a/lib/pages/dashboard/add_widget_dialog.dart b/lib/pages/dashboard/add_widget_dialog.dart index 11d2ae0b3..b332304b8 100644 --- a/lib/pages/dashboard/add_widget_dialog.dart +++ b/lib/pages/dashboard/add_widget_dialog.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:popover/popover.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:elastic_dashboard/services/nt_connection.dart'; +import 'package:elastic_dashboard/services/nt_widget_registry.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_text_input.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_toggle_switch.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/layout_drag_tile.dart'; +import 'package:elastic_dashboard/widgets/dialog_widgets/nt_widget_drag_tile.dart'; import 'package:elastic_dashboard/widgets/draggable_containers/models/layout_container_model.dart'; import 'package:elastic_dashboard/widgets/draggable_containers/models/widget_container_model.dart'; import 'package:elastic_dashboard/widgets/draggable_dialog.dart'; @@ -114,6 +117,24 @@ class _AddWidgetDialogState extends State { ), ListView( children: [ + ExpansionTile( + title: const Text('Network Tables Widgets'), + childrenPadding: const EdgeInsets.only(left: 16), + children: NTWidgetRegistry.registeredWidgetNames + .sorted((a, b) => a.compareTo(b)) + .map( + (widgetName) => NTWidgetDragTile( + ntConnection: widget.ntConnection, + preferences: widget.preferences, + gridIndex: widget.gridIndex, + widgetName: widgetName, + onDragUpdate: widget.onNTDragUpdate, + onDragEnd: widget.onNTDragEnd, + onRemoveWidget: () => onRemove(widget.grid), + ), + ) + .toList(), + ), LayoutDragTile( gridIndex: widget.gridIndex, title: 'List Layout', diff --git a/lib/services/nt_widget_registry.dart b/lib/services/nt_widget_registry.dart index 3e204e3c2..9eadbfb23 100644 --- a/lib/services/nt_widget_registry.dart +++ b/lib/services/nt_widget_registry.dart @@ -58,7 +58,7 @@ typedef NTModelProvider = T Function({ required NTConnection ntConnection, required SharedPreferences preferences, - required String topic, + required String? topic, NT4Type? dataType, double? period, NT4StructMeta? ntStructMeta, @@ -68,13 +68,15 @@ typedef MultiTopicNTModelProvider = T Function({ required NTConnection ntConnection, required SharedPreferences preferences, - required String topic, + required String? topic, double? period, }); typedef NTWidgetProvider = T Function({Key? key}); class NTWidgetRegistry { + static final List _registeredNames = []; + static final Map _widgetNameBuildMap = {}; static final Map _modelNameBuildMap = {}; @@ -89,6 +91,9 @@ class NTWidgetRegistry { static const double _normalSize = 128.0; + static List get registeredWidgetNames => + _registeredNames.toList(growable: false); + NTWidgetRegistry._(); static bool _initialized = false; @@ -442,7 +447,7 @@ class NTWidgetRegistry { SharedPreferences preferences, NT4StructMeta? ntStructMeta, String type, - String topic, { + String? topic, { NT4Type? dataType, double? period, }) { @@ -617,7 +622,7 @@ class NTWidgetRegistry { ({ required NTConnection ntConnection, required SharedPreferences preferences, - required String topic, + required String? topic, double? period, NT4Type? dataType, NT4StructMeta? ntStructMeta, @@ -649,7 +654,7 @@ class NTWidgetRegistry { ({ required NTConnection ntConnection, required SharedPreferences preferences, - required String topic, + required String? topic, double? period, NT4Type? dataType, NT4StructMeta? ntStructMeta, @@ -719,6 +724,7 @@ class NTWidgetRegistry { required double? defaultWidth, required double? defaultHeight, }) { + _registeredNames.add(names.first); for (String name in names) { register( name: name, diff --git a/lib/widgets/dialog_widgets/layout_drag_tile.dart b/lib/widgets/dialog_widgets/layout_drag_tile.dart index 3c5a3a889..cb27246c3 100644 --- a/lib/widgets/dialog_widgets/layout_drag_tile.dart +++ b/lib/widgets/dialog_widgets/layout_drag_tile.dart @@ -1,6 +1,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + import 'package:elastic_dashboard/widgets/draggable_containers/models/layout_container_model.dart'; import 'package:elastic_dashboard/widgets/gesture/drag_listener.dart'; @@ -67,6 +69,10 @@ class _LayoutDragTileState extends State { Widget build(BuildContext context) => InkWell( onTap: () {}, child: DragListener( + overrideVertical: false, + supportedDevices: PointerDeviceKind.values + .whereNot((element) => element == PointerDeviceKind.trackpad) + .toSet(), onDragStart: (details) { if (draggingWidget != null) { return; diff --git a/lib/widgets/dialog_widgets/nt_widget_drag_tile.dart b/lib/widgets/dialog_widgets/nt_widget_drag_tile.dart new file mode 100644 index 000000000..1e2343e12 --- /dev/null +++ b/lib/widgets/dialog_widgets/nt_widget_drag_tile.dart @@ -0,0 +1,155 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:elastic_dashboard/services/nt4_client.dart'; +import 'package:elastic_dashboard/services/nt_connection.dart'; +import 'package:elastic_dashboard/services/nt_widget_registry.dart'; +import 'package:elastic_dashboard/widgets/draggable_containers/models/nt_widget_container_model.dart'; +import 'package:elastic_dashboard/widgets/draggable_containers/models/widget_container_model.dart'; +import 'package:elastic_dashboard/widgets/gesture/drag_listener.dart'; +import 'package:elastic_dashboard/widgets/nt_widgets/nt_widget.dart'; + +class NTWidgetDragTile extends StatefulWidget { + final NTConnection ntConnection; + final SharedPreferences preferences; + final NT4StructMeta? ntStructMeta; + + final int gridIndex; + final String widgetName; + + final void Function(Offset globalPosition, WidgetContainerModel widget) + onDragUpdate; + + final void Function(WidgetContainerModel widget) onDragEnd; + + final void Function() onRemoveWidget; + + const NTWidgetDragTile({ + super.key, + required this.ntConnection, + required this.preferences, + this.ntStructMeta, + required this.gridIndex, + required this.widgetName, + required this.onDragUpdate, + required this.onDragEnd, + required this.onRemoveWidget, + }); + + @override + State createState() => _NTWidgetDragTileState(); +} + +class _NTWidgetDragTileState extends State { + WidgetContainerModel? draggingWidget; + + void cancelDrag() { + if (draggingWidget != null) { + draggingWidget?.unSubscribe(); + draggingWidget?.softDispose(deleting: true); + draggingWidget?.dispose(); + + widget.onRemoveWidget(); + + draggingWidget = null; + } + } + + @override + void didUpdateWidget(NTWidgetDragTile oldWidget) { + if (widget.gridIndex != oldWidget.gridIndex) { + cancelDrag(); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + cancelDrag(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) => InkWell( + onTap: () {}, + child: DragListener( + overrideVertical: false, + supportedDevices: PointerDeviceKind.values + .whereNot((element) => element == PointerDeviceKind.trackpad) + .toSet(), + onDragStart: (details) { + if (draggingWidget != null) { + return; + } + + // Prevents 2 finger drags from dragging a widget + if (details.kind != null && + details.kind! == PointerDeviceKind.trackpad) { + draggingWidget = null; + return; + } + + setState(() { + NTWidgetModel widgetModel = NTWidgetRegistry.buildNTModelFromType( + widget.ntConnection, + widget.preferences, + widget.ntStructMeta, + widget.widgetName, + null, + ); + + NTWidget? ntWidget = NTWidgetRegistry.buildNTWidgetFromModel( + widgetModel, + ); + + if (ntWidget == null) { + widgetModel.unSubscribe(); + widgetModel.softDispose(deleting: true); + widgetModel.dispose(); + return; + } + + double width = NTWidgetRegistry.getDefaultWidth(widgetModel); + double height = NTWidgetRegistry.getDefaultHeight(widgetModel); + + draggingWidget = NTWidgetContainerModel( + ntConnection: widget.ntConnection, + preferences: widget.preferences, + initialPosition: Rect.fromLTWH(0.0, 0.0, width, height), + title: widget.widgetName, + childModel: widgetModel, + ); + }); + }, + onDragUpdate: (details) { + if (draggingWidget == null) { + return; + } + + widget.onDragUpdate.call(details.globalPosition, draggingWidget!); + }, + onDragEnd: (details) { + if (draggingWidget == null) { + return; + } + + widget.onDragEnd.call(draggingWidget!); + + setState(() => draggingWidget = null); + }, + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: ListTile( + dense: true, + contentPadding: const EdgeInsets.only(right: 20), + title: Text(widget.widgetName), + ), + ), + ), + ); +} diff --git a/lib/widgets/nt_widgets/multi_topic/accelerometer.dart b/lib/widgets/nt_widgets/multi_topic/accelerometer.dart index dde0604fe..4b8d7cb7f 100644 --- a/lib/widgets/nt_widgets/multi_topic/accelerometer.dart +++ b/lib/widgets/nt_widgets/multi_topic/accelerometer.dart @@ -10,13 +10,13 @@ class AccelerometerModel extends MultiTopicNTWidgetModel { @override String type = AccelerometerWidget.widgetType; - late NT4Subscription _valueSubscription; - NT4Subscription get valueSubscription => _valueSubscription; + NT4Subscription? _valueSubscription; + NT4Subscription? get valueSubscription => _valueSubscription; String get valueTopic => '$topic/Value'; @override - List get subscriptions => [_valueSubscription]; + List get subscriptions => [?_valueSubscription]; AccelerometerModel({ required super.ntConnection, @@ -33,6 +33,11 @@ class AccelerometerModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + _valueSubscription = null; + return; + } + _valueSubscription = ntConnection.subscribe(valueTopic, super.period); } } @@ -47,7 +52,7 @@ class AccelerometerWidget extends NTWidget { AccelerometerModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.valueSubscription, + valueListenable: model.valueSubscription ?? ValueNotifier(null), builder: (context, data, child) { double value = tryCast(data) ?? 0.0; diff --git a/lib/widgets/nt_widgets/multi_topic/basic_swerve_drive.dart b/lib/widgets/nt_widgets/multi_topic/basic_swerve_drive.dart index 52afcda63..e7dadf819 100644 --- a/lib/widgets/nt_widgets/multi_topic/basic_swerve_drive.dart +++ b/lib/widgets/nt_widgets/multi_topic/basic_swerve_drive.dart @@ -32,28 +32,28 @@ class BasicSwerveModel extends MultiTopicNTWidgetModel { String get robotAngleTopic => '$topic/Robot Angle'; - late NT4Subscription frontLeftAngleSubscription; - late NT4Subscription frontLeftVelocitySubscription; - late NT4Subscription frontRightAngleSubscription; - late NT4Subscription frontRightVelocitySubscription; - late NT4Subscription backLeftAngleSubscription; - late NT4Subscription backLeftVelocitySubscription; - late NT4Subscription backRightAngleSubscription; - late NT4Subscription backRightVelocitySubscription; + NT4Subscription? frontLeftAngleSubscription; + NT4Subscription? frontLeftVelocitySubscription; + NT4Subscription? frontRightAngleSubscription; + NT4Subscription? frontRightVelocitySubscription; + NT4Subscription? backLeftAngleSubscription; + NT4Subscription? backLeftVelocitySubscription; + NT4Subscription? backRightAngleSubscription; + NT4Subscription? backRightVelocitySubscription; - late NT4Subscription robotAngleSubscription; + NT4Subscription? robotAngleSubscription; @override List get subscriptions => [ - frontLeftAngleSubscription, - frontLeftVelocitySubscription, - frontRightAngleSubscription, - frontRightVelocitySubscription, - backLeftAngleSubscription, - backLeftVelocitySubscription, - backRightAngleSubscription, - backRightVelocitySubscription, - robotAngleSubscription, + ?frontLeftAngleSubscription, + ?frontLeftVelocitySubscription, + ?frontRightAngleSubscription, + ?frontRightVelocitySubscription, + ?backLeftAngleSubscription, + ?backLeftVelocitySubscription, + ?backRightAngleSubscription, + ?backRightVelocitySubscription, + ?robotAngleSubscription, ]; bool _showRobotRotation = true; @@ -88,6 +88,20 @@ class BasicSwerveModel extends MultiTopicNTWidgetModel { } void initSubscriptions() { + if (topic == null) { + frontLeftAngleSubscription = null; + frontLeftVelocitySubscription = null; + frontRightAngleSubscription = null; + frontRightVelocitySubscription = null; + backLeftAngleSubscription = null; + backLeftVelocitySubscription = null; + backRightAngleSubscription = null; + backRightVelocitySubscription = null; + + robotAngleSubscription = null; + return; + } + frontLeftAngleSubscription = ntConnection.subscribe( frontLeftAngleTopic, super.period, @@ -218,26 +232,26 @@ class SwerveDriveWidget extends NTWidget { listenable: Listenable.merge(model.subscriptions), builder: (context, child) { double frontLeftAngle = - tryCast(model.frontLeftAngleSubscription.value) ?? 0.0; + tryCast(model.frontLeftAngleSubscription?.value) ?? 0.0; double frontLeftVelocity = - tryCast(model.frontLeftVelocitySubscription.value) ?? 0.0; + tryCast(model.frontLeftVelocitySubscription?.value) ?? 0.0; double frontRightAngle = - tryCast(model.frontRightAngleSubscription.value) ?? 0.0; + tryCast(model.frontRightAngleSubscription?.value) ?? 0.0; double frontRightVelocity = - tryCast(model.frontRightVelocitySubscription.value) ?? 0.0; + tryCast(model.frontRightVelocitySubscription?.value) ?? 0.0; double backLeftAngle = - tryCast(model.backLeftAngleSubscription.value) ?? 0.0; + tryCast(model.backLeftAngleSubscription?.value) ?? 0.0; double backLeftVelocity = - tryCast(model.backLeftVelocitySubscription.value) ?? 0.0; + tryCast(model.backLeftVelocitySubscription?.value) ?? 0.0; double backRightAngle = - tryCast(model.backRightAngleSubscription.value) ?? 0.0; + tryCast(model.backRightAngleSubscription?.value) ?? 0.0; double backRightVelocity = - tryCast(model.backRightVelocitySubscription.value) ?? 0.0; + tryCast(model.backRightVelocitySubscription?.value) ?? 0.0; - double robotAngle = tryCast(model.robotAngleSubscription.value) ?? 0.0; + double robotAngle = tryCast(model.robotAngleSubscription?.value) ?? 0.0; if (model.rotationUnit == 'Degrees') { frontLeftAngle = radians(frontLeftAngle); diff --git a/lib/widgets/nt_widgets/multi_topic/camera_stream.dart b/lib/widgets/nt_widgets/multi_topic/camera_stream.dart index 6537b7536..71dd8323d 100644 --- a/lib/widgets/nt_widgets/multi_topic/camera_stream.dart +++ b/lib/widgets/nt_widgets/multi_topic/camera_stream.dart @@ -17,10 +17,10 @@ class CameraStreamModel extends MultiTopicNTWidgetModel { String get streamsTopic => '$topic/streams'; - late NT4Subscription streamsSubscription; + NT4Subscription? streamsSubscription; @override - List get subscriptions => [streamsSubscription]; + List get subscriptions => [?streamsSubscription]; int? quality; int? fps; @@ -103,6 +103,11 @@ class CameraStreamModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + streamsSubscription = null; + return; + } + streamsSubscription = ntConnection.subscribe(streamsTopic, super.period); } @@ -324,7 +329,7 @@ class CameraStreamWidget extends NTWidget { ]), builder: (context, child) { List rawStreams = - tryCast(model.streamsSubscription.value) ?? []; + tryCast(model.streamsSubscription?.value) ?? []; List streams = []; for (Object? stream in rawStreams) { diff --git a/lib/widgets/nt_widgets/multi_topic/combo_box_chooser.dart b/lib/widgets/nt_widgets/multi_topic/combo_box_chooser.dart index d148632f5..9b3b58cae 100644 --- a/lib/widgets/nt_widgets/multi_topic/combo_box_chooser.dart +++ b/lib/widgets/nt_widgets/multi_topic/combo_box_chooser.dart @@ -20,20 +20,20 @@ class ComboBoxChooserModel extends MultiTopicNTWidgetModel { String get activeTopicName => '$topic/active'; String get defaultTopicName => '$topic/default'; - late NT4Subscription optionsSubscription; - late NT4Subscription selectedSubscription; - late NT4Subscription activeSubscription; - late NT4Subscription defaultSubscription; + NT4Subscription? optionsSubscription; + NT4Subscription? selectedSubscription; + NT4Subscription? activeSubscription; + NT4Subscription? defaultSubscription; @override List get subscriptions => [ - optionsSubscription, - selectedSubscription, - activeSubscription, - defaultSubscription, + ?optionsSubscription, + ?selectedSubscription, + ?activeSubscription, + ?defaultSubscription, ]; - late Listenable chooserStateListenable; + Listenable? chooserStateListenable; final TextEditingController _searchController = TextEditingController(); @@ -73,6 +73,15 @@ class ComboBoxChooserModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + optionsSubscription = null; + selectedSubscription = null; + activeSubscription = null; + defaultSubscription = null; + chooserStateListenable = null; + return; + } + optionsSubscription = ntConnection.subscribe( optionsTopicName, super.period, @@ -87,7 +96,7 @@ class ComboBoxChooserModel extends MultiTopicNTWidgetModel { super.period, ); chooserStateListenable = Listenable.merge(subscriptions); - chooserStateListenable.addListener(onChooserStateUpdate); + chooserStateListenable!.addListener(onChooserStateUpdate); previousOptions = null; previousActive = null; @@ -102,7 +111,7 @@ class ComboBoxChooserModel extends MultiTopicNTWidgetModel { @override void resetSubscription() { unpublishSelectedTopic(); - chooserStateListenable.removeListener(onChooserStateUpdate); + chooserStateListenable?.removeListener(onChooserStateUpdate); super.resetSubscription(); } @@ -132,7 +141,7 @@ class ComboBoxChooserModel extends MultiTopicNTWidgetModel { ]; void onChooserStateUpdate() { - List? rawOptions = optionsSubscription.value + List? rawOptions = optionsSubscription?.value ?.tryCast>(); List? currentOptions = rawOptions?.whereType().toList(); @@ -141,17 +150,17 @@ class ComboBoxChooserModel extends MultiTopicNTWidgetModel { currentOptions?.sort(); } - String? currentActive = tryCast(activeSubscription.value); + String? currentActive = tryCast(activeSubscription?.value); if (currentActive != null && currentActive.isEmpty) { currentActive = null; } - String? currentSelected = tryCast(selectedSubscription.value); + String? currentSelected = tryCast(selectedSubscription?.value); if (currentSelected != null && currentSelected.isEmpty) { currentSelected = null; } - String? currentDefault = tryCast(defaultSubscription.value); + String? currentDefault = tryCast(defaultSubscription?.value); if (currentDefault != null && currentDefault.isEmpty) { currentDefault = null; } diff --git a/lib/widgets/nt_widgets/multi_topic/command_scheduler.dart b/lib/widgets/nt_widgets/multi_topic/command_scheduler.dart index 66e7dd536..58519af55 100644 --- a/lib/widgets/nt_widgets/multi_topic/command_scheduler.dart +++ b/lib/widgets/nt_widgets/multi_topic/command_scheduler.dart @@ -19,15 +19,15 @@ class CommandSchedulerModel extends MultiTopicNTWidgetModel { String get idsTopicName => '$topic/Ids'; String get cancelTopicName => '$topic/Cancel'; - late NT4Subscription namesSubscription; - late NT4Subscription idsSubscription; - late NT4Subscription cancelSubscription; + NT4Subscription? namesSubscription; + NT4Subscription? idsSubscription; + NT4Subscription? cancelSubscription; @override List get subscriptions => [ - namesSubscription, - idsSubscription, - cancelSubscription, + ?namesSubscription, + ?idsSubscription, + ?cancelSubscription, ]; CommandSchedulerModel({ @@ -45,6 +45,13 @@ class CommandSchedulerModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + namesSubscription = null; + idsSubscription = null; + cancelSubscription = null; + return; + } + namesSubscription = ntConnection.subscribe(namesTopicName, super.period); idsSubscription = ntConnection.subscribe(idsTopicName, super.period); cancelSubscription = ntConnection.subscribe(cancelTopicName, super.period); @@ -59,7 +66,7 @@ class CommandSchedulerModel extends MultiTopicNTWidgetModel { void cancelCommand(int id) { List currentCancellationsRaw = - cancelSubscription.value?.tryCast>() ?? []; + cancelSubscription?.value?.tryCast>() ?? []; List currentCancellations = currentCancellationsRaw .whereType() @@ -93,10 +100,10 @@ class CommandSchedulerWidget extends NTWidget { listenable: Listenable.merge(model.subscriptions), builder: (context, child) { List rawNames = - model.namesSubscription.value?.tryCast>() ?? []; + model.namesSubscription?.value?.tryCast>() ?? []; List rawIds = - model.idsSubscription.value?.tryCast>() ?? []; + model.idsSubscription?.value?.tryCast>() ?? []; List names = rawNames.whereType().toList(); List ids = rawIds.whereType().toList(); diff --git a/lib/widgets/nt_widgets/multi_topic/command_widget.dart b/lib/widgets/nt_widgets/multi_topic/command_widget.dart index c7ec5e6c1..a80e766bd 100644 --- a/lib/widgets/nt_widgets/multi_topic/command_widget.dart +++ b/lib/widgets/nt_widgets/multi_topic/command_widget.dart @@ -14,13 +14,13 @@ class CommandModel extends MultiTopicNTWidgetModel { String get runningTopicName => '$topic/running'; String get nameTopicName => '$topic/name'; - late NT4Subscription runningSubscription; - late NT4Subscription nameSubscription; + NT4Subscription? runningSubscription; + NT4Subscription? nameSubscription; @override List get subscriptions => [ - runningSubscription, - nameSubscription, + ?runningSubscription, + ?nameSubscription, ]; NT4Topic? runningTopic; @@ -66,6 +66,12 @@ class CommandModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + runningSubscription = null; + nameSubscription = null; + return; + } + runningSubscription = ntConnection.subscribe( runningTopicName, super.period, @@ -123,7 +129,9 @@ class CommandWidget extends NTWidget { Widget build(BuildContext context) { CommandModel model = cast(context.watch()); - String buttonText = model.topic.substring(model.topic.lastIndexOf('/') + 1); + String buttonText = (model.topic != null) + ? model.topic!.substring(model.topic!.lastIndexOf('/') + 1) + : ''; ThemeData theme = Theme.of(context); @@ -145,12 +153,12 @@ class CommandWidget extends NTWidget { // Prevents widget from locking up if double pressed fast enough bool running = - model.runningSubscription.value?.tryCast() ?? false; + model.runningSubscription?.value?.tryCast() ?? false; model.ntConnection.updateDataFromTopic(model.runningTopic!, !running); }, child: ValueListenableBuilder( - valueListenable: model.runningSubscription, + valueListenable: model.runningSubscription ?? ValueNotifier(null), builder: (context, data, child) { bool running = tryCast(data) ?? false; @@ -191,7 +199,7 @@ class CommandWidget extends NTWidget { Visibility( visible: model.showType, child: ValueListenableBuilder( - valueListenable: model.nameSubscription, + valueListenable: model.nameSubscription ?? ValueNotifier(null), builder: (context, data, child) { String name = tryCast(data) ?? 'Unknown'; diff --git a/lib/widgets/nt_widgets/multi_topic/differential_drive.dart b/lib/widgets/nt_widgets/multi_topic/differential_drive.dart index e75e7faef..ece44c065 100644 --- a/lib/widgets/nt_widgets/multi_topic/differential_drive.dart +++ b/lib/widgets/nt_widgets/multi_topic/differential_drive.dart @@ -16,13 +16,13 @@ class DifferentialDriveModel extends MultiTopicNTWidgetModel { String get leftSpeedTopicName => '$topic/Left Motor Speed'; String get rightSpeedTopicName => '$topic/Right Motor Speed'; - late NT4Subscription leftSpeedSubscription; - late NT4Subscription rightSpeedSubscription; + NT4Subscription? leftSpeedSubscription; + NT4Subscription? rightSpeedSubscription; @override List get subscriptions => [ - leftSpeedSubscription, - rightSpeedSubscription, + ?leftSpeedSubscription, + ?rightSpeedSubscription, ]; NT4Topic? leftSpeedTopic; @@ -49,6 +49,11 @@ class DifferentialDriveModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + leftSpeedSubscription = null; + rightSpeedSubscription = null; + return; + } leftSpeedSubscription = ntConnection.subscribe( leftSpeedTopicName, super.period, @@ -90,8 +95,8 @@ class DifferentialDrive extends NTWidget { model.rightSpeedCurrentValue, ]), builder: (context, child) { - double leftSpeed = tryCast(model.leftSpeedSubscription.value) ?? 0.0; - double rightSpeed = tryCast(model.rightSpeedSubscription.value) ?? 0.0; + double leftSpeed = tryCast(model.leftSpeedSubscription?.value) ?? 0.0; + double rightSpeed = tryCast(model.rightSpeedSubscription?.value) ?? 0.0; if (leftSpeed != model.leftSpeedPreviousValue) { model.leftSpeedCurrentValue.value = leftSpeed; diff --git a/lib/widgets/nt_widgets/multi_topic/encoder_widget.dart b/lib/widgets/nt_widgets/multi_topic/encoder_widget.dart index 0c9e190bd..03709fc96 100644 --- a/lib/widgets/nt_widgets/multi_topic/encoder_widget.dart +++ b/lib/widgets/nt_widgets/multi_topic/encoder_widget.dart @@ -13,13 +13,13 @@ class EncoderModel extends MultiTopicNTWidgetModel { String get distanceTopic => '$topic/Distance'; String get speedTopic => '$topic/Speed'; - late NT4Subscription distanceSubscription; - late NT4Subscription speedSubscription; + NT4Subscription? distanceSubscription; + NT4Subscription? speedSubscription; @override List get subscriptions => [ - distanceSubscription, - speedSubscription, + ?distanceSubscription, + ?speedSubscription, ]; EncoderModel({ @@ -66,7 +66,8 @@ class EncoderWidget extends NTWidget { ), ), child: ValueListenableBuilder( - valueListenable: model.distanceSubscription, + valueListenable: + model.distanceSubscription ?? ValueNotifier(null), builder: (context, value, child) { double distance = tryCast(value) ?? 0.0; return SelectableText( @@ -95,7 +96,8 @@ class EncoderWidget extends NTWidget { ), ), child: ValueListenableBuilder( - valueListenable: model.speedSubscription, + valueListenable: + model.speedSubscription ?? ValueNotifier(null), builder: (context, value, child) { double speed = tryCast(value) ?? 0.0; return SelectableText( diff --git a/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget.dart b/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget.dart index ba320d938..961d4e30f 100644 --- a/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget.dart +++ b/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget.dart @@ -286,8 +286,7 @@ class FieldWidget extends NTWidget { Widget build(BuildContext context) { FieldWidgetModel model = cast(context.watch()); - List listeners = []; - listeners.add(model.robotSubscription); + List listeners = [?model.robotSubscription]; if (model.showOtherObjects || model.showTrajectories) { listeners.addAll(model.otherObjectSubscriptions); } diff --git a/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget_model.dart b/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget_model.dart index 46cbf8144..126fa3791 100644 --- a/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget_model.dart +++ b/lib/widgets/nt_widgets/multi_topic/field_widget/field_widget_model.dart @@ -37,14 +37,14 @@ class FieldWidgetModel extends MultiTopicNTWidgetModel { String type = 'Field'; String get robotTopicName => '$topic/Robot'; - late NT4Subscription robotSubscription; + NT4Subscription? robotSubscription; final List _otherObjectTopics = []; final List _otherObjectSubscriptions = []; @override List get subscriptions => [ - robotSubscription, + ?robotSubscription, ..._otherObjectSubscriptions, ]; @@ -144,7 +144,7 @@ class FieldWidgetModel extends MultiTopicNTWidgetModel { FieldObject getRobotObject() { List robotPositionRaw = - robotSubscription.value?.tryCast>() ?? []; + robotSubscription?.value?.tryCast>() ?? []; if (isPoseStruct(robotTopicName)) { List poseBytes = robotPositionRaw.whereType().toList(); @@ -374,7 +374,8 @@ class FieldWidgetModel extends MultiTopicNTWidgetModel { super.init(); topicAnnounceListener = (nt4Topic) { - if (nt4Topic.name.startsWith(topic) && + if (topic != null && + nt4Topic.name.startsWith(topic!) && !nt4Topic.name.endsWith('Robot') && !nt4Topic.name.contains('.') && !_otherObjectTopics.contains(nt4Topic.name)) { @@ -393,6 +394,10 @@ class FieldWidgetModel extends MultiTopicNTWidgetModel { void initializeSubscriptions() { _otherObjectSubscriptions.clear(); + if (topic == null) { + robotSubscription = null; + return; + } robotSubscription = ntConnection.subscribe(robotTopicName, super.period); } diff --git a/lib/widgets/nt_widgets/multi_topic/fms_info.dart b/lib/widgets/nt_widgets/multi_topic/fms_info.dart index 4a4272bf5..ba7abe4f5 100644 --- a/lib/widgets/nt_widgets/multi_topic/fms_info.dart +++ b/lib/widgets/nt_widgets/multi_topic/fms_info.dart @@ -21,21 +21,21 @@ class FMSInfoModel extends MultiTopicNTWidgetModel { String get replayNumberTopic => '$topic/ReplayNumber'; String get stationNumberTopic => '$topic/StationNumber'; - late NT4Subscription eventNameSubscription; - late NT4Subscription controlDataSubscription; - late NT4Subscription allianceSubscription; - late NT4Subscription matchNumberSubscription; - late NT4Subscription matchTypeSubscription; - late NT4Subscription replayNumberSubscription; + NT4Subscription? eventNameSubscription; + NT4Subscription? controlDataSubscription; + NT4Subscription? allianceSubscription; + NT4Subscription? matchNumberSubscription; + NT4Subscription? matchTypeSubscription; + NT4Subscription? replayNumberSubscription; @override List get subscriptions => [ - eventNameSubscription, - controlDataSubscription, - allianceSubscription, - matchNumberSubscription, - matchTypeSubscription, - replayNumberSubscription, + ?eventNameSubscription, + ?controlDataSubscription, + ?allianceSubscription, + ?matchNumberSubscription, + ?matchTypeSubscription, + ?replayNumberSubscription, ]; FMSInfoModel({ @@ -53,6 +53,16 @@ class FMSInfoModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + eventNameSubscription = null; + controlDataSubscription = null; + allianceSubscription = null; + matchNumberSubscription = null; + matchTypeSubscription = null; + replayNumberSubscription = null; + + return; + } eventNameSubscription = ntConnection.subscribe( eventNameTopic, super.period, @@ -111,12 +121,12 @@ class FMSInfo extends NTWidget { return ListenableBuilder( listenable: Listenable.merge(model.subscriptions), builder: (context, child) { - String eventName = tryCast(model.eventNameSubscription.value) ?? ''; - int controlData = tryCast(model.controlDataSubscription.value) ?? 32; - bool redAlliance = tryCast(model.allianceSubscription.value) ?? true; - int matchNumber = tryCast(model.matchNumberSubscription.value) ?? 0; - int matchType = tryCast(model.matchTypeSubscription.value) ?? 0; - int replayNumber = tryCast(model.replayNumberSubscription.value) ?? 0; + String eventName = tryCast(model.eventNameSubscription?.value) ?? ''; + int controlData = tryCast(model.controlDataSubscription?.value) ?? 32; + bool redAlliance = tryCast(model.allianceSubscription?.value) ?? true; + int matchNumber = tryCast(model.matchNumberSubscription?.value) ?? 0; + int matchType = tryCast(model.matchTypeSubscription?.value) ?? 0; + int replayNumber = tryCast(model.replayNumberSubscription?.value) ?? 0; String eventNameDisplay = '$eventName${(eventName != '') ? ' ' : ''}'; String matchTypeString = _getMatchTypeString(matchType); diff --git a/lib/widgets/nt_widgets/multi_topic/gyro.dart b/lib/widgets/nt_widgets/multi_topic/gyro.dart index c7714b29b..0cbcb81ba 100644 --- a/lib/widgets/nt_widgets/multi_topic/gyro.dart +++ b/lib/widgets/nt_widgets/multi_topic/gyro.dart @@ -16,10 +16,10 @@ class GyroModel extends MultiTopicNTWidgetModel { String get valueTopic => '$topic/Value'; - late NT4Subscription valueSubscription; + NT4Subscription? valueSubscription; @override - List get subscriptions => [valueSubscription]; + List get subscriptions => [?valueSubscription]; bool _counterClockwisePositive = false; @@ -50,6 +50,10 @@ class GyroModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + valueSubscription = null; + return; + } valueSubscription = ntConnection.subscribe(valueTopic, super.period); } @@ -102,7 +106,7 @@ class Gyro extends NTWidget { return Transform.scale( scale: squareSide / normalSquareSide, child: ValueListenableBuilder( - valueListenable: model.valueSubscription, + valueListenable: model.valueSubscription ?? ValueNotifier(null), builder: (context, data, child) { double value = tryCast(data) ?? 0.0; diff --git a/lib/widgets/nt_widgets/multi_topic/motor_controller.dart b/lib/widgets/nt_widgets/multi_topic/motor_controller.dart index b20f9b999..06ca6ee7a 100644 --- a/lib/widgets/nt_widgets/multi_topic/motor_controller.dart +++ b/lib/widgets/nt_widgets/multi_topic/motor_controller.dart @@ -13,10 +13,10 @@ class MotorControllerModel extends MultiTopicNTWidgetModel { String get valueTopic => '$topic/Value'; - late NT4Subscription valueSubscription; + NT4Subscription? valueSubscription; @override - List get subscriptions => [valueSubscription]; + List get subscriptions => [?valueSubscription]; MotorControllerModel({ required super.ntConnection, @@ -33,6 +33,11 @@ class MotorControllerModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + valueSubscription = null; + return; + } + valueSubscription = ntConnection.subscribe(valueTopic, super.period); } } @@ -47,7 +52,7 @@ class MotorController extends NTWidget { MotorControllerModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.valueSubscription, + valueListenable: model.valueSubscription ?? ValueNotifier(null), builder: (context, data, child) { double value = tryCast(data) ?? 0.0; diff --git a/lib/widgets/nt_widgets/multi_topic/network_alerts.dart b/lib/widgets/nt_widgets/multi_topic/network_alerts.dart index dfe5a3f22..b7cf80567 100644 --- a/lib/widgets/nt_widgets/multi_topic/network_alerts.dart +++ b/lib/widgets/nt_widgets/multi_topic/network_alerts.dart @@ -14,15 +14,15 @@ class NetworkAlertsModel extends MultiTopicNTWidgetModel { String get warningsTopicName => '$topic/warnings'; String get infosTopicName => '$topic/infos'; - late NT4Subscription errorsSubscription; - late NT4Subscription warningsSubscription; - late NT4Subscription infosSubscription; + NT4Subscription? errorsSubscription; + NT4Subscription? warningsSubscription; + NT4Subscription? infosSubscription; @override List get subscriptions => [ - errorsSubscription, - warningsSubscription, - infosSubscription, + ?errorsSubscription, + ?warningsSubscription, + ?infosSubscription, ]; NetworkAlertsModel({ @@ -40,6 +40,13 @@ class NetworkAlertsModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + errorsSubscription = null; + warningsSubscription = null; + infosSubscription = null; + return; + } + errorsSubscription = ntConnection.subscribe(errorsTopicName, super.period); warningsSubscription = ntConnection.subscribe( warningsTopicName, @@ -64,13 +71,13 @@ class NetworkAlerts extends NTWidget { listenable: Listenable.merge(model.subscriptions), builder: (context, child) { List errorsRaw = - model.errorsSubscription.value?.tryCast>() ?? []; + model.errorsSubscription?.value?.tryCast>() ?? []; List warningsRaw = - model.warningsSubscription.value?.tryCast>() ?? []; + model.warningsSubscription?.value?.tryCast>() ?? []; List infosRaw = - model.infosSubscription.value?.tryCast>() ?? []; + model.infosSubscription?.value?.tryCast>() ?? []; List errors = errorsRaw.whereType().toList(); List warnings = warningsRaw.whereType().toList(); diff --git a/lib/widgets/nt_widgets/multi_topic/pid_controller.dart b/lib/widgets/nt_widgets/multi_topic/pid_controller.dart index d5d86d1fc..2af9fdbd6 100644 --- a/lib/widgets/nt_widgets/multi_topic/pid_controller.dart +++ b/lib/widgets/nt_widgets/multi_topic/pid_controller.dart @@ -21,17 +21,17 @@ class PIDControllerModel extends MultiTopicNTWidgetModel { NT4Topic? _kdTopic; NT4Topic? _setpointTopic; - late NT4Subscription kpSubscription; - late NT4Subscription kiSubscription; - late NT4Subscription kdSubscription; - late NT4Subscription setpointSubscription; + NT4Subscription? kpSubscription; + NT4Subscription? kiSubscription; + NT4Subscription? kdSubscription; + NT4Subscription? setpointSubscription; @override List get subscriptions => [ - kpSubscription, - kiSubscription, - kdSubscription, - setpointSubscription, + ?kpSubscription, + ?kiSubscription, + ?kdSubscription, + ?setpointSubscription, ]; TextEditingController? kpTextController; @@ -59,6 +59,14 @@ class PIDControllerModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + kpSubscription = null; + kiSubscription = null; + kdSubscription = null; + setpointSubscription = null; + return; + } + kpSubscription = ntConnection.subscribe(kpTopicName, super.period); kiSubscription = ntConnection.subscribe(kiTopicName, super.period); kdSubscription = ntConnection.subscribe(kdTopicName, super.period); @@ -169,10 +177,10 @@ class PIDControllerWidget extends NTWidget { model.setpointTextController, ]), builder: (context, child) { - double kP = tryCast(model.kpSubscription.value) ?? 0.0; - double kI = tryCast(model.kiSubscription.value) ?? 0.0; - double kD = tryCast(model.kdSubscription.value) ?? 0.0; - double setpoint = tryCast(model.setpointSubscription.value) ?? 0.0; + double kP = tryCast(model.kpSubscription?.value) ?? 0.0; + double kI = tryCast(model.kiSubscription?.value) ?? 0.0; + double kD = tryCast(model.kdSubscription?.value) ?? 0.0; + double setpoint = tryCast(model.setpointSubscription?.value) ?? 0.0; // Creates the text editing controllers if they are null bool wasNull = diff --git a/lib/widgets/nt_widgets/multi_topic/power_distribution.dart b/lib/widgets/nt_widgets/multi_topic/power_distribution.dart index 299894c76..c9b8f393e 100644 --- a/lib/widgets/nt_widgets/multi_topic/power_distribution.dart +++ b/lib/widgets/nt_widgets/multi_topic/power_distribution.dart @@ -17,15 +17,15 @@ class PowerDistributionModel extends MultiTopicNTWidgetModel { String get voltageTopic => '$topic/Voltage'; String get currentTopic => '$topic/TotalCurrent'; - late NT4Subscription voltageSubscription; - late NT4Subscription currentSubscription; + NT4Subscription? voltageSubscription; + NT4Subscription? currentSubscription; final List channelSubscriptions = []; @override List get subscriptions => [ - voltageSubscription, - currentSubscription, + ?voltageSubscription, + ?currentSubscription, ...channelSubscriptions, ]; @@ -44,12 +44,18 @@ class PowerDistributionModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { - voltageSubscription = ntConnection.subscribe(voltageTopic, super.period); - currentSubscription = ntConnection.subscribe(currentTopic, super.period); - channelTopics.clear(); channelSubscriptions.clear(); + if (topic == null) { + voltageSubscription = null; + currentSubscription = null; + return; + } + + voltageSubscription = ntConnection.subscribe(voltageTopic, super.period); + currentSubscription = ntConnection.subscribe(currentTopic, super.period); + for (int channel = 0; channel <= numberOfChannels; channel++) { channelTopics.add('$topic/Chan$channel'); } @@ -79,7 +85,9 @@ class PowerDistribution extends NTWidget { mainAxisSize: MainAxisSize.max, children: [ ValueListenableBuilder( - valueListenable: model.channelSubscriptions[channel], + valueListenable: + model.channelSubscriptions.elementAtOrNull(channel) ?? + ValueNotifier(null), builder: (context, value, child) { double current = tryCast(value) ?? 0.0; @@ -132,7 +140,9 @@ class PowerDistribution extends NTWidget { Text('Ch. $channel'), const SizedBox(width: 10), ValueListenableBuilder( - valueListenable: model.channelSubscriptions[channel], + valueListenable: + model.channelSubscriptions.elementAtOrNull(channel) ?? + ValueNotifier(null), builder: (context, value, child) { double current = tryCast(value) ?? 0.0; return Container( @@ -180,7 +190,8 @@ class PowerDistribution extends NTWidget { const Text('Voltage'), const SizedBox(height: 2.5), ValueListenableBuilder( - valueListenable: model.voltageSubscription, + valueListenable: + model.voltageSubscription ?? ValueNotifier(null), builder: (context, value, child) { double voltage = tryCast(value) ?? 0.0; @@ -210,7 +221,8 @@ class PowerDistribution extends NTWidget { const Text('Total Current'), const SizedBox(height: 2.5), ValueListenableBuilder( - valueListenable: model.currentSubscription, + valueListenable: + model.currentSubscription ?? ValueNotifier(null), builder: (context, value, child) { double totalCurrent = tryCast(value) ?? 0.0; diff --git a/lib/widgets/nt_widgets/multi_topic/profiled_pid_controller.dart b/lib/widgets/nt_widgets/multi_topic/profiled_pid_controller.dart index d47510579..3e8d3a1de 100644 --- a/lib/widgets/nt_widgets/multi_topic/profiled_pid_controller.dart +++ b/lib/widgets/nt_widgets/multi_topic/profiled_pid_controller.dart @@ -21,17 +21,17 @@ class ProfiledPIDControllerModel extends MultiTopicNTWidgetModel { NT4Topic? _kdTopic; NT4Topic? _goalTopic; - late NT4Subscription kpSubscription; - late NT4Subscription kiSubscription; - late NT4Subscription kdSubscription; - late NT4Subscription goalSubscription; + NT4Subscription? kpSubscription; + NT4Subscription? kiSubscription; + NT4Subscription? kdSubscription; + NT4Subscription? goalSubscription; @override List get subscriptions => [ - kpSubscription, - kiSubscription, - kdSubscription, - goalSubscription, + ?kpSubscription, + ?kiSubscription, + ?kdSubscription, + ?goalSubscription, ]; TextEditingController? kpTextController; @@ -59,6 +59,14 @@ class ProfiledPIDControllerModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + kpSubscription = null; + kiSubscription = null; + kdSubscription = null; + goalSubscription = null; + return; + } + kpSubscription = ntConnection.subscribe(kpTopicName, super.period); kiSubscription = ntConnection.subscribe(kiTopicName, super.period); kdSubscription = ntConnection.subscribe(kdTopicName, super.period); @@ -166,10 +174,10 @@ class ProfiledPIDControllerWidget extends NTWidget { model.goalTextController, ]), builder: (context, child) { - double kP = tryCast(model.kpSubscription.value) ?? 0.0; - double kI = tryCast(model.kiSubscription.value) ?? 0.0; - double kD = tryCast(model.kdSubscription.value) ?? 0.0; - double goal = tryCast(model.goalSubscription.value) ?? 0.0; + double kP = tryCast(model.kpSubscription?.value) ?? 0.0; + double kI = tryCast(model.kiSubscription?.value) ?? 0.0; + double kD = tryCast(model.kdSubscription?.value) ?? 0.0; + double goal = tryCast(model.goalSubscription?.value) ?? 0.0; // Creates the text editing controllers if they are null bool wasNull = diff --git a/lib/widgets/nt_widgets/multi_topic/relay_widget.dart b/lib/widgets/nt_widgets/multi_topic/relay_widget.dart index 0d2798ae3..d60bb1a93 100644 --- a/lib/widgets/nt_widgets/multi_topic/relay_widget.dart +++ b/lib/widgets/nt_widgets/multi_topic/relay_widget.dart @@ -12,10 +12,10 @@ class RelayModel extends MultiTopicNTWidgetModel { String get valueTopicName => '$topic/Value'; - late NT4Subscription valueSubscription; + NT4Subscription? valueSubscription; @override - List get subscriptions => [valueSubscription]; + List get subscriptions => [?valueSubscription]; NT4Topic? valueTopic; @@ -36,6 +36,11 @@ class RelayModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + valueSubscription = null; + return; + } + valueSubscription = ntConnection.subscribe(valueTopicName, super.period); } @@ -64,7 +69,7 @@ class RelayWidget extends NTWidget { RelayModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.valueSubscription, + valueListenable: model.valueSubscription ?? ValueNotifier(null), builder: (context, data, child) { String selected = tryCast(data) ?? 'Off'; diff --git a/lib/widgets/nt_widgets/multi_topic/robot_preferences.dart b/lib/widgets/nt_widgets/multi_topic/robot_preferences.dart index a7ca0d1a3..80d3d0ef0 100644 --- a/lib/widgets/nt_widgets/multi_topic/robot_preferences.dart +++ b/lib/widgets/nt_widgets/multi_topic/robot_preferences.dart @@ -45,7 +45,7 @@ class RobotPreferencesModel extends MultiTopicNTWidgetModel { @override void init() { topicAnnounceListener = (topic) { - if (!topic.name.contains(this.topic) || + if (this.topic != null && !topic.name.contains(this.topic!) || preferenceTopicNames.contains(topic.name) || topic.name.contains('.type')) { return; diff --git a/lib/widgets/nt_widgets/multi_topic/split_button_chooser.dart b/lib/widgets/nt_widgets/multi_topic/split_button_chooser.dart index 2870d6588..f50cfad33 100644 --- a/lib/widgets/nt_widgets/multi_topic/split_button_chooser.dart +++ b/lib/widgets/nt_widgets/multi_topic/split_button_chooser.dart @@ -16,20 +16,20 @@ class SplitButtonChooserModel extends MultiTopicNTWidgetModel { String get activeTopicName => '$topic/active'; String get defaultTopicName => '$topic/default'; - late NT4Subscription optionsSubscription; - late NT4Subscription selectedSubscription; - late NT4Subscription activeSubscription; - late NT4Subscription defaultSubscription; + NT4Subscription? optionsSubscription; + NT4Subscription? selectedSubscription; + NT4Subscription? activeSubscription; + NT4Subscription? defaultSubscription; @override List get subscriptions => [ - optionsSubscription, - selectedSubscription, - activeSubscription, - defaultSubscription, + ?optionsSubscription, + ?selectedSubscription, + ?activeSubscription, + ?defaultSubscription, ]; - late Listenable chooserStateListenable; + Listenable? chooserStateListenable; String? previousDefault; String? previousSelected; @@ -53,6 +53,15 @@ class SplitButtonChooserModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + optionsSubscription = null; + selectedSubscription = null; + activeSubscription = null; + defaultSubscription = null; + chooserStateListenable = null; + return; + } + optionsSubscription = ntConnection.subscribe( optionsTopicName, super.period, @@ -67,7 +76,7 @@ class SplitButtonChooserModel extends MultiTopicNTWidgetModel { super.period, ); chooserStateListenable = Listenable.merge(subscriptions); - chooserStateListenable.addListener(onChooserStateUpdate); + chooserStateListenable!.addListener(onChooserStateUpdate); previousOptions = null; previousActive = null; @@ -82,7 +91,7 @@ class SplitButtonChooserModel extends MultiTopicNTWidgetModel { @override void resetSubscription() { unpublishSelectedTopic(); - chooserStateListenable.removeListener(onChooserStateUpdate); + chooserStateListenable?.removeListener(onChooserStateUpdate); super.resetSubscription(); } @@ -95,22 +104,22 @@ class SplitButtonChooserModel extends MultiTopicNTWidgetModel { } void onChooserStateUpdate() { - List? rawOptions = optionsSubscription.value + List? rawOptions = optionsSubscription?.value ?.tryCast>(); List? currentOptions = rawOptions?.whereType().toList(); - String? currentActive = tryCast(activeSubscription.value); + String? currentActive = tryCast(activeSubscription?.value); if (currentActive != null && currentActive.isEmpty) { currentActive = null; } - String? currentSelected = tryCast(selectedSubscription.value); + String? currentSelected = tryCast(selectedSubscription?.value); if (currentSelected != null && currentSelected.isEmpty) { currentSelected = null; } - String? currentDefault = tryCast(defaultSubscription.value); + String? currentDefault = tryCast(defaultSubscription?.value); if (currentDefault != null && currentDefault.isEmpty) { currentDefault = null; } diff --git a/lib/widgets/nt_widgets/multi_topic/subsystem_widget.dart b/lib/widgets/nt_widgets/multi_topic/subsystem_widget.dart index 43af07658..9c374bbcd 100644 --- a/lib/widgets/nt_widgets/multi_topic/subsystem_widget.dart +++ b/lib/widgets/nt_widgets/multi_topic/subsystem_widget.dart @@ -13,13 +13,13 @@ class SubsystemModel extends MultiTopicNTWidgetModel { String get defaultCommandTopic => '$topic/.default'; String get currentCommandTopic => '$topic/.command'; - late NT4Subscription defaultCommandSubscription; - late NT4Subscription currentCommandSubscription; + NT4Subscription? defaultCommandSubscription; + NT4Subscription? currentCommandSubscription; @override List get subscriptions => [ - defaultCommandSubscription, - currentCommandSubscription, + ?defaultCommandSubscription, + ?currentCommandSubscription, ]; SubsystemModel({ @@ -37,6 +37,12 @@ class SubsystemModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + defaultCommandSubscription = null; + currentCommandSubscription = null; + return; + } + defaultCommandSubscription = ntConnection.subscribe( defaultCommandTopic, super.period, @@ -61,7 +67,8 @@ class SubsystemWidget extends NTWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ ValueListenableBuilder( - valueListenable: model.defaultCommandSubscription, + valueListenable: + model.defaultCommandSubscription ?? ValueNotifier(null), builder: (context, value, child) { String defaultCommand = tryCast(value) ?? 'none'; @@ -73,7 +80,8 @@ class SubsystemWidget extends NTWidget { ), const SizedBox(height: 5), ValueListenableBuilder( - valueListenable: model.currentCommandSubscription, + valueListenable: + model.currentCommandSubscription ?? ValueNotifier(null), builder: (context, value, child) { String currentCommand = tryCast(value) ?? 'none'; diff --git a/lib/widgets/nt_widgets/multi_topic/three_axis_accelerometer.dart b/lib/widgets/nt_widgets/multi_topic/three_axis_accelerometer.dart index a4698b3fa..e4ff44646 100644 --- a/lib/widgets/nt_widgets/multi_topic/three_axis_accelerometer.dart +++ b/lib/widgets/nt_widgets/multi_topic/three_axis_accelerometer.dart @@ -14,15 +14,15 @@ class ThreeAxisAccelerometerModel extends MultiTopicNTWidgetModel { String get yTopic => '$topic/Y'; String get zTopic => '$topic/Z'; - late NT4Subscription xSubscription; - late NT4Subscription ySubscription; - late NT4Subscription zSubscription; + NT4Subscription? xSubscription; + NT4Subscription? ySubscription; + NT4Subscription? zSubscription; @override List get subscriptions => [ - xSubscription, - ySubscription, - zSubscription, + ?xSubscription, + ?ySubscription, + ?zSubscription, ]; ThreeAxisAccelerometerModel({ @@ -40,6 +40,13 @@ class ThreeAxisAccelerometerModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + xSubscription = null; + ySubscription = null; + zSubscription = null; + return; + } + xSubscription = ntConnection.subscribe(xTopic, super.period); ySubscription = ntConnection.subscribe(yTopic, super.period); zSubscription = ntConnection.subscribe(zTopic, super.period); @@ -76,7 +83,7 @@ class ThreeAxisAccelerometer extends NTWidget { ), padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ValueListenableBuilder( - valueListenable: model.xSubscription, + valueListenable: model.xSubscription ?? ValueNotifier(null), builder: (context, value, child) { double xAccel = tryCast(value) ?? 0.0; return Text( @@ -113,7 +120,7 @@ class ThreeAxisAccelerometer extends NTWidget { ), padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ValueListenableBuilder( - valueListenable: model.ySubscription, + valueListenable: model.ySubscription ?? ValueNotifier(null), builder: (context, value, child) { double yAccel = tryCast(value) ?? 0.0; return Text( @@ -150,7 +157,7 @@ class ThreeAxisAccelerometer extends NTWidget { ), padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ValueListenableBuilder( - valueListenable: model.zSubscription, + valueListenable: model.zSubscription ?? ValueNotifier(null), builder: (context, value, child) { double zAccel = tryCast(value) ?? 0.0; return Text( diff --git a/lib/widgets/nt_widgets/multi_topic/ultrasonic.dart b/lib/widgets/nt_widgets/multi_topic/ultrasonic.dart index cc15f0dd9..21fda98dd 100644 --- a/lib/widgets/nt_widgets/multi_topic/ultrasonic.dart +++ b/lib/widgets/nt_widgets/multi_topic/ultrasonic.dart @@ -12,10 +12,10 @@ class UltrasonicModel extends MultiTopicNTWidgetModel { String get valueTopic => '$topic/Value'; - late NT4Subscription valueSubscription; + NT4Subscription? valueSubscription; @override - List get subscriptions => [valueSubscription]; + List get subscriptions => [?valueSubscription]; UltrasonicModel({ required super.ntConnection, @@ -32,6 +32,11 @@ class UltrasonicModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + valueSubscription = null; + return; + } + valueSubscription = ntConnection.subscribe(valueTopic, super.period); } } @@ -57,7 +62,7 @@ class Ultrasonic extends NTWidget { ), ), child: ValueListenableBuilder( - valueListenable: model.valueSubscription, + valueListenable: model.valueSubscription ?? ValueNotifier(null), builder: (context, data, child) { double value = tryCast(data) ?? 0.0; return SelectableText( diff --git a/lib/widgets/nt_widgets/multi_topic/yagsl_swerve_drive.dart b/lib/widgets/nt_widgets/multi_topic/yagsl_swerve_drive.dart index 3a01c5478..b828988bd 100644 --- a/lib/widgets/nt_widgets/multi_topic/yagsl_swerve_drive.dart +++ b/lib/widgets/nt_widgets/multi_topic/yagsl_swerve_drive.dart @@ -24,23 +24,23 @@ class YAGSLSwerveDriveModel extends MultiTopicNTWidgetModel { String get robotLengthTopic => '$topic/sizeFrontBack'; String get rotationUnitTopic => '$topic/rotationUnit'; - late NT4Subscription measuredStatesSubscription; - late NT4Subscription desiredStatesSubscription; - late NT4Subscription robotRotationSubscription; - late NT4Subscription maxSpeedSubscription; - late NT4Subscription robotWidthSubscription; - late NT4Subscription robotLengthSubscription; - late NT4Subscription rotationUnitSubscription; + NT4Subscription? measuredStatesSubscription; + NT4Subscription? desiredStatesSubscription; + NT4Subscription? robotRotationSubscription; + NT4Subscription? maxSpeedSubscription; + NT4Subscription? robotWidthSubscription; + NT4Subscription? robotLengthSubscription; + NT4Subscription? rotationUnitSubscription; @override List get subscriptions => [ - measuredStatesSubscription, - desiredStatesSubscription, - robotRotationSubscription, - maxSpeedSubscription, - robotWidthSubscription, - robotLengthSubscription, - rotationUnitSubscription, + ?measuredStatesSubscription, + ?desiredStatesSubscription, + ?robotRotationSubscription, + ?maxSpeedSubscription, + ?robotWidthSubscription, + ?robotLengthSubscription, + ?rotationUnitSubscription, ]; bool _showRobotRotation = true; @@ -94,6 +94,17 @@ class YAGSLSwerveDriveModel extends MultiTopicNTWidgetModel { @override void initializeSubscriptions() { + if (topic == null) { + measuredStatesSubscription = null; + desiredStatesSubscription = null; + robotRotationSubscription = null; + maxSpeedSubscription = null; + robotWidthSubscription = null; + robotLengthSubscription = null; + rotationUnitSubscription = null; + return; + } + measuredStatesSubscription = ntConnection.subscribe( measuredStatesTopic, super.period, @@ -190,9 +201,9 @@ class YAGSLSwerveDrive extends NTWidget { listenable: Listenable.merge(model.subscriptions), builder: (context, snapshot) { List measuredStatesRaw = - tryCast(model.measuredStatesSubscription.value) ?? []; + tryCast(model.measuredStatesSubscription?.value) ?? []; List desiredStatesRaw = - tryCast(model.desiredStatesSubscription.value) ?? []; + tryCast(model.desiredStatesSubscription?.value) ?? []; List measuredStates = measuredStatesRaw .whereType() @@ -201,8 +212,8 @@ class YAGSLSwerveDrive extends NTWidget { .whereType() .toList(); - double width = tryCast(model.robotWidthSubscription.value) ?? 1.0; - double length = tryCast(model.robotLengthSubscription.value) ?? width; + double width = tryCast(model.robotWidthSubscription?.value) ?? 1.0; + double length = tryCast(model.robotLengthSubscription?.value) ?? width; if (width <= 0.0) { width = 1.0; @@ -215,10 +226,10 @@ class YAGSLSwerveDrive extends NTWidget { double lengthWidthRatio = length / width; String rotationUnit = - tryCast(model.rotationUnitSubscription.value) ?? 'radians'; + tryCast(model.rotationUnitSubscription?.value) ?? 'radians'; double robotAngle = - tryCast(model.robotRotationSubscription.value) ?? 0.0; + tryCast(model.robotRotationSubscription?.value) ?? 0.0; if (rotationUnit == 'degrees') { robotAngle = radians(robotAngle); @@ -228,7 +239,7 @@ class YAGSLSwerveDrive extends NTWidget { robotAngle -= radians(model.angleOffset); - double maxSpeed = tryCast(model.maxSpeedSubscription.value) ?? 4.5; + double maxSpeed = tryCast(model.maxSpeedSubscription?.value) ?? 4.5; if (maxSpeed <= 0.0) { maxSpeed = 4.5; diff --git a/lib/widgets/nt_widgets/nt_widget.dart b/lib/widgets/nt_widgets/nt_widget.dart index 3020f3da7..f5ef83f2f 100644 --- a/lib/widgets/nt_widgets/nt_widget.dart +++ b/lib/widgets/nt_widgets/nt_widget.dart @@ -31,12 +31,12 @@ sealed class NTWidgetModel extends ChangeNotifier { late double _period; - late String _topic; + String? _topic; // ignore: unnecessary_getters_setters - String get topic => _topic; + String? get topic => _topic; - set topic(String value) => _topic = value; + set topic(String? value) => _topic = value; // ignore: unnecessary_getters_setters double get period => _period; @@ -46,7 +46,7 @@ sealed class NTWidgetModel extends ChangeNotifier { NTWidgetModel({ required this.ntConnection, required this.preferences, - required String topic, + required String? topic, double? period, }) : _topic = topic { this.period = @@ -60,7 +60,7 @@ sealed class NTWidgetModel extends ChangeNotifier { required this.preferences, required Map jsonData, }) { - _topic = tryCast(jsonData['topic']) ?? ''; + _topic = tryCast(jsonData['topic']); _period = tryCast(jsonData['period']) ?? @@ -227,17 +227,21 @@ class SingleTopicNTWidgetModel extends NTWidgetModel { @override @mustCallSuper void init() async { - subscription = ntConnection.subscribeWithOptions( - topic, - NT4SubscriptionOptions( - periodicRateSeconds: period, - structMeta: ntStructMeta, - ), - ); + if (topic != null) { + subscription = ntConnection.subscribeWithOptions( + topic!, + NT4SubscriptionOptions( + periodicRateSeconds: period, + structMeta: ntStructMeta, + ), + ); + } } void createTopicIfNull() { - ntTopic ??= ntConnection.getTopicFromName(topic); + if (topic != null) { + ntTopic ??= ntConnection.getTopicFromName(topic!); + } } @override @@ -254,13 +258,15 @@ class SingleTopicNTWidgetModel extends NTWidgetModel { @override void resetSubscription() { if (subscription == null) { - subscription = ntConnection.subscribeWithOptions( - topic, - NT4SubscriptionOptions( - periodicRateSeconds: period, - structMeta: ntStructMeta, - ), - ); + if (topic != null) { + subscription = ntConnection.subscribeWithOptions( + topic!, + NT4SubscriptionOptions( + periodicRateSeconds: period, + structMeta: ntStructMeta, + ), + ); + } ntTopic = null; @@ -271,13 +277,15 @@ class SingleTopicNTWidgetModel extends NTWidgetModel { bool resetDataType = subscription!.topic != topic; ntConnection.unSubscribe(subscription!); - subscription = ntConnection.subscribeWithOptions( - topic, - NT4SubscriptionOptions( - periodicRateSeconds: period, - structMeta: ntStructMeta, - ), - ); + if (topic != null) { + subscription = ntConnection.subscribeWithOptions( + topic!, + NT4SubscriptionOptions( + periodicRateSeconds: period, + structMeta: ntStructMeta, + ), + ); + } ntTopic = null; @@ -292,9 +300,9 @@ class SingleTopicNTWidgetModel extends NTWidgetModel { // If the path of the struct has changed, we want to update its // value of the struct field - if (ntStructMeta != null) { + if (ntStructMeta != null && topic != null) { subscription!.updateValue( - ntConnection.getLastAnnouncedValue(topic), + ntConnection.getLastAnnouncedValue(topic!), subscription!.timestamp, ); } diff --git a/lib/widgets/nt_widgets/single_topic/boolean_box.dart b/lib/widgets/nt_widgets/single_topic/boolean_box.dart index ed73e20a1..d1a2a0688 100644 --- a/lib/widgets/nt_widgets/single_topic/boolean_box.dart +++ b/lib/widgets/nt_widgets/single_topic/boolean_box.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:dot_cast/dot_cast.dart'; @@ -206,7 +207,7 @@ class BooleanBox extends NTWidget { BooleanBoxModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { bool value = tryCast(data) ?? false; diff --git a/lib/widgets/nt_widgets/single_topic/large_text_display.dart b/lib/widgets/nt_widgets/single_topic/large_text_display.dart index d8c4e0c16..c62dad322 100644 --- a/lib/widgets/nt_widgets/single_topic/large_text_display.dart +++ b/lib/widgets/nt_widgets/single_topic/large_text_display.dart @@ -15,7 +15,7 @@ class LargeTextDisplay extends NTWidget { SingleTopicNTWidgetModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, value, _) { String data = value?.toString() ?? ''; diff --git a/lib/widgets/nt_widgets/single_topic/match_time.dart b/lib/widgets/nt_widgets/single_topic/match_time.dart index 2133c8881..df6e05d16 100644 --- a/lib/widgets/nt_widgets/single_topic/match_time.dart +++ b/lib/widgets/nt_widgets/single_topic/match_time.dart @@ -179,7 +179,7 @@ class MatchTimeWidget extends NTWidget { MatchTimeModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { double time = tryCast(data) ?? -1.0; time = time.floorToDouble(); diff --git a/lib/widgets/nt_widgets/single_topic/multi_color_view.dart b/lib/widgets/nt_widgets/single_topic/multi_color_view.dart index 1c43c742d..e44b37180 100644 --- a/lib/widgets/nt_widgets/single_topic/multi_color_view.dart +++ b/lib/widgets/nt_widgets/single_topic/multi_color_view.dart @@ -15,7 +15,7 @@ class MultiColorView extends NTWidget { SingleTopicNTWidgetModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { List hexStringsRaw = data?.tryCast>() ?? []; List hexStrings = hexStringsRaw.whereType().toList(); diff --git a/lib/widgets/nt_widgets/single_topic/number_bar.dart b/lib/widgets/nt_widgets/single_topic/number_bar.dart index dd11f1800..6f2e839c9 100644 --- a/lib/widgets/nt_widgets/single_topic/number_bar.dart +++ b/lib/widgets/nt_widgets/single_topic/number_bar.dart @@ -208,7 +208,7 @@ class NumberBar extends NTWidget { input.toStringAsFixed(input.truncateToDouble() == input ? 0 : 2); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { double value = tryCast(data)?.toDouble() ?? 0.0; diff --git a/lib/widgets/nt_widgets/single_topic/number_slider.dart b/lib/widgets/nt_widgets/single_topic/number_slider.dart index e6313cb45..91ebeb82e 100644 --- a/lib/widgets/nt_widgets/single_topic/number_slider.dart +++ b/lib/widgets/nt_widgets/single_topic/number_slider.dart @@ -197,13 +197,13 @@ class NumberSlider extends NTWidget { return ListenableBuilder( listenable: Listenable.merge([ - model.subscription!, + model.subscription, model.displayValue, model.dragging, ]), builder: (context, child) { double value = - tryCast(model.subscription!.value)?.toDouble() ?? 0.0; + tryCast(model.subscription?.value)?.toDouble() ?? 0.0; double clampedValue = value.clamp(model.minValue, model.maxValue); diff --git a/lib/widgets/nt_widgets/single_topic/radial_gauge.dart b/lib/widgets/nt_widgets/single_topic/radial_gauge.dart index 43f32668c..4439bc3aa 100644 --- a/lib/widgets/nt_widgets/single_topic/radial_gauge.dart +++ b/lib/widgets/nt_widgets/single_topic/radial_gauge.dart @@ -305,7 +305,7 @@ class RadialGaugeWidget extends NTWidget { RadialGaugeModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { double value = tryCast(data)?.toDouble() ?? 0.0; diff --git a/lib/widgets/nt_widgets/single_topic/single_color_view.dart b/lib/widgets/nt_widgets/single_topic/single_color_view.dart index 34ce34992..cb6403238 100644 --- a/lib/widgets/nt_widgets/single_topic/single_color_view.dart +++ b/lib/widgets/nt_widgets/single_topic/single_color_view.dart @@ -15,7 +15,7 @@ class SingleColorView extends NTWidget { SingleTopicNTWidgetModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { String hexString = tryCast(data) ?? ''; diff --git a/lib/widgets/nt_widgets/single_topic/text_display.dart b/lib/widgets/nt_widgets/single_topic/text_display.dart index 7c0d762df..d25ad2762 100644 --- a/lib/widgets/nt_widgets/single_topic/text_display.dart +++ b/lib/widgets/nt_widgets/single_topic/text_display.dart @@ -40,7 +40,11 @@ class TextDisplayModel extends SingleTopicNTWidgetModel { if (preferences.getBool(PrefKeys.autoTextSubmitButton) ?? false) { showSubmitButton ??= true; } else { - showSubmitButton ??= ntConnection.getTopicFromName(topic)?.isPersistent; + if (topic != null) { + showSubmitButton ??= ntConnection + .getTopicFromName(topic!) + ?.isPersistent; + } showSubmitButton ??= false; } _showSubmitButton = showSubmitButton; @@ -109,9 +113,9 @@ class TextDisplay extends NTWidget { ThemeData themeData = Theme.of(context); return ListenableBuilder( - listenable: Listenable.merge([model.subscription!, model.controller]), + listenable: Listenable.merge([model.subscription, model.controller]), builder: (context, child) { - Object? data = model.subscription!.value; + Object? data = model.subscription?.value; if (data?.toString() != model.previousValue?.toString()) { // Needed to prevent errors diff --git a/lib/widgets/nt_widgets/single_topic/toggle_button.dart b/lib/widgets/nt_widgets/single_topic/toggle_button.dart index 95b8945d8..a8234c331 100644 --- a/lib/widgets/nt_widgets/single_topic/toggle_button.dart +++ b/lib/widgets/nt_widgets/single_topic/toggle_button.dart @@ -15,13 +15,15 @@ class ToggleButton extends NTWidget { SingleTopicNTWidgetModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { bool value = tryCast(data) ?? false; - String buttonText = model.topic.substring( - model.topic.lastIndexOf('/') + 1, - ); + String buttonText = (model.topic != null) + ? model.topic!.substring( + model.topic!.lastIndexOf('/') + 1, + ) + : ''; Size buttonSize = MediaQuery.of(context).size; diff --git a/lib/widgets/nt_widgets/single_topic/toggle_switch.dart b/lib/widgets/nt_widgets/single_topic/toggle_switch.dart index 99a595954..fef4880d1 100644 --- a/lib/widgets/nt_widgets/single_topic/toggle_switch.dart +++ b/lib/widgets/nt_widgets/single_topic/toggle_switch.dart @@ -15,7 +15,7 @@ class ToggleSwitch extends NTWidget { SingleTopicNTWidgetModel model = cast(context.watch()); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { bool value = tryCast(data) ?? false; diff --git a/lib/widgets/nt_widgets/single_topic/voltage_view.dart b/lib/widgets/nt_widgets/single_topic/voltage_view.dart index 1d5145589..8ae6182e2 100644 --- a/lib/widgets/nt_widgets/single_topic/voltage_view.dart +++ b/lib/widgets/nt_widgets/single_topic/voltage_view.dart @@ -208,7 +208,7 @@ class VoltageView extends NTWidget { input.toStringAsFixed(input.truncateToDouble() == input ? 0 : 2); return ValueListenableBuilder( - valueListenable: model.subscription!, + valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { double voltage = tryCast(data)?.toDouble() ?? 0.0; From 7fc8d776212a888e9fa96fba65dba16c3984ca97 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:26:24 -0500 Subject: [PATCH 2/5] Fix lints --- lib/widgets/nt_widgets/single_topic/boolean_box.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/widgets/nt_widgets/single_topic/boolean_box.dart b/lib/widgets/nt_widgets/single_topic/boolean_box.dart index d1a2a0688..58bf2dabc 100644 --- a/lib/widgets/nt_widgets/single_topic/boolean_box.dart +++ b/lib/widgets/nt_widgets/single_topic/boolean_box.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:dot_cast/dot_cast.dart'; From e81e54ccaefd5ac81efd74f940d1b0faeb8a6f46 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:43:22 -0500 Subject: [PATCH 3/5] Fixed multi color view --- lib/widgets/nt_widgets/single_topic/multi_color_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/nt_widgets/single_topic/multi_color_view.dart b/lib/widgets/nt_widgets/single_topic/multi_color_view.dart index e44b37180..aaf7f6603 100644 --- a/lib/widgets/nt_widgets/single_topic/multi_color_view.dart +++ b/lib/widgets/nt_widgets/single_topic/multi_color_view.dart @@ -17,7 +17,7 @@ class MultiColorView extends NTWidget { return ValueListenableBuilder( valueListenable: model.subscription ?? ValueNotifier(null), builder: (context, data, child) { - List hexStringsRaw = data?.tryCast>() ?? []; + List hexStringsRaw = tryCast>(data) ?? []; List hexStrings = hexStringsRaw.whereType().toList(); List colors = []; From f4dbea28607438342aac4a847c99c90afc56028f Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:08:23 -0500 Subject: [PATCH 4/5] More styling improvements --- lib/pages/dashboard/add_widget_dialog.dart | 1 - .../dialog_widgets/nt_widget_drag_tile.dart | 19 ++++++++++++------- .../models/nt_widget_container_model.dart | 3 ++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/pages/dashboard/add_widget_dialog.dart b/lib/pages/dashboard/add_widget_dialog.dart index b332304b8..de21b4838 100644 --- a/lib/pages/dashboard/add_widget_dialog.dart +++ b/lib/pages/dashboard/add_widget_dialog.dart @@ -119,7 +119,6 @@ class _AddWidgetDialogState extends State { children: [ ExpansionTile( title: const Text('Network Tables Widgets'), - childrenPadding: const EdgeInsets.only(left: 16), children: NTWidgetRegistry.registeredWidgetNames .sorted((a, b) => a.compareTo(b)) .map( diff --git a/lib/widgets/dialog_widgets/nt_widget_drag_tile.dart b/lib/widgets/dialog_widgets/nt_widget_drag_tile.dart index 1e2343e12..e0048112a 100644 --- a/lib/widgets/dialog_widgets/nt_widget_drag_tile.dart +++ b/lib/widgets/dialog_widgets/nt_widget_drag_tile.dart @@ -142,13 +142,18 @@ class _NTWidgetDragTileState extends State { setState(() => draggingWidget = null); }, - child: Padding( - padding: const EdgeInsetsDirectional.only(start: 16), - child: ListTile( - dense: true, - contentPadding: const EdgeInsets.only(right: 20), - title: Text(widget.widgetName), - ), + child: Column( + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 32), + child: ListTile( + dense: true, + contentPadding: const EdgeInsets.only(right: 20), + title: Text(widget.widgetName), + ), + ), + const Divider(height: 0), + ], ), ), ); diff --git a/lib/widgets/draggable_containers/models/nt_widget_container_model.dart b/lib/widgets/draggable_containers/models/nt_widget_container_model.dart index 179549dcf..3639761ba 100644 --- a/lib/widgets/draggable_containers/models/nt_widget_container_model.dart +++ b/lib/widgets/draggable_containers/models/nt_widget_container_model.dart @@ -204,11 +204,12 @@ class NTWidgetContainerModel extends WidgetContainerModel { Flexible( child: DialogTextInput( onSubmit: (value) { - childModel.topic = value; + childModel.topic = value.isNotEmpty ? value : null; childModel.resetSubscription(); }, label: 'Topic', initialText: childModel.topic, + allowEmptySubmission: true, ), ), const SizedBox(width: 5), From 1fabc7688bfe855e2034ad0b817ec440a5517c91 Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:58:57 -0500 Subject: [PATCH 5/5] Renamed "Layouts" tab to "Widgets" --- lib/pages/dashboard/add_widget_dialog.dart | 2 +- test/pages/dashboard_page_test.dart | 96 ++++++++++++++++++++-- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/lib/pages/dashboard/add_widget_dialog.dart b/lib/pages/dashboard/add_widget_dialog.dart index de21b4838..29285029a 100644 --- a/lib/pages/dashboard/add_widget_dialog.dart +++ b/lib/pages/dashboard/add_widget_dialog.dart @@ -97,7 +97,7 @@ class _AddWidgetDialogState extends State { const TabBar( tabs: [ Tab(text: 'Network Tables'), - Tab(text: 'Layouts'), + Tab(text: 'Widgets'), ], ), const SizedBox(height: 5), diff --git a/test/pages/dashboard_page_test.dart b/test/pages/dashboard_page_test.dart index aab368979..49ddc1262 100644 --- a/test/pages/dashboard_page_test.dart +++ b/test/pages/dashboard_page_test.dart @@ -20,12 +20,14 @@ import 'package:elastic_dashboard/services/ip_address_util.dart'; import 'package:elastic_dashboard/services/nt4_client.dart'; import 'package:elastic_dashboard/services/nt4_type.dart'; import 'package:elastic_dashboard/services/nt_connection.dart'; +import 'package:elastic_dashboard/services/nt_widget_registry.dart'; import 'package:elastic_dashboard/services/settings.dart'; import 'package:elastic_dashboard/services/update_checker.dart'; import 'package:elastic_dashboard/widgets/custom_appbar.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_dropdown_chooser.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_text_input.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/layout_drag_tile.dart'; +import 'package:elastic_dashboard/widgets/dialog_widgets/nt_widget_drag_tile.dart'; import 'package:elastic_dashboard/widgets/draggable_containers/draggable_list_layout.dart'; import 'package:elastic_dashboard/widgets/draggable_containers/draggable_widget_container.dart'; import 'package:elastic_dashboard/widgets/draggable_dialog.dart'; @@ -390,10 +392,10 @@ void main() { findsOneWidget, ); - final layoutsTab = find.text('Layouts'); - expect(layoutsTab, findsOneWidget); + final widgetsTab = find.text('Widgets'); + expect(widgetsTab, findsOneWidget); - await widgetTester.tap(layoutsTab); + await widgetTester.tap(widgetsTab); await widgetTester.pumpAndSettle(); final listLayoutContainer = find.widgetWithText( @@ -583,10 +585,10 @@ void main() { findsOneWidget, ); - final layoutsTab = find.text('Layouts'); - expect(layoutsTab, findsOneWidget); + final widgetsTab = find.text('Widgets'); + expect(widgetsTab, findsOneWidget); - await widgetTester.tap(layoutsTab); + await widgetTester.tap(widgetsTab); await widgetTester.pumpAndSettle(); final listLayoutContainer = find.widgetWithText( @@ -661,6 +663,88 @@ void main() { expect(testBooleanInLayout, findsNothing); }); + + testWidgets('Widgets', skip: true, (widgetTester) async { + SharedPreferences.setMockInitialValues({}); + await pumpDashboardPage( + widgetTester, + await SharedPreferences.getInstance(), + ntConnection: createMockOnlineNT4(), + ); + + final addWidget = find.widgetWithText(MenuItemButton, 'Add Widget'); + + expect(addWidget, findsOneWidget); + expect(find.widgetWithText(DraggableDialog, 'Add Widget'), findsNothing); + + MenuItemButton addWidgetButton = + addWidget.evaluate().first.widget as MenuItemButton; + + addWidgetButton.onPressed?.call(); + + await widgetTester.pumpAndSettle(); + + expect( + find.widgetWithText(DraggableDialog, 'Add Widget'), + findsOneWidget, + ); + + final widgetsTab = find.text('Widgets'); + expect(widgetsTab, findsOneWidget); + + await widgetTester.tap(widgetsTab); + await widgetTester.pumpAndSettle(); + + final widgetsDropdown = find + .widgetWithText( + ExpansionTile, + 'Network Tables Widgets', + ) + .hitTestable(); + expect(widgetsDropdown, findsOneWidget); + + expect(find.byType(NTWidgetDragTile), findsNothing); + + await widgetTester.tap(widgetsDropdown); + await widgetTester.pumpAndSettle(); + + expect(find.byType(NTWidgetDragTile), findsWidgets); + + Future testWidgetDragOut(String typeName) async { + final dragOutTile = find + .widgetWithText(NTWidgetDragTile, typeName) + .hitTestable(); + expect(dragOutTile, findsOneWidget); + + await widgetTester.ensureVisible(dragOutTile); + + final widget = find.widgetWithText(WidgetContainer, typeName); + expect(widget, findsNothing); + + final gesture = await widgetTester.startGesture( + widgetTester.getCenter(dragOutTile), + kind: PointerDeviceKind.mouse, + ); + await widgetTester.pump(); + await gesture.moveBy( + const Offset(100, 0), + timeStamp: Duration(milliseconds: 100), + ); + await widgetTester.pump(); + + expect(widget, findsOneWidget); + + await gesture.moveBy(const Offset(100, 0)); + await gesture.up(); + + await widgetTester.pumpAndSettle(); + } + + NTWidgetRegistry.ensureInitialized(); + for (final name in NTWidgetRegistry.registeredWidgetNames) { + await testWidgetDragOut(name); + } + }); }); group('[ElasticLib]', () {