From 3991ff64c58999d798a430633ed681c1aa845143 Mon Sep 17 00:00:00 2001 From: Gold872 Date: Sat, 2 Dec 2023 21:29:59 -0500 Subject: [PATCH] Added option to resize to driver station's height --- lib/main.dart | 25 ++----- lib/pages/dashboard_page.dart | 68 ++++++++++++++++++- lib/services/ds_interop.dart | 92 +++++++++++++++++++++----- lib/services/globals.dart | 7 ++ lib/services/nt4_connection.dart | 5 +- lib/widgets/custom_appbar.dart | 17 ++++- lib/widgets/settings_dialog.dart | 52 +++++++++++---- pubspec.lock | 2 +- pubspec.yaml | 3 +- test/widgets/settings_dialog_test.dart | 47 +++++++++++++ 10 files changed, 264 insertions(+), 54 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a309a845..9e7aec46 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:ui'; import 'package:elastic_dashboard/pages/dashboard_page.dart'; import 'package:elastic_dashboard/services/field_images.dart'; @@ -10,6 +9,7 @@ import 'package:elastic_dashboard/services/nt4_connection.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:screen_retriever/screen_retriever.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; @@ -53,30 +53,19 @@ void main() async { Globals.showGrid = preferences.getBool(PrefKeys.showGrid) ?? Globals.showGrid; Globals.cornerRadius = preferences.getDouble(PrefKeys.cornerRadius) ?? Globals.cornerRadius; + Globals.autoResizeToDS = + preferences.getBool(PrefKeys.autoResizeToDS) ?? Globals.autoResizeToDS; nt4Connection .nt4Connect(preferences.getString(PrefKeys.ipAddress) ?? '127.0.0.1'); - nt4Connection.dsClientConnect((ip) async { - if (Globals.ipAddressMode != IPAddressMode.driverStation) { - return; - } - - if (preferences.getString(PrefKeys.ipAddress) != ip) { - await preferences.setString(PrefKeys.ipAddress, ip); - } else { - return; - } - - nt4Connection.changeIPAddress(ip); - }); - await FieldImages.loadFields('assets/fields/'); - FlutterView screenView = PlatformDispatcher.instance.views.first; - Size screenSize = screenView.physicalSize * screenView.devicePixelRatio; + Display primaryDisplay = await screenRetriever.getPrimaryDisplay(); + Size screenSize = + primaryDisplay.size * (primaryDisplay.scaleFactor?.toDouble() ?? 1.0); - await windowManager.setMinimumSize(screenSize * 0.55); + await windowManager.setMinimumSize(screenSize * 0.60); await windowManager.setTitleBarStyle(TitleBarStyle.hidden, windowButtonVisibility: false); diff --git a/lib/pages/dashboard_page.dart b/lib/pages/dashboard_page.dart index ad896eb9..36034f9e 100644 --- a/lib/pages/dashboard_page.dart +++ b/lib/pages/dashboard_page.dart @@ -25,6 +25,7 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:screen_retriever/screen_retriever.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; @@ -73,6 +74,29 @@ class _DashboardPageState extends State with WindowListener { setupShortcuts(); + nt4Connection.dsClientConnect( + onIPAnnounced: (ip) async { + if (Globals.ipAddressMode != IPAddressMode.driverStation) { + return; + } + + if (_preferences.getString(PrefKeys.ipAddress) != ip) { + await _preferences.setString(PrefKeys.ipAddress, ip); + } else { + return; + } + + nt4Connection.changeIPAddress(ip); + }, + onDriverStationDockChanged: (docked) { + if (Globals.autoResizeToDS && docked) { + _onDriverStationDocked(); + } else { + _onDriverStationUndocked(); + } + }, + ); + nt4Connection.addConnectedListener(() { setState(() { for (DashboardGrid grid in grids) { @@ -766,9 +790,11 @@ class _DashboardPageState extends State with WindowListener { case IPAddressMode.driverStation: String? lastAnnouncedIP = nt4Connection.dsClient.lastAnnouncedIP; - if (lastAnnouncedIP != null) { - _updateIPAddress(lastAnnouncedIP); + if (lastAnnouncedIP == null) { + break; } + + _updateIPAddress(lastAnnouncedIP); break; case IPAddressMode.roboRIOmDNS: _updateIPAddress( @@ -836,6 +862,19 @@ class _DashboardPageState extends State with WindowListener { await _preferences.setDouble(PrefKeys.cornerRadius, newRadius); }, + onResizeToDSChanged: (value) async { + setState(() { + Globals.autoResizeToDS = value; + + if (value && nt4Connection.dsClient.driverStationDocked) { + _onDriverStationDocked(); + } else { + _onDriverStationUndocked(); + } + }); + + await _preferences.setBool(PrefKeys.autoResizeToDS, value); + }, onColorChanged: widget.onColorChanged, ), ); @@ -849,6 +888,31 @@ class _DashboardPageState extends State with WindowListener { }); } + void _onDriverStationDocked() async { + Display primaryDisplay = await screenRetriever.getPrimaryDisplay(); + double pixelRatio = primaryDisplay.scaleFactor?.toDouble() ?? 1.0; + Size screenSize = primaryDisplay.size * pixelRatio; + + await windowManager.unmaximize(); + + Size newScreenSize = + Size(screenSize.width + 16, (screenSize.height + 8) - 250) / pixelRatio; + + await windowManager.setSize(newScreenSize); + + await windowManager.setAlignment(Alignment.topCenter); + + Globals.isWindowMaximizable = false; + Globals.isWindowDraggable = false; + await windowManager.setResizable(false); + } + + void _onDriverStationUndocked() async { + Globals.isWindowMaximizable = true; + Globals.isWindowDraggable = true; + await windowManager.setResizable(true); + } + void showWindowCloseConfirmation(BuildContext context) { showDialog( barrierDismissible: false, diff --git a/lib/services/ds_interop.dart b/lib/services/ds_interop.dart index 4761f873..58ca025d 100644 --- a/lib/services/ds_interop.dart +++ b/lib/services/ds_interop.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:elastic_dashboard/services/ip_address_util.dart'; +import 'package:elastic_dashboard/services/log.dart'; import 'package:flutter/foundation.dart'; class DSInteropClient { @@ -12,22 +13,35 @@ class DSInteropClient { Function()? onDisconnect; Function(String ip)? onNewIPAnnounced; + Function(bool isDocked)? onDriverStationDockChanged; - String? lastAnnouncedIP; Socket? _socket; + ServerSocket? _dbModeServer; - DSInteropClient({this.onNewIPAnnounced, this.onConnect, this.onDisconnect}) { + List _tcpBuffer = []; + + String? _lastAnnouncedIP; + bool _driverStationDocked = false; + + String? get lastAnnouncedIP => _lastAnnouncedIP; + bool get driverStationDocked => _driverStationDocked; + + DSInteropClient({ + this.onNewIPAnnounced, + this.onDriverStationDockChanged, + this.onConnect, + this.onDisconnect, + }) { _connect(); } void _connect() async { try { _socket = await Socket.connect(serverBaseAddress, 1742); + _dbModeServer = await ServerSocket.bind(serverBaseAddress, 1741); } catch (e) { - if (kDebugMode) { - print( - '[DS INTEROP] Failed to connect, attempting to reconnect in 5 seconds.'); - } + logger.debug( + '[DS INTEROP] Failed to connect, attempting to reconnect in 5 seconds.'); Future.delayed(const Duration(seconds: 5), _connect); return; } @@ -38,45 +52,91 @@ class DSInteropClient { _serverConnectionActive = true; onConnect?.call(); } - _socketOnMessage(utf8.decode(data)); + _tcpSocketOnMessage(utf8.decode(data)); + }, + onDone: _socketClose, + ); + + _dbModeServer!.listen( + (socket) { + socket.listen( + (data) { + _tcpServerOnMessage(data); + }, + ); }, onDone: _socketClose, ); } - void _socketOnMessage(String data) { + void _tcpSocketOnMessage(String data) { var jsonData = jsonDecode(data.toString()); if (jsonData is! Map) { - if (kDebugMode) { - print('[DS INTEROP] Ignoring text message, not a Json Object'); - } + logger.warning('[DS INTEROP] Ignoring text message, not a Json Object'); return; } var rawIP = jsonData['robotIP']; if (rawIP == null || rawIP == 0) { - if (kDebugMode) { - print('[DS INTEROP] Ignoring Json message, robot IP is not valid'); - } + logger + .warning('[DS INTEROP] Ignoring Json message, robot IP is not valid'); return; } String ipAddress = IPAddressUtil.getIpFromInt32Value(rawIP); - if (lastAnnouncedIP != ipAddress) { + if (_lastAnnouncedIP != ipAddress) { onNewIPAnnounced?.call(ipAddress); } - lastAnnouncedIP = ipAddress; + _lastAnnouncedIP = ipAddress; + } + + void _tcpServerOnMessage(Uint8List data) { + _tcpBuffer.addAll(data); + Map mappedData = {}; + + int sublistIndex = 0; + + for (int i = 0; i < _tcpBuffer.length - 1;) { + int size = (_tcpBuffer[i] << 8) | _tcpBuffer[i + 1]; + + if (i >= _tcpBuffer.length - 1 - size || size == 0) { + break; + } + + Uint8List sublist = + Uint8List.fromList(_tcpBuffer.sublist(i + 2, i + 2 + size)); + int tagID = sublist[0]; + mappedData[tagID] = sublist.sublist(1); + + sublistIndex = i + size + 2; + + i += size + 2; + } + + _tcpBuffer = _tcpBuffer.sublist(sublistIndex); + + if (mappedData.containsKey(0x09)) { + bool docked = (mappedData[0x09]![0] & 0x04 != 0); + _driverStationDocked = docked; + + onDriverStationDockChanged?.call(docked); + } } void _socketClose() { _socket?.close(); _socket = null; + _dbModeServer?.close(); + _dbModeServer = null; + _serverConnectionActive = false; + _driverStationDocked = false; + onDriverStationDockChanged?.call(false); onDisconnect?.call(); if (kDebugMode) { diff --git a/lib/services/globals.dart b/lib/services/globals.dart index 90334a33..fe2d469a 100644 --- a/lib/services/globals.dart +++ b/lib/services/globals.dart @@ -12,6 +12,12 @@ class Globals { static int gridSize = 128; static double cornerRadius = 15.0; static bool showGrid = false; + static bool autoResizeToDS = false; + + // window_manager doesn't support drag disable/maximize + // disable on some platforms, this is a dumb workaround for it + static bool isWindowDraggable = true; + static bool isWindowMaximizable = true; static const double defaultPeriod = 0.1; static const double defaultGraphPeriod = 0.033; @@ -26,4 +32,5 @@ class PrefKeys { static String gridSize = 'grid_size'; static String cornerRadius = 'corner_radius'; static String showGrid = 'show_grid'; + static String autoResizeToDS = 'auto_resize_to_driver_station'; } diff --git a/lib/services/nt4_connection.dart b/lib/services/nt4_connection.dart index e02359a3..846205b0 100644 --- a/lib/services/nt4_connection.dart +++ b/lib/services/nt4_connection.dart @@ -52,9 +52,12 @@ class NT4Connection { allTopicsSubscription = _ntClient.subscribeTopicsOnly('/'); } - void dsClientConnect(Function(String ip)? onIPAnnounced) { + void dsClientConnect( + {Function(String ip)? onIPAnnounced, + Function(bool isDocked)? onDriverStationDockChanged}) { _dsClient = DSInteropClient( onNewIPAnnounced: onIPAnnounced, + onDriverStationDockChanged: onDriverStationDockChanged, onConnect: () => _dsConnected = true, onDisconnect: () => _dsConnected = false, ); diff --git a/lib/widgets/custom_appbar.dart b/lib/widgets/custom_appbar.dart index 74fb830b..da5f39a9 100644 --- a/lib/widgets/custom_appbar.dart +++ b/lib/widgets/custom_appbar.dart @@ -1,3 +1,4 @@ +import 'package:elastic_dashboard/services/globals.dart'; import 'package:flutter/material.dart'; import 'package:titlebar_buttons/titlebar_buttons.dart'; import 'package:window_manager/window_manager.dart'; @@ -40,6 +41,10 @@ class CustomAppBar extends AppBar { child: DecoratedMaximizeButton( type: buttonType, onPressed: () async { + if (!Globals.isWindowMaximizable) { + return; + } + if (await windowManager.isMaximized()) { windowManager.unmaximize(); } else { @@ -97,13 +102,21 @@ class _WindowDragArea extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.translucent, onPanStart: (details) { + if (!Globals.isWindowDraggable) { + return; + } + windowManager.startDragging(); }, onDoubleTap: () async { + if (!Globals.isWindowMaximizable) { + return; + } + if (await windowManager.isMaximized()) { - windowManager.unmaximize(); + await windowManager.unmaximize(); } else { - windowManager.maximize(); + await windowManager.maximize(); } }, child: child ?? Container(), diff --git a/lib/widgets/settings_dialog.dart b/lib/widgets/settings_dialog.dart index d0a70009..26775221 100644 --- a/lib/widgets/settings_dialog.dart +++ b/lib/widgets/settings_dialog.dart @@ -20,6 +20,7 @@ class SettingsDialog extends StatefulWidget { final Function(bool value)? onGridToggle; final Function(String? gridSize)? onGridSizeChanged; final Function(String? radius)? onCornerRadiusChanged; + final Function(bool value)? onResizeToDSChanged; const SettingsDialog({ super.key, @@ -31,6 +32,7 @@ class SettingsDialog extends StatefulWidget { this.onGridToggle, this.onGridSizeChanged, this.onCornerRadiusChanged, + this.onResizeToDSChanged, }); @override @@ -131,7 +133,8 @@ class _SettingsDialogState extends State { Flexible( child: DialogToggleSwitch( initialValue: - widget.preferences.getBool(PrefKeys.showGrid) ?? false, + widget.preferences.getBool(PrefKeys.showGrid) ?? + Globals.showGrid, label: 'Show Grid', onToggle: (value) { setState(() { @@ -159,18 +162,41 @@ class _SettingsDialogState extends State { ], ), const Divider(), - DialogTextInput( - initialText: widget.preferences - .getDouble(PrefKeys.cornerRadius) - ?.toString() ?? - Globals.cornerRadius.toString(), - label: 'Corner Radius', - onSubmit: (value) { - setState(() { - widget.onCornerRadiusChanged?.call(value); - }); - }, - formatter: FilteringTextInputFormatter.allow(RegExp(r"[0-9.]")), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Flexible( + flex: 2, + child: DialogTextInput( + initialText: widget.preferences + .getDouble(PrefKeys.cornerRadius) + ?.toString() ?? + Globals.cornerRadius.toString(), + label: 'Corner Radius', + onSubmit: (value) { + setState(() { + widget.onCornerRadiusChanged?.call(value); + }); + }, + formatter: + FilteringTextInputFormatter.allow(RegExp(r"[0-9.]")), + ), + ), + Flexible( + flex: 3, + child: DialogToggleSwitch( + initialValue: + widget.preferences.getBool(PrefKeys.autoResizeToDS) ?? + Globals.autoResizeToDS, + label: 'Resize to Driver Station Height', + onToggle: (value) { + setState(() { + widget.onResizeToDSChanged?.call(value); + }); + }, + ), + ), + ], ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 9a663d10..f1d085c9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -809,7 +809,7 @@ packages: source: hosted version: "1.2.3" screen_retriever: - dependency: transitive + dependency: "direct main" description: name: screen_retriever sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" diff --git a/pubspec.yaml b/pubspec.yaml index d0b58332..ef11ad2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: path_provider: ^2.0.15 patterns_canvas: ^0.4.0 provider: ^6.0.5 + screen_retriever: ^0.1.9 searchable_listview: ^2.7.0 shared_preferences: ^2.1.2 syncfusion_flutter_charts: ^23.1.44 @@ -42,7 +43,7 @@ dependencies: version: ^3.0.2 visibility_detector: ^0.4.0+2 web_socket_channel: ^2.4.0 - window_manager: ^0.3.5 + window_manager: ^0.3.7 dev_dependencies: flutter_test: diff --git a/test/widgets/settings_dialog_test.dart b/test/widgets/settings_dialog_test.dart index 5c7361c4..ec10315f 100644 --- a/test/widgets/settings_dialog_test.dart +++ b/test/widgets/settings_dialog_test.dart @@ -30,6 +30,8 @@ class FakeSettingsMethods { void changeGridSize() {} void changeCornerRadius() {} + + void changeDSAutoResize() {} } void main() { @@ -47,6 +49,7 @@ void main() { PrefKeys.showGrid: false, PrefKeys.gridSize: 128, PrefKeys.cornerRadius: 15.0, + PrefKeys.autoResizeToDS: false, }); preferences = await SharedPreferences.getInstance(); @@ -80,6 +83,7 @@ void main() { expect(find.text('Show Grid'), findsWidgets); expect(find.text('Grid Size'), findsWidgets); expect(find.text('Corner Radius'), findsOneWidget); + expect(find.text('Resize to Driver Station Height'), findsOneWidget); final closeButton = find.widgetWithText(TextButton, 'Close'); @@ -360,4 +364,47 @@ void main() { expect(preferences.getDouble(PrefKeys.cornerRadius), 25.0); verify(fakeSettings.changeCornerRadius()).called(greaterThanOrEqualTo(1)); }); + + testWidgets('Toggle driver station auto resize', (widgetTester) async { + FlutterError.onError = ignoreOverflowErrors; + setupMockOfflineNT4(); + + await widgetTester.pumpWidget(MaterialApp( + home: Scaffold( + body: SettingsDialog( + onResizeToDSChanged: (value) async { + fakeSettings.changeDSAutoResize(); + + await preferences.setBool(PrefKeys.autoResizeToDS, value); + }, + preferences: preferences, + ), + ), + )); + + await widgetTester.pumpAndSettle(); + + final autoResizeSwitch = find.widgetWithText( + DialogToggleSwitch, 'Resize to Driver Station Height'); + + expect(autoResizeSwitch, findsOneWidget); + + // Widget tester.tap will not work for some reason + final switchWidget = find + .descendant(of: autoResizeSwitch, matching: find.byType(Switch)) + .evaluate() + .first + .widget as Switch; + + switchWidget.onChanged?.call(true); + await widgetTester.pumpAndSettle(); + + expect(preferences.getBool(PrefKeys.autoResizeToDS), true); + + switchWidget.onChanged?.call(false); + await widgetTester.pumpAndSettle(); + + expect(preferences.getBool(PrefKeys.autoResizeToDS), false); + verify(fakeSettings.changeDSAutoResize()).called(2); + }); }