Skip to content

Commit a4de100

Browse files
Widget/coral level (#1)
* Add coralLevel chooser * Change size of buttons --------- Co-authored-by: Eran Ovadia <[email protected]>
1 parent 973e9d8 commit a4de100

File tree

4 files changed

+331
-3
lines changed

4 files changed

+331
-3
lines changed

lib/services/app_distributor.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ const bool isWPILib = bool.fromEnvironment('ELASTIC_WPILIB');
22

33
const String logoPath = 'assets/logos/logo.png';
44

5-
const String appTitle = !isWPILib ? 'Elastic' : 'Elastic (WPILib)';
5+
const String appTitle = !isWPILib ? 'PurpleBoard' : 'Elastic (WPILib)';

lib/services/nt_widget_builder.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:elastic_dashboard/widgets/nt_widgets/multi_topic/coral_level_chooser.dart';
12
import 'package:flutter/material.dart';
23

34
import 'package:dot_cast/dot_cast.dart';
@@ -181,6 +182,12 @@ class NTWidgetBuilder {
181182
widget: ComboBoxChooser.new,
182183
fromJson: ComboBoxChooserModel.fromJson,
183184
minHeight: _normalSize * 0.85);
185+
186+
registerWithAlias(
187+
names: {CoralLevelChooser.widgetType, 'Coral Chooser'},
188+
model: CoralLevelChooserModel.new,
189+
widget: CoralLevelChooser.new,
190+
fromJson: CoralLevelChooserModel.fromJson);
184191

185192
register(
186193
name: CommandSchedulerWidget.widgetType,
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'package:dot_cast/dot_cast.dart';
4+
import 'package:provider/provider.dart';
5+
6+
import 'package:elastic_dashboard/services/nt4_client.dart';
7+
import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_toggle_switch.dart';
8+
import 'package:elastic_dashboard/widgets/nt_widgets/nt_widget.dart';
9+
10+
class CoralLevelChooserModel extends MultiTopicNTWidgetModel {
11+
@override
12+
String type = CoralLevelChooser.widgetType;
13+
14+
String get optionsTopicName => '$topic/options';
15+
String get selectedTopicName => '$topic/selected';
16+
String get activeTopicName => '$topic/active';
17+
String get defaultTopicName => '$topic/default';
18+
19+
late NT4Subscription optionsSubscription;
20+
late NT4Subscription selectedSubscription;
21+
late NT4Subscription activeSubscription;
22+
late NT4Subscription defaultSubscription;
23+
24+
@override
25+
List<NT4Subscription> get subscriptions => [
26+
optionsSubscription,
27+
selectedSubscription,
28+
activeSubscription,
29+
defaultSubscription,
30+
];
31+
32+
late Listenable chooserStateListenable;
33+
34+
final TextEditingController _searchController = TextEditingController();
35+
36+
String? previousDefault;
37+
String? previousSelected;
38+
String? previousActive;
39+
List<String>? previousOptions;
40+
int indexCurrnetOption = 0;
41+
42+
NT4Topic? _selectedTopic;
43+
44+
bool _sortOptions = false;
45+
46+
bool get sortOptions => _sortOptions;
47+
48+
set sortOptions(bool value) {
49+
_sortOptions = value;
50+
previousOptions?.sort();
51+
refresh();
52+
}
53+
54+
CoralLevelChooserModel({
55+
required super.ntConnection,
56+
required super.preferences,
57+
required super.topic,
58+
bool sortOptions = false,
59+
super.dataType,
60+
super.period,
61+
}) : _sortOptions = sortOptions,
62+
super();
63+
64+
CoralLevelChooserModel.fromJson({
65+
required super.ntConnection,
66+
required super.preferences,
67+
required Map<String, dynamic> jsonData,
68+
}) : super.fromJson(jsonData: jsonData) {
69+
_sortOptions = tryCast(jsonData['sort_options']) ?? _sortOptions;
70+
}
71+
72+
@override
73+
void initializeSubscriptions() {
74+
optionsSubscription =
75+
ntConnection.subscribe(optionsTopicName, super.period);
76+
selectedSubscription =
77+
ntConnection.subscribe(selectedTopicName, super.period);
78+
activeSubscription = ntConnection.subscribe(activeTopicName, super.period);
79+
defaultSubscription =
80+
ntConnection.subscribe(defaultTopicName, super.period);
81+
chooserStateListenable = Listenable.merge(subscriptions);
82+
chooserStateListenable.addListener(onChooserStateUpdate);
83+
84+
previousOptions = null;
85+
previousActive = null;
86+
previousDefault = null;
87+
previousSelected = null;
88+
89+
// Initial caching of the chooser state, when switching
90+
// topics the listener won't be called, so we have to call it manually
91+
onChooserStateUpdate();
92+
}
93+
94+
@override
95+
void resetSubscription() {
96+
_selectedTopic = null;
97+
chooserStateListenable.removeListener(onChooserStateUpdate);
98+
99+
super.resetSubscription();
100+
}
101+
102+
@override
103+
Map<String, dynamic> toJson() {
104+
return {
105+
...super.toJson(),
106+
'sort_options': _sortOptions,
107+
};
108+
}
109+
110+
@override
111+
List<Widget> getEditProperties(BuildContext context) {
112+
return [
113+
DialogToggleSwitch(
114+
label: 'Sort Options Alphabetically',
115+
initialValue: _sortOptions,
116+
onToggle: (value) {
117+
sortOptions = value;
118+
},
119+
),
120+
];
121+
}
122+
123+
void onChooserStateUpdate() {
124+
List<Object?>? rawOptions =
125+
optionsSubscription.value?.tryCast<List<Object?>>();
126+
127+
List<String>? currentOptions = rawOptions?.whereType<String>().toList();
128+
129+
if (sortOptions) {
130+
currentOptions?.sort();
131+
}
132+
133+
String? currentActive = tryCast(activeSubscription.value);
134+
if (currentActive != null && currentActive.isEmpty) {
135+
currentActive = null;
136+
}
137+
138+
String? currentSelected = tryCast(selectedSubscription.value);
139+
if (currentSelected != null && currentSelected.isEmpty) {
140+
currentSelected = null;
141+
}
142+
143+
String? currentDefault = tryCast(defaultSubscription.value);
144+
if (currentDefault != null && currentDefault.isEmpty) {
145+
currentDefault = null;
146+
}
147+
148+
bool hasValue = currentOptions != null ||
149+
currentActive != null ||
150+
currentDefault != null;
151+
152+
bool publishCurrent =
153+
hasValue && previousSelected != null && currentSelected == null;
154+
155+
// We only want to publish the selected topic if we're getting values
156+
// from the others, since it means the chooser is published on network tables
157+
if (hasValue) {
158+
publishSelectedTopic();
159+
}
160+
161+
if (currentOptions != null) {
162+
previousOptions = currentOptions;
163+
}
164+
if (currentSelected != null) {
165+
previousSelected = currentSelected;
166+
}
167+
if (currentActive != null) {
168+
previousActive = currentActive;
169+
}
170+
if (currentDefault != null) {
171+
previousDefault = currentDefault;
172+
}
173+
174+
if (publishCurrent) {
175+
publishSelectedValue(previousSelected, true);
176+
}
177+
178+
notifyListeners();
179+
}
180+
181+
void publishSelectedTopic() {
182+
if (_selectedTopic != null) {
183+
return;
184+
}
185+
186+
NT4Topic? existing = ntConnection.getTopicFromName(selectedTopicName);
187+
188+
if (existing != null) {
189+
existing.properties.addAll({
190+
'retained': true,
191+
});
192+
ntConnection.publishTopic(existing);
193+
_selectedTopic = existing;
194+
} else {
195+
_selectedTopic = ntConnection.publishNewTopic(
196+
selectedTopicName,
197+
NT4TypeStr.kString,
198+
properties: {
199+
'retained': true,
200+
},
201+
);
202+
}
203+
}
204+
205+
void publishSelectedValue(String? selected, [bool initial = false]) {
206+
if (selected == null || !ntConnection.isNT4Connected) {
207+
return;
208+
}
209+
210+
if (_selectedTopic == null) {
211+
publishSelectedTopic();
212+
}
213+
214+
ntConnection.updateDataFromTopic(
215+
_selectedTopic!,
216+
selected,
217+
initial ? 0 : null,
218+
);
219+
}
220+
}
221+
222+
class CoralLevelChooser extends NTWidget {
223+
static const String widgetType = 'CoralLevel Chooser';
224+
225+
const CoralLevelChooser({super.key}) : super();
226+
227+
@override
228+
Widget build(BuildContext context) {
229+
CoralLevelChooserModel model = cast(context.watch<NTWidgetModel>());
230+
231+
String? preview = model.previousSelected ?? model.previousDefault;
232+
233+
bool showWarning = model.previousActive != preview;
234+
235+
return Row(
236+
mainAxisSize: MainAxisSize.min,
237+
children: [
238+
Flexible(
239+
child: Container(
240+
constraints: const BoxConstraints(
241+
minHeight: 36.0,
242+
),
243+
child: _CoralLevel(
244+
selected: preview,
245+
options: model.previousOptions ?? [preview ?? ''],
246+
textController: model._searchController,
247+
onValueChanged: (int value) {
248+
model.indexCurrnetOption += value;
249+
if (model.indexCurrnetOption < 0) {
250+
model.indexCurrnetOption = 0;
251+
} else if (model.indexCurrnetOption >=
252+
model.previousOptions!.length) {
253+
model.indexCurrnetOption = model.previousOptions!.length - 1;
254+
}
255+
model.publishSelectedValue(
256+
model.previousOptions?.elementAt(model.indexCurrnetOption));
257+
},
258+
),
259+
),
260+
),
261+
const SizedBox(width: 5),
262+
(showWarning)
263+
? const Tooltip(
264+
message:
265+
'Selected value has not been published to Network Tables.\nRobot code will not be receiving the correct value.',
266+
child: Icon(Icons.priority_high, color: Colors.red),
267+
)
268+
: const Icon(Icons.check, color: Colors.green),
269+
],
270+
);
271+
}
272+
}
273+
274+
class _CoralLevel extends StatelessWidget {
275+
final List<String> options;
276+
final String? selected;
277+
final Function(int value) onValueChanged;
278+
final TextEditingController textController;
279+
280+
const _CoralLevel({
281+
required this.options,
282+
required this.onValueChanged,
283+
required this.textController,
284+
this.selected,
285+
});
286+
287+
@override
288+
Widget build(BuildContext context) {
289+
return ExcludeFocus(
290+
child: Tooltip(
291+
message: selected ?? '',
292+
waitDuration: const Duration(microseconds: 250),
293+
child: Row(
294+
children: [
295+
_createIncrementDicrementButton(
296+
Icons.remove, Colors.red, () => onValueChanged(-1)),
297+
Text("L" + (selected ?? ''), textScaleFactor: 3),
298+
_createIncrementDicrementButton(
299+
Icons.add, Colors.green, () => onValueChanged(1))
300+
],
301+
),
302+
),
303+
);
304+
}
305+
306+
Widget _createIncrementDicrementButton(
307+
IconData icon, Color color, VoidCallback? onPressed) {
308+
return RawMaterialButton(
309+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
310+
constraints: BoxConstraints(minWidth: 20.0, minHeight: 20.0),
311+
onPressed: onPressed,
312+
elevation: 2.0,
313+
child: Icon(
314+
icon,
315+
color: color,
316+
size: 40.0,
317+
),
318+
shape: CircleBorder(),
319+
);
320+
}
321+
}

lib/widgets/nt_widgets/nt_widget.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ class MultiTopicNTWidgetModel extends NTWidgetModel {
314314

315315
@override
316316
List<String> getAvailableDisplayTypes() {
317-
if (type == 'ComboBox Chooser' || type == 'Split Button Chooser') {
318-
return ['ComboBox Chooser', 'Split Button Chooser'];
317+
if (type == 'ComboBox Chooser' || type == 'Split Button Chooser' || type == 'CoralLevel Chooser') {
318+
return ['ComboBox Chooser', 'Split Button Chooser', 'CoralLevel Chooser'];
319319
}
320320

321321
return [type];

0 commit comments

Comments
 (0)