diff --git a/example/hotspot_provisioning_example_app/pubspec.lock b/example/hotspot_provisioning_example_app/pubspec.lock index b7b08ba0422..3a9ae3bc49f 100644 --- a/example/hotspot_provisioning_example_app/pubspec.lock +++ b/example/hotspot_provisioning_example_app/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -300,26 +300,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -633,10 +633,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.4" typed_data: dependency: transitive description: @@ -657,10 +657,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" viam_flutter_hotspot_provisioning_widget: dependency: "direct main" description: @@ -717,5 +717,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.27.0" diff --git a/lib/widgets/resources/arm.dart b/lib/widgets/resources/arm.dart index d9de9c26d6a..4c12adbd37d 100644 --- a/lib/widgets/resources/arm.dart +++ b/lib/widgets/resources/arm.dart @@ -1,171 +1,45 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import '../../viam_sdk.dart'; -import '../../widgets.dart'; +import 'arm_widgets/joint_positions_widget.dart'; +import 'arm_widgets/pose_widget.dart'; + +class ArmNotifier extends ChangeNotifier { + void armHasMoved() { + notifyListeners(); + } +} /// A widget to control an [Arm]. -class ViamArmWidget extends StatefulWidget { +class ViamArmWidgetNew extends StatefulWidget { /// The [Arm] final Arm arm; - const ViamArmWidget({ + const ViamArmWidgetNew({ super.key, required this.arm, }); @override - State createState() => _ViamArmWidgetState(); -} - -enum _PoseField { - x, - y, - z, - theta, - oX, - oY, - oZ; - - String get title { - switch (this) { - case x: - return 'X'; - case y: - return 'Y'; - case z: - return 'Z'; - case theta: - return 'Theta'; - case oX: - return 'OX'; - case oY: - return 'OY'; - case oZ: - return 'OZ'; - } - } + State createState() => _ViamArmWidgetNewState(); } -class _ViamArmWidgetState extends State { - Pose endPosition = Pose(); - List jointPositions = []; - - Future _getPositions() async { - final ep = await widget.arm.endPosition(); - final jp = await widget.arm.jointPositions(); - setState(() { - jointPositions = jp; - endPosition = ep; - }); - } +class _ViamArmWidgetNewState extends State { + final ArmNotifier _armNotifier = ArmNotifier(); @override - void initState() { - super.initState(); - _getPositions(); - } - - Future updateEndPosition(_PoseField field, double increment) async { - final ep = endPosition; - switch (field) { - case _PoseField.x: - ep.x += increment; - case _PoseField.y: - ep.y += increment; - case _PoseField.z: - ep.z += increment; - case _PoseField.theta: - ep.theta += increment; - case _PoseField.oX: - ep.oX += increment; - case _PoseField.oY: - ep.oY += increment; - case _PoseField.oZ: - ep.oZ += increment; - } - - await widget.arm.moveToPosition(ep); - await _getPositions(); - } - - Future updateJointPosition(int joint, double increment) async { - final jp = jointPositions; - jp[joint] += increment; - await widget.arm.moveToJointPositions(jp); - await _getPositions(); - } - - TableRow _getEndPositionRow(_PoseField field) { - double value; - switch (field) { - case _PoseField.x: - value = endPosition.x; - case _PoseField.y: - value = endPosition.y; - case _PoseField.z: - value = endPosition.z; - case _PoseField.theta: - value = endPosition.theta; - case _PoseField.oX: - value = endPosition.oX; - case _PoseField.oY: - value = endPosition.oY; - case _PoseField.oZ: - value = endPosition.oZ; - } - - return TableRow(children: [ - _ArmTableCell(Text(field.title, textAlign: TextAlign.end)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, -10), text: '--', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, -1), text: '-', size: ViamButtonSizeClass.small)), - _ArmTableCell(Text(value.toStringAsFixed(2), textAlign: TextAlign.center)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, 1), text: '+', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateEndPosition(field, 10), text: '++', size: ViamButtonSizeClass.small)), - ]); + void dispose() { + _armNotifier.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text('End Positions (mm)', style: TextStyle(fontWeight: FontWeight.bold)), - Table( - columnWidths: const {0: IntrinsicColumnWidth()}, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: _PoseField.values.map((e) => _getEndPositionRow(e)).toList(), - ), - const SizedBox(height: 16), - const Text('Joints (degrees)', style: TextStyle(fontWeight: FontWeight.bold)), - Table( - columnWidths: const {0: IntrinsicColumnWidth()}, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: jointPositions - .mapIndexed((index, element) => TableRow(children: [ - _ArmTableCell(Text('Joint $index', textAlign: TextAlign.end)), - _ArmTableCell( - ViamButton(onPressed: () => updateJointPosition(index, -10), text: '--', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateJointPosition(index, -1), text: '-', size: ViamButtonSizeClass.small)), - _ArmTableCell(Text(element.toStringAsFixed(2), textAlign: TextAlign.center)), - _ArmTableCell(ViamButton(onPressed: () => updateJointPosition(index, 1), text: '+', size: ViamButtonSizeClass.small)), - _ArmTableCell(ViamButton(onPressed: () => updateJointPosition(index, 10), text: '++', size: ViamButtonSizeClass.small)), - ])) - .toList(), - ) + JointPositionsWidget(arm: widget.arm, updateNotifier: _armNotifier), + PoseWidget(arm: widget.arm, updateNotifier: _armNotifier), ], ); } } - -class _ArmTableCell extends StatelessWidget { - final Widget child; - - const _ArmTableCell(this.child); - - @override - Widget build(BuildContext context) { - return TableCell(child: Padding(padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1), child: child)); - } -} diff --git a/lib/widgets/resources/arm_new.dart b/lib/widgets/resources/arm_new.dart deleted file mode 100644 index d52efe4e5da..00000000000 --- a/lib/widgets/resources/arm_new.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../viam_sdk.dart'; -import 'arm_widgets/joint_positions_widget.dart'; -import 'arm_widgets/pose_widget.dart'; - -/// A widget to control an [Arm]. -class ViamArmWidgetNew extends StatelessWidget { - /// The [Arm] - final Arm arm; - - const ViamArmWidgetNew({ - super.key, - required this.arm, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - JointPositionsWidget(arm: arm), - PoseWidget(arm: arm), - ], - ); - } -} diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index 07cf1a68cd8..1f048d1b935 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -2,33 +2,81 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../viam_sdk.dart' as viam; +import '../arm.dart'; class JointPositionsWidget extends StatefulWidget { final viam.Arm arm; - const JointPositionsWidget({super.key, required this.arm}); + final ArmNotifier updateNotifier; + const JointPositionsWidget({ + super.key, + required this.arm, + required this.updateNotifier, + }); @override State createState() => _JointPositionsWidgetState(); } -bool _isLive = false; - class _JointPositionsWidgetState extends State { List _jointValues = []; + bool _isLive = false; + List _textControllers = []; @override void initState() { super.initState(); + widget.updateNotifier.addListener(_getJointPositions); _getJointPositions(); } + @override + void dispose() { + widget.updateNotifier.removeListener(_getJointPositions); + for (final controller in _textControllers) { + controller.dispose(); + } + super.dispose(); + } + Future _getJointPositions() async { + for (final controller in _textControllers) { + controller.dispose(); + } + _jointValues = await widget.arm.jointPositions(); - setState(() {}); + _textControllers = List.generate( + _jointValues.length, + (index) => TextEditingController(text: _jointValues[index].toStringAsFixed(1)), + ); + if (mounted) { + setState(() {}); + } + } + + void _updateJointValue(int index, double value) { + const double minPosition = -359.0; + const double maxPosition = 359.0; + final clampedValue = value.clamp(minPosition, maxPosition); + + setState(() { + _jointValues[index] = clampedValue; + final formattedValue = clampedValue.toStringAsFixed(1); + if (_textControllers[index].text != formattedValue) { + _textControllers[index].text = formattedValue; + _textControllers[index].selection = TextSelection.fromPosition( + TextPosition(offset: _textControllers[index].text.length), + ); + } + }); + + if (_isLive) { + _setJointPositions(); + } } Future _setJointPositions() async { await widget.arm.moveToJointPositions(_jointValues); + widget.updateNotifier.armHasMoved(); } @override @@ -50,7 +98,18 @@ class _JointPositionsWidgetState extends State { children: _jointValues.isEmpty ? [CircularProgressIndicator.adaptive()] : List.generate(_jointValues.length, (index) { - return _BuildJointControlRow(index: index, arm: widget.arm, startJointValues: _jointValues); + return _BuildJointControlRow( + index: index, + value: _jointValues[index], + controller: _textControllers[index], + onSliderChanged: (newValue) => _updateJointValue(index, newValue), + onSubmitted: (newValue) { + final parsedValue = double.tryParse(newValue) ?? _jointValues[index]; + _updateJointValue(index, parsedValue); + }, + onDecrement: () => _updateJointValue(index, _jointValues[index] - 1.0), + onIncrement: () => _updateJointValue(index, _jointValues[index] + 1.0), + ); }), ), ), @@ -84,109 +143,79 @@ class _JointPositionsWidgetState extends State { } } -class _BuildJointControlRow extends StatefulWidget { - final int index; - final viam.Arm arm; - final List startJointValues; - const _BuildJointControlRow({required this.index, required this.arm, required this.startJointValues}); - - @override - State<_BuildJointControlRow> createState() => _BuildJointControlRowState(); -} - -class _BuildJointControlRowState extends State<_BuildJointControlRow> { +class _BuildJointControlRow extends StatelessWidget { static const double _minPosition = -359.0; static const double _maxPosition = 359.0; - List _jointValues = []; - List _textControllers = []; - - @override - void initState() { - _jointValues = widget.startJointValues; - _textControllers = List.generate( - _jointValues.length, - (index) => TextEditingController(text: _jointValues[index].toStringAsFixed(1)), - ); - super.initState(); - } - - @override - void dispose() { - for (final controller in _textControllers) { - controller.dispose(); - } - super.dispose(); - } - - void _updateJointValue(int index, double value) { - final clampedValue = value.clamp(_minPosition, _maxPosition); - - setState(() { - _jointValues[index] = clampedValue; - final formattedValue = clampedValue.toStringAsFixed(1); - if (_textControllers[index].text != formattedValue) { - _textControllers[index].text = formattedValue; - _textControllers[index].selection = TextSelection.fromPosition( - TextPosition(offset: _textControllers[index].text.length), - ); - } - }); - - if (_isLive) { - widget.arm.moveToJointPositions(_jointValues); - } - } + final int index; + final double value; + final TextEditingController controller; + final ValueChanged onSliderChanged; + final ValueChanged onSubmitted; + final VoidCallback onIncrement; + final VoidCallback onDecrement; + + const _BuildJointControlRow({ + required this.index, + required this.value, + required this.controller, + required this.onSliderChanged, + required this.onSubmitted, + required this.onIncrement, + required this.onDecrement, + }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( + child: Column( children: [ - SizedBox( - width: 30, - child: Text( - 'J${widget.index + 1}', - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Expanded( - child: Slider( - value: _jointValues[widget.index], - min: _minPosition, - max: _maxPosition, - divisions: (_maxPosition - _minPosition).toInt(), - onChanged: (newValue) => _updateJointValue(widget.index, newValue), - ), - ), - SizedBox( - width: 70, - child: TextField( - controller: _textControllers[widget.index], - textAlign: TextAlign.center, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), - ], - onSubmitted: (newValue) { - final parsedValue = double.tryParse(newValue) ?? _jointValues[widget.index]; - _updateJointValue(widget.index, parsedValue); - }, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - _updateJointValue(widget.index, _jointValues[widget.index] - 1.0); - }, + Row( + children: [ + SizedBox( + width: 35, + child: Text( + 'J${index + 1}', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + SizedBox( + width: 70, + child: TextField( + controller: controller, + textAlign: TextAlign.center, + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), + ], + onSubmitted: onSubmitted, + ), + ), + Spacer(), + IconButton( + icon: const Icon(Icons.remove), + onPressed: onDecrement, + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: onIncrement, + ), + ], ), - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - _updateJointValue(widget.index, _jointValues[widget.index] + 1.0); - }, + Row( + children: [ + SizedBox(width: 15), + Expanded( + child: Slider( + value: value, + min: _minPosition, + max: _maxPosition, + divisions: (_maxPosition - _minPosition).round(), + onChanged: onSliderChanged, + ), + ), + ], ), ], ), diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart index d77ba018f5c..601c49750ce 100644 --- a/lib/widgets/resources/arm_widgets/pose_widget.dart +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -5,6 +5,7 @@ import 'package:grpc/grpc.dart'; import '../../../protos/common/common.dart'; import '../../../src/utils.dart'; import '../../../viam_sdk.dart' as viam; +import '../arm.dart'; class _TextControlStruct { TextEditingController x; @@ -20,7 +21,12 @@ class _TextControlStruct { class PoseWidget extends StatefulWidget { final viam.Arm arm; - const PoseWidget({super.key, required this.arm}); + final ArmNotifier updateNotifier; + const PoseWidget({ + super.key, + required this.arm, + required this.updateNotifier, + }); @override State createState() => _PoseWidgetState(); @@ -38,27 +44,37 @@ class _PoseWidgetState extends State { bool _isGoingToPose = false; Pose _controlValues = Pose(); - late final _TextControlStruct _textControllers; + _TextControlStruct? _textControllers; @override void initState() { super.initState(); + widget.updateNotifier.addListener(_getStartPose); _getStartPose(); } @override void dispose() { - _textControllers.x.dispose(); - _textControllers.y.dispose(); - _textControllers.z.dispose(); - _textControllers.oX.dispose(); - _textControllers.oY.dispose(); - _textControllers.oZ.dispose(); - _textControllers.theta.dispose(); + widget.updateNotifier.removeListener(_getStartPose); + _disposeControllers(); super.dispose(); } + void _disposeControllers() { + if (_textControllers != null) { + _textControllers!.x.dispose(); + _textControllers!.y.dispose(); + _textControllers!.z.dispose(); + _textControllers!.oX.dispose(); + _textControllers!.oY.dispose(); + _textControllers!.oZ.dispose(); + _textControllers!.theta.dispose(); + } + } + Future _getStartPose() async { + _disposeControllers(); + final startPose = await widget.arm.endPosition(); _controlValues = startPose; _textControllers = _TextControlStruct( @@ -75,23 +91,26 @@ class _PoseWidgetState extends State { Future _updatePose() async { try { - if (!_isGoingToPose) { - setState(() { - _isGoingToPose = true; - }); - await widget.arm.moveToPosition(_controlValues); + if (_isGoingToPose) return; + setState(() { + _isGoingToPose = true; + }); + await widget.arm.moveToPosition(_controlValues); + widget.updateNotifier.armHasMoved(); + } on GrpcError catch (e) { + if (mounted) await showErrorDialog(context, title: 'An error occurred', error: e.message); + } finally { + if (mounted) { setState(() { _isGoingToPose = false; }); } - } on GrpcError catch (e) { - if (mounted) await showErrorDialog(context, title: 'An error occurred', error: e.message); } } - void _updateControlValue(String index, TextEditingController textController, double value) { + void _updateControlValue(String axis, TextEditingController textController, double value) { setState(() { - switch (index) { + switch (axis) { case 'x': _controlValues.x = value; case 'y': @@ -120,149 +139,151 @@ class _PoseWidgetState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - Divider(), - Text( - 'Pose Values', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Divider(), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, + return _textControllers == null + ? Center(child: CircularProgressIndicator.adaptive()) + : Column( children: [ - _BuildJointControlRow( - label: 'X', - value: _controlValues.x.roundToDouble(), - controller: _textControllers.x, - min: _minPosition, - max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue( - 'x', - _textControllers.x, - newValue.clamp(_minPosition, _maxPosition), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, - ), - _BuildJointControlRow( - label: 'Y', - value: _controlValues.y.roundToDouble(), - controller: _textControllers.y, - min: _minPosition, - max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue( - 'y', - _textControllers.y, - newValue.clamp(_minPosition, _maxPosition), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, - ), - _BuildJointControlRow( - label: 'Z', - value: _controlValues.z.roundToDouble(), - controller: _textControllers.z, - min: _minPosition, - max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue( - 'z', - _textControllers.z, - newValue.clamp(_minPosition, _maxPosition), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, - ), - _BuildJointControlRow( - label: 'OX', - value: _controlValues.oX.roundToDouble(), - controller: _textControllers.oX, - min: _minOrientation, - max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue( - 'oX', - _textControllers.oX, - newValue.clamp(_minOrientation, _maxOrientation), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, - ), - _BuildJointControlRow( - label: 'OY', - value: _controlValues.oY.roundToDouble(), - controller: _textControllers.oY, - min: _minOrientation, - max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue( - 'oY', - _textControllers.oY, - newValue.clamp(_minOrientation, _maxOrientation), + Divider(), + Text( + 'Pose Values', + style: TextStyle( + fontWeight: FontWeight.bold, ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), - _BuildJointControlRow( - label: 'OZ', - value: _controlValues.oZ.roundToDouble(), - controller: _textControllers.oZ, - min: _minOrientation, - max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue( - 'oZ', - _textControllers.oZ, - newValue.clamp(_minOrientation, _maxOrientation), + Divider(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _BuildJointControlRow( + label: 'X', + value: _controlValues.x.roundToDouble(), + controller: _textControllers!.x, + min: _minPosition, + max: _maxPosition, + onValueChanged: (newValue) => _updateControlValue( + 'x', + _textControllers!.x, + newValue.clamp(_minPosition, _maxPosition), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + ), + _BuildJointControlRow( + label: 'Y', + value: _controlValues.y.roundToDouble(), + controller: _textControllers!.y, + min: _minPosition, + max: _maxPosition, + onValueChanged: (newValue) => _updateControlValue( + 'y', + _textControllers!.y, + newValue.clamp(_minPosition, _maxPosition), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + ), + _BuildJointControlRow( + label: 'Z', + value: _controlValues.z.roundToDouble(), + controller: _textControllers!.z, + min: _minPosition, + max: _maxPosition, + onValueChanged: (newValue) => _updateControlValue( + 'z', + _textControllers!.z, + newValue.clamp(_minPosition, _maxPosition), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + ), + _BuildJointControlRow( + label: 'OX', + value: _controlValues.oX.roundToDouble(), + controller: _textControllers!.oX, + min: _minOrientation, + max: _maxOrientation, + onValueChanged: (newValue) => _updateControlValue( + 'oX', + _textControllers!.oX, + newValue.clamp(_minOrientation, _maxOrientation), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + ), + _BuildJointControlRow( + label: 'OY', + value: _controlValues.oY.roundToDouble(), + controller: _textControllers!.oY, + min: _minOrientation, + max: _maxOrientation, + onValueChanged: (newValue) => _updateControlValue( + 'oY', + _textControllers!.oY, + newValue.clamp(_minOrientation, _maxOrientation), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + ), + _BuildJointControlRow( + label: 'OZ', + value: _controlValues.oZ.roundToDouble(), + controller: _textControllers!.oZ, + min: _minOrientation, + max: _maxOrientation, + onValueChanged: (newValue) => _updateControlValue( + 'oZ', + _textControllers!.oZ, + newValue.clamp(_minOrientation, _maxOrientation), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + ), + _BuildJointControlRow( + label: 'Theta', + value: _controlValues.theta.roundToDouble(), + controller: _textControllers!.theta, + min: _minTheta, + max: _maxTheta, + onValueChanged: (newValue) => _updateControlValue( + 'theta', + _textControllers!.theta, + newValue.clamp(_minTheta, _maxTheta), + ), + onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + ), + ], ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), - _BuildJointControlRow( - label: 'Theta', - value: _controlValues.theta.roundToDouble(), - controller: _textControllers.theta, - min: _minTheta, - max: _maxTheta, - onValueChanged: (newValue) => _updateControlValue( - 'theta', - _textControllers.theta, - newValue.clamp(_minTheta, _maxTheta), + Padding( + padding: const EdgeInsets.fromLTRB(20.0, 0, 20.0, 20.0), + child: Row( + spacing: 8, + children: [ + Switch( + value: _isLive, + onChanged: (newValue) { + setState(() { + _isLive = newValue; + }); + }, + ), + Text( + 'Live', + ), + Tooltip( + message: 'In Live mode, pose will update \non release of the slider', + textAlign: TextAlign.center, + triggerMode: TooltipTriggerMode.tap, + preferBelow: false, + child: Icon(Icons.info_outline), + ), + Spacer(), + OutlinedButton.icon( + onPressed: _isLive ? null : _updatePose, + label: Text('Execute'), + icon: Icon(Icons.play_arrow), + ), + ], ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, ), ], - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20.0, 0, 20.0, 20.0), - child: Row( - spacing: 8, - children: [ - Switch( - value: _isLive, - onChanged: (newValue) { - setState(() { - _isLive = newValue; - }); - }, - ), - Text( - "Live", - ), - Tooltip( - message: "In Live mode, pose will update \non release of the slider", - textAlign: TextAlign.center, - triggerMode: TooltipTriggerMode.tap, - preferBelow: false, - child: Icon(Icons.info_outline), - ), - Spacer(), - OutlinedButton.icon( - onPressed: _isLive ? null : _updatePose, - label: Text("Execute"), - icon: Icon(Icons.play_arrow), - ), - ], - ), - ), - ], - ); + ); } } @@ -273,7 +294,7 @@ class _BuildJointControlRow extends StatelessWidget { final double min; final double max; final ValueChanged onValueChanged; - final ValueChanged onValueChangedEnd; + final ValueChanged onValueChangedEnd; const _BuildJointControlRow({ required this.label, @@ -289,56 +310,64 @@ class _BuildJointControlRow extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( + child: Column( children: [ - SizedBox( - width: 50, - child: Text( - label, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Expanded( - child: Slider( - value: value, - min: min, - max: max, - label: value.toStringAsFixed(1), - onChanged: onValueChanged, - onChangeEnd: onValueChangedEnd, - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 70, - child: TextField( - controller: controller, - textAlign: TextAlign.center, - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^-?\d+\.?\d{0,1}')), - ], - onSubmitted: (newValue) { - final parsedValue = double.tryParse(newValue) ?? value; - onValueChanged(parsedValue); - onValueChangedEnd(parsedValue); - }, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () async { - onValueChanged(value - (max == 1 ? 0.1 : 1.0)); - onValueChangedEnd(value); - }, + Row( + children: [ + SizedBox( + width: 55, + child: Text( + label, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + SizedBox( + width: 70, + child: TextField( + controller: controller, + textAlign: TextAlign.center, + keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^-?\d+\.?\d{0,1}')), + ], + onSubmitted: (newValue) { + final parsedValue = double.tryParse(newValue) ?? value; + onValueChanged(parsedValue); + onValueChangedEnd(parsedValue); + }, + ), + ), + Spacer(), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () async { + onValueChanged(value - (max == 1 ? 0.1 : 1.0)); + onValueChangedEnd(value); + }, + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + onValueChanged(value + (max == 1 ? 0.1 : 1.0)); + onValueChangedEnd(value); + }, + ), + ], ), - IconButton( - icon: const Icon(Icons.add), - onPressed: () async { - onValueChanged(value + (max == 1 ? 0.1 : 1.0)); - onValueChangedEnd(value); - }, + Row( + children: [ + SizedBox(width: 35), + Expanded( + child: Slider( + value: value, + min: min, + max: max, + label: value.toStringAsFixed(1), + onChanged: onValueChanged, + onChangeEnd: onValueChangedEnd, + ), + ), + ], ), ], ),