From ec2ed4e6c58740f4868b003ac4f7326da918e4ca Mon Sep 17 00:00:00 2001 From: Jonas Bark Date: Sun, 26 Oct 2025 19:27:35 +0100 Subject: [PATCH] refactoring #5 --- lib/pages/device.dart | 12 ++-- lib/pages/touch_area.dart | 8 ++- lib/utils/keymap/keymap.dart | 10 +++- lib/utils/keymap/manager.dart | 10 ++-- lib/widgets/keymap_explanation.dart | 86 ++++++++++++++++++----------- lib/widgets/testbed.dart | 59 ++++++++++++++------ 6 files changed, 120 insertions(+), 65 deletions(-) diff --git a/lib/pages/device.dart b/lib/pages/device.dart index 99efc8f..a7361cc 100644 --- a/lib/pages/device.dart +++ b/lib/pages/device.dart @@ -285,7 +285,7 @@ class _DevicePageState extends State with WidgetsBindingObserver { if (app == null) { return; } else if (app.name == 'New') { - final profileName = await KeypadManager().showNewProfileDialog(context); + final profileName = await KeymapManager().showNewProfileDialog(context); if (profileName != null && profileName.isNotEmpty) { final customApp = CustomApp(profileName: profileName); actionHandler.supportedApp = customApp; @@ -321,13 +321,13 @@ class _DevicePageState extends State with WidgetsBindingObserver { IconButton( onPressed: () async { final currentProfile = actionHandler.supportedApp?.name; - final action = await KeypadManager().showManageProfileDialog( + final action = await KeymapManager().showManageProfileDialog( context, currentProfile, ); if (action != null) { if (action == 'rename') { - final newName = await KeypadManager().showRenameProfileDialog( + final newName = await KeymapManager().showRenameProfileDialog( context, currentProfile!, ); @@ -347,7 +347,7 @@ class _DevicePageState extends State with WidgetsBindingObserver { setState(() {}); } } else if (action == 'duplicate') { - final newName = await KeypadManager().duplicate( + final newName = await KeymapManager().duplicate( context, currentProfile!, ); @@ -357,7 +357,7 @@ class _DevicePageState extends State with WidgetsBindingObserver { setState(() {}); } } else if (action == 'delete') { - final confirmed = await KeypadManager().showDeleteConfirmDialog( + final confirmed = await KeymapManager().showDeleteConfirmDialog( context, currentProfile!, ); @@ -367,7 +367,7 @@ class _DevicePageState extends State with WidgetsBindingObserver { setState(() {}); } } else if (action == 'import') { - final jsonData = await KeypadManager().showImportDialog(context); + final jsonData = await KeymapManager().showImportDialog(context); if (jsonData != null && jsonData.isNotEmpty) { final success = await settings.importCustomAppProfile(jsonData); if (mounted) { diff --git a/lib/pages/touch_area.dart b/lib/pages/touch_area.dart index 6d59505..3c2ffdf 100644 --- a/lib/pages/touch_area.dart +++ b/lib/pages/touch_area.dart @@ -349,7 +349,7 @@ class KeypairExplanation extends StatelessWidget { ) else Icon(keyPair.icon), - if (keyPair.physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.media)) ...[ + if (keyPair.isSpecialKey && actionHandler.supportedModes.contains(SupportedMode.media)) _KeyWidget( label: switch (keyPair.physicalKey) { PhysicalKeyboardKey.mediaPlayPause => 'Play/Pause', @@ -358,8 +358,12 @@ class KeypairExplanation extends StatelessWidget { PhysicalKeyboardKey.mediaTrackNext => 'Next', PhysicalKeyboardKey.audioVolumeUp => 'Volume Up', PhysicalKeyboardKey.audioVolumeDown => 'Volume Down', - _ => keyPair.logicalKey?.keyLabel ?? 'Unknown', + _ => 'Unknown', }, + ) + else if (keyPair.physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard)) ...[ + _KeyWidget( + label: keyPair.logicalKey?.keyLabel ?? 'Unknown', ), if (keyPair.isLongPress) Text('long\npress', style: TextStyle(fontSize: 10)), ] else ...[ diff --git a/lib/utils/keymap/keymap.dart b/lib/utils/keymap/keymap.dart index edf1ac7..1802643 100644 --- a/lib/utils/keymap/keymap.dart +++ b/lib/utils/keymap/keymap.dart @@ -8,6 +8,7 @@ import 'package:swift_control/main.dart'; import 'package:swift_control/utils/keymap/buttons.dart'; import '../actions/base_actions.dart'; +import 'apps/custom_app.dart'; class Keymap { static Keymap custom = Keymap(keyPairs: []); @@ -46,6 +47,10 @@ class Keymap { void addKeyPair(KeyPair keyPair) { keyPairs.add(keyPair); _updateStream.add(null); + + if (actionHandler.supportedApp is CustomApp) { + settings.setApp(actionHandler.supportedApp!); + } } } @@ -126,8 +131,9 @@ class KeyPair { : Offset.zero; final buttons = decoded['actions'] - .map((e) => ControllerButton.values.firstOrNullWhere((element) => element.name == e)) - .where((e) => e != null) + .map( + (e) => ControllerButton.values.firstOrNullWhere((element) => element.name == e) ?? ControllerButton(e), + ) .cast() .toList(); if (buttons.isEmpty) { diff --git a/lib/utils/keymap/manager.dart b/lib/utils/keymap/manager.dart index 2c127fa..146ba64 100644 --- a/lib/utils/keymap/manager.dart +++ b/lib/utils/keymap/manager.dart @@ -5,15 +5,15 @@ import 'package:swift_control/main.dart'; import 'apps/custom_app.dart'; -class KeypadManager { +class KeymapManager { // Singleton instance - static final KeypadManager _instance = KeypadManager._internal(); + static final KeymapManager _instance = KeymapManager._internal(); // Private constructor - KeypadManager._internal(); + KeymapManager._internal(); // Factory constructor to return the singleton instance - factory KeypadManager() { + factory KeymapManager() { return _instance; } @@ -104,7 +104,7 @@ class KeypadManager { return showDialog( context: context, builder: (context) => AlertDialog( - title: Text('Duplicate Profile'), + title: Text('Create new custom profile by duplicating "$currentName"'), content: TextField( controller: controller, decoration: InputDecoration(labelText: 'New Profile Name'), diff --git a/lib/widgets/keymap_explanation.dart b/lib/widgets/keymap_explanation.dart index 1336c4e..683c957 100644 --- a/lib/widgets/keymap_explanation.dart +++ b/lib/widgets/keymap_explanation.dart @@ -8,6 +8,7 @@ import 'package:swift_control/pages/device.dart'; import 'package:swift_control/utils/actions/base_actions.dart'; import 'package:swift_control/utils/keymap/apps/custom_app.dart'; import 'package:swift_control/utils/keymap/keymap.dart'; +import 'package:swift_control/utils/keymap/manager.dart'; import 'package:swift_control/widgets/button_widget.dart'; import 'package:swift_control/widgets/custom_keymap_selector.dart'; @@ -33,6 +34,17 @@ class _KeymapExplanationState extends State { }); } + @override + void didUpdateWidget(KeymapExplanation oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.keymap != widget.keymap) { + _updateStreamListener.cancel(); + _updateStreamListener = widget.keymap.updateStream.listen((_) { + setState(() {}); + }); + } + } + @override void dispose() { super.dispose(); @@ -41,11 +53,8 @@ class _KeymapExplanationState extends State { @override Widget build(BuildContext context) { - final allAvailableButtons = connection.devices.flatMap((e) => e.availableButtons).distinct(); - - final availableKeypairs = widget.keymap.keyPairs.filter( - (e) => allAvailableButtons.containsAny(e.buttons), - ); + final availableKeypairs = widget.keymap.keyPairs; + final allAvailableButtons = connection.devices.flatMap((d) => d.availableButtons); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -91,7 +100,8 @@ class _KeymapExplanationState extends State { runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ - for (final button in keyPair.buttons) IntrinsicWidth(child: ButtonWidget(button: button)), + for (final button in keyPair.buttons.filter((b) => allAvailableButtons.contains(b))) + IntrinsicWidth(child: ButtonWidget(button: button)), ], ), ), @@ -225,36 +235,46 @@ class _ButtonEditor extends StatelessWidget { else Text('No action assigned'), - PopupMenuButton( - enabled: true, - itemBuilder: (context) => [ - if (actions.length > 1) ...actions, - PopupMenuItem( - value: null, - onTap: () { - keyPair.isLongPress = !keyPair.isLongPress; - onUpdate(); - }, - child: CheckboxListTile( - value: keyPair.isLongPress, - onChanged: (value) { - keyPair.isLongPress = value ?? false; - + if (actionHandler.supportedApp is CustomApp) + PopupMenuButton( + enabled: true, + itemBuilder: (context) => [ + if (actions.length > 1) ...actions, + PopupMenuItem( + value: null, + onTap: () { + keyPair.isLongPress = !keyPair.isLongPress; onUpdate(); - Navigator.of(context).pop(); }, - title: const Text('Long Press Mode (vs. repeating)'), - ), - ), - ], - onSelected: (key) { - keyPair.physicalKey = key; - keyPair.logicalKey = null; + child: CheckboxListTile( + value: keyPair.isLongPress, + onChanged: (value) { + keyPair.isLongPress = value ?? false; - onUpdate(); - }, - icon: Icon(Icons.edit), - ), + onUpdate(); + Navigator.of(context).pop(); + }, + title: const Text('Long Press Mode (vs. repeating)'), + ), + ), + ], + onSelected: (key) { + keyPair.physicalKey = key; + keyPair.logicalKey = null; + + onUpdate(); + }, + icon: Icon(Icons.edit), + ) + else + IconButton( + onPressed: () async { + final currentProfile = actionHandler.supportedApp!.name; + await KeymapManager().duplicate(context, currentProfile); + onUpdate(); + }, + icon: Icon(Icons.edit), + ), ], ); } diff --git a/lib/widgets/testbed.dart b/lib/widgets/testbed.dart index 8002d99..8f85ca0 100644 --- a/lib/widgets/testbed.dart +++ b/lib/widgets/testbed.dart @@ -1,9 +1,15 @@ +import 'dart:async'; import 'dart:math' as math; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/button_widget.dart'; + +import '../bluetooth/messages/notification.dart'; /// A developer overlay that visualizes touches and keyboard events. /// - Touch dots appear where you touch and fade out over [touchRevealDuration]. @@ -40,6 +46,7 @@ class Testbed extends StatefulWidget { class _TestbedState extends State with SingleTickerProviderStateMixin { late final Ticker _ticker; + late StreamSubscription _actionSubscription; // ----- Touch tracking ----- final Map _active = {}; @@ -55,6 +62,25 @@ class _TestbedState extends State with SingleTickerProviderStateMixin { void initState() { super.initState(); _focusNode = FocusNode(debugLabel: 'TestbedFocus', canRequestFocus: true, skipTraversal: true); + _actionSubscription = connection.actionStream.listen((data) async { + if (!mounted) { + return; + } + if (data is ButtonNotification) { + for (final button in data.buttonsClicked) { + final sample = _KeySample( + button: button, + text: '🔘 ${button.name}', + timestamp: DateTime.now(), + ); + _keys.insert(0, sample); + if (_keys.length > widget.maxKeyboardEvents) { + _keys.removeLast(); + } + } + setState(() {}); + } + }); _ticker = createTicker((_) { // Cull expired touch and key samples. @@ -215,10 +241,9 @@ class _TouchesPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final paint = - Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 2; + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2; for (final s in samples) { final age = now.difference(s.timestamp); @@ -242,17 +267,15 @@ class _TouchesPainter extends CustomPainter { canvas.drawCircle(s.position, rOuter, paint); // Inner fill (stronger) - final fill = - Paint() - ..style = PaintingStyle.fill - ..color = color.withOpacity(0.35 + 0.35 * fade); + final fill = Paint() + ..style = PaintingStyle.fill + ..color = color.withOpacity(0.35 + 0.35 * fade); canvas.drawCircle(s.position, rInner, fill); // Tiny center dot for precision - final center = - Paint() - ..style = PaintingStyle.fill - ..color = color.withOpacity(0.9 * fade); + final center = Paint() + ..style = PaintingStyle.fill + ..color = color.withOpacity(0.9 * fade); canvas.drawCircle(s.position, 2.5, center); } } @@ -269,7 +292,8 @@ class _TouchesPainter extends CustomPainter { // ===== Keyboard overlay ===== class _KeySample { - _KeySample({required this.text, required this.timestamp}); + _KeySample({required this.text, required this.timestamp, this.button}); + final ControllerButton? button; final String text; final DateTime timestamp; } @@ -297,7 +321,7 @@ class _KeyboardOverlay extends StatelessWidget { children: [ for (final item in items) _KeyboardToast( - text: item.text, + item: item, age: now.difference(item.timestamp), duration: duration, badgeColor: badgeColor, @@ -310,14 +334,14 @@ class _KeyboardOverlay extends StatelessWidget { class _KeyboardToast extends StatelessWidget { const _KeyboardToast({ - required this.text, + required this.item, required this.age, required this.duration, required this.badgeColor, required this.textStyle, }); - final String text; + final _KeySample item; final Duration age; final Duration duration; final Color badgeColor; @@ -329,13 +353,14 @@ class _KeyboardToast extends StatelessWidget { final fade = 1.0 - t; return Material( + color: Colors.transparent, child: Opacity( opacity: fade, child: Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(12)), - child: Text(text, style: textStyle), + child: item.button != null ? ButtonWidget(button: item.button!) : Text(item.text, style: textStyle), ), ), );