implement MyWhoosh link in the editor

This commit is contained in:
Jonas Bark
2025-10-27 12:14:41 +01:00
parent 4021f3131d
commit 828aa70a56
15 changed files with 240 additions and 360 deletions

View File

@@ -60,7 +60,7 @@ class Connection {
final existingDevice = bluetoothDevices.firstOrNullWhere(
(e) => e.device.deviceId == result.deviceId,
);
if (existingDevice != null) {
if (existingDevice != null && existingDevice.rssi != result.rssi) {
existingDevice.rssi = result.rssi;
_connectionStreams.add(existingDevice); // Notify UI of update
}

View File

@@ -132,7 +132,7 @@ class WhooshLink {
if (jsonObject != null) {
final jsonString = jsonEncode(jsonObject);
_socket?.writeln(jsonString);
return 'Sent action to MyWhoosh: $action';
return 'Sent action to MyWhoosh: $action ${value ?? ''}';
} else {
return 'No action available for button: $action';
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:device_auto_rotate_checker/device_auto_rotate_checker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart';
@@ -13,10 +12,8 @@ import 'package:swift_control/pages/markdown.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:swift_control/utils/keymap/manager.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
import 'package:swift_control/widgets/loading_widget.dart';
import 'package:swift_control/widgets/logviewer.dart';
import 'package:swift_control/widgets/scan.dart';
import 'package:swift_control/widgets/small_progress_indicator.dart';
import 'package:swift_control/widgets/testbed.dart';
import 'package:swift_control/widgets/title.dart';
import 'package:swift_control/widgets/warning.dart';
@@ -249,15 +246,16 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
Text(
'Remote Control Mode: ${(actionHandler as RemoteActions).isConnected ? 'Connected' : 'Not connected'}',
),
LoadingWidget(
futureCallback: () async {
final requirement = RemoteRequirement();
await requirement.reconnect();
},
renderChild: (isLoading, tap) => TextButton(
onPressed: tap,
child: isLoading ? SmallProgressIndicator() : Text('Reconnect'),
),
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
child: Text('Reconnect'),
onTap: () async {
final requirement = RemoteRequirement();
await requirement.reconnect();
},
),
],
),
],
),
@@ -358,7 +356,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
final profileName = await KeymapManager().showNewProfileDialog(context);
if (profileName != null && profileName.isNotEmpty) {
final customApp = CustomApp(profileName: profileName);
actionHandler.supportedApp = customApp;
actionHandler.init(customApp);
await settings.setApp(customApp);
controller.text = profileName;
setState(() {});
@@ -388,99 +386,11 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
Row(
children: [
IconButton(
onPressed: () async {
final currentProfile = actionHandler.supportedApp is CustomApp
? actionHandler.supportedApp?.name
: null;
final action = await KeymapManager().showManageProfileDialog(
context,
currentProfile,
);
if (action != null) {
if (action == 'rename') {
final newName = await KeymapManager().showRenameProfileDialog(
context,
currentProfile!,
);
if (newName != null && newName.isNotEmpty && newName != currentProfile) {
await settings.duplicateCustomAppProfile(currentProfile, newName);
await settings.deleteCustomAppProfile(currentProfile);
final customApp = CustomApp(profileName: newName);
final savedKeymap = settings.getCustomAppKeymap(newName);
if (savedKeymap != null) {
customApp.decodeKeymap(savedKeymap);
}
actionHandler.supportedApp = customApp;
await settings.setApp(customApp);
controller.text = newName;
setState(() {});
}
} else if (action == 'duplicate') {
final newName = await KeymapManager().duplicate(
context,
currentProfile!,
);
if (newName != null) {
controller.text = newName;
setState(() {});
}
} else if (action == 'delete') {
final confirmed = await KeymapManager().showDeleteConfirmDialog(
context,
currentProfile!,
);
if (confirmed == true) {
await settings.deleteCustomAppProfile(currentProfile);
controller.text = '';
setState(() {});
}
} else if (action == 'import') {
final jsonData = await KeymapManager().showImportDialog(context);
if (jsonData != null && jsonData.isNotEmpty) {
final success = await settings.importCustomAppProfile(jsonData);
if (mounted) {
if (success) {
_snackBarMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text('Profile imported successfully'),
duration: Duration(seconds: 5),
),
);
setState(() {});
} else {
_snackBarMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text('Failed to import profile. Invalid format.'),
duration: Duration(seconds: 5),
backgroundColor: Colors.red,
),
);
}
}
}
} else if (action == 'export') {
final currentProfile =
(actionHandler.supportedApp as CustomApp).profileName;
final jsonData = settings.exportCustomAppProfile(currentProfile);
if (jsonData != null) {
Clipboard.setData(ClipboardData(text: jsonData));
if (mounted) {
_snackBarMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text(
'Profile "$currentProfile" exported to clipboard',
),
duration: Duration(seconds: 5),
),
);
}
}
}
}
},
icon: Icon(Icons.more_vert),
KeymapManager().getManageProfileDialog(
context,
actionHandler.supportedApp is CustomApp
? actionHandler.supportedApp?.name
: null,
),
],
),

View File

@@ -146,7 +146,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
currentPath = route.settings.name;
return true;
});
if (currentPath == null || currentPath != '/device') {
if (currentPath == '/') {
Navigator.push(
context,
MaterialPageRoute(

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -372,7 +373,14 @@ class KeypairExplanation extends StatelessWidget {
)
else
Icon(keyPair.icon),
if (keyPair.isSpecialKey && actionHandler.supportedModes.contains(SupportedMode.media))
if (keyPair.inGameAction != null && whooshLink.isConnected.value)
_KeyWidget(
label: [
keyPair.inGameAction.toString().split('.').last,
if (keyPair.inGameActionValue != null) ': ${keyPair.inGameActionValue}',
].joinToString(separator: ''),
)
else if (keyPair.isSpecialKey && actionHandler.supportedModes.contains(SupportedMode.media))
_KeyWidget(
label: switch (keyPair.physicalKey) {
PhysicalKeyboardKey.mediaPlayPause => 'Play/Pause',

View File

@@ -2,7 +2,6 @@ import 'package:accessibility/accessibility.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/main.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/buttons.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
@@ -30,20 +29,26 @@ class AndroidActions extends BaseActions {
return ("Could not perform ${button.name.splitByUpperCase()}: No keymap set");
}
if (supportedApp is CustomApp) {
final keyPair = supportedApp!.keymap.getKeyPair(button);
if (keyPair != null && keyPair.isSpecialKey) {
await accessibilityHandler.controlMedia(switch (keyPair.physicalKey) {
PhysicalKeyboardKey.mediaTrackNext => MediaAction.next,
PhysicalKeyboardKey.mediaPlayPause => MediaAction.playPause,
PhysicalKeyboardKey.audioVolumeUp => MediaAction.volumeUp,
PhysicalKeyboardKey.audioVolumeDown => MediaAction.volumeDown,
_ => throw SingleLineException("No action for key: ${keyPair.physicalKey}"),
});
return "Key pressed: ${keyPair.toString()}";
}
final keyPair = supportedApp!.keymap.getKeyPair(button);
if (keyPair == null) {
return ("Could not perform ${button.name.splitByUpperCase()}: No action assigned");
}
final point = await resolveTouchPosition(action: button, windowInfo: windowInfo);
if (keyPair.inGameAction != null && whooshLink.isConnected.value) {
return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
} else if (keyPair.isSpecialKey) {
await accessibilityHandler.controlMedia(switch (keyPair.physicalKey) {
PhysicalKeyboardKey.mediaTrackNext => MediaAction.next,
PhysicalKeyboardKey.mediaPlayPause => MediaAction.playPause,
PhysicalKeyboardKey.audioVolumeUp => MediaAction.volumeUp,
PhysicalKeyboardKey.audioVolumeDown => MediaAction.volumeDown,
_ => throw SingleLineException("No action for key: ${keyPair.physicalKey}"),
});
return "Key pressed: ${keyPair.toString()}";
}
final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: windowInfo);
if (point != Offset.zero) {
try {
await accessibilityHandler.performTouch(point.dx, point.dy, isKeyDown: isKeyDown, isKeyUp: isKeyUp);

View File

@@ -46,9 +46,8 @@ abstract class BaseActions {
}
}
Future<Offset> resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) async {
final keyPair = supportedApp!.keymap.getKeyPair(action);
if (keyPair != null && keyPair.touchPosition != Offset.zero) {
Future<Offset> resolveTouchPosition({required KeyPair keyPair, required WindowEvent? windowInfo}) async {
if (keyPair.touchPosition != Offset.zero) {
// convert relative position to absolute position based on window info
// TODO support multiple screens

View File

@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:keypress_simulator/keypress_simulator.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/base_actions.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
@@ -22,7 +23,9 @@ class DesktopActions extends BaseActions {
}
// Handle regular key press mode (existing behavior)
if (keyPair.physicalKey != null) {
if (keyPair.inGameAction != null && whooshLink.isConnected.value) {
return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
} else if (keyPair.physicalKey != null) {
if (isKeyDown && isKeyUp) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
@@ -35,7 +38,7 @@ class DesktopActions extends BaseActions {
return 'Key released: $keyPair';
}
} else {
final point = await resolveTouchPosition(action: action, windowInfo: null);
final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: null);
if (point != Offset.zero) {
if (isKeyDown && isKeyUp) {
await keyPressSimulator.simulateMouseClickDown(point);

View File

@@ -7,6 +7,7 @@ import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/base_actions.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
import 'package:universal_ble/universal_ble.dart';
@@ -26,14 +27,16 @@ class RemoteActions extends BaseActions {
return 'Keymap entry not found for action: ${action.toString().splitByUpperCase()}';
}
if (!(actionHandler as RemoteActions).isConnected) {
if (keyPair.inGameAction != null && whooshLink.isConnected.value) {
return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
} else if (!(actionHandler as RemoteActions).isConnected) {
return 'Not connected to a device';
}
if (keyPair.physicalKey != null && keyPair.touchPosition == Offset.zero) {
return ('Physical key actions are not supported, yet');
} else {
final point = await resolveTouchPosition(action: action, windowInfo: null);
final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: null);
final point2 = point; //Offset(100, 99.0);
await sendAbsMouseReport(0, point2.dx.toInt(), point2.dy.toInt());
await sendAbsMouseReport(1, point2.dx.toInt(), point2.dy.toInt());
@@ -44,13 +47,9 @@ class RemoteActions extends BaseActions {
}
@override
Future<Offset> resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) async {
Future<Offset> resolveTouchPosition({required KeyPair keyPair, required WindowEvent? windowInfo}) async {
// for remote actions we use the relative position only
final keyPair = supportedApp!.keymap.getKeyPair(action);
if (keyPair != null && keyPair.touchPosition != Offset.zero) {
return keyPair.touchPosition;
}
return Offset.zero;
return keyPair.touchPosition;
}
Uint8List absMouseReport(int buttons3bit, int x, int y) {

View File

@@ -60,6 +60,8 @@ class KeyPair {
LogicalKeyboardKey? logicalKey;
Offset touchPosition;
bool isLongPress;
InGameAction? inGameAction;
int? inGameActionValue;
KeyPair({
required this.buttons,
@@ -67,6 +69,8 @@ class KeyPair {
required this.logicalKey,
this.touchPosition = Offset.zero,
this.isLongPress = false,
this.inGameAction,
this.inGameActionValue,
});
bool get isSpecialKey =>
@@ -85,10 +89,9 @@ class KeyPair {
PhysicalKeyboardKey.mediaTrackNext ||
PhysicalKeyboardKey.audioVolumeUp ||
PhysicalKeyboardKey.audioVolumeDown => Icons.music_note_outlined,
_ =>
physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard)
? Icons.keyboard
: Icons.touch_app,
_ when physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard) => Icons.keyboard,
_ when inGameAction != null && whooshLink.isConnected.value => Icons.link,
_ => Icons.touch_app,
};
}
@@ -115,6 +118,8 @@ class KeyPair {
if (physicalKey != null) 'physicalKey': physicalKey?.usbHidUsage.toString() ?? '0',
if (touchPosition != Offset.zero) 'touchPosition': {'x': touchPosition.dx, 'y': touchPosition.dy},
'isLongPress': isLongPress,
'inGameAction': inGameAction?.name,
'inGameActionValue': inGameActionValue,
});
}
@@ -149,6 +154,10 @@ class KeyPair {
: null,
touchPosition: touchPosition,
isLongPress: decoded['isLongPress'] ?? false,
inGameAction: decoded.containsKey('inGameAction')
? InGameAction.values.firstOrNullWhere((element) => element.name == decoded['inGameAction'])
: null,
inGameActionValue: decoded['inGameActionValue'],
);
}
}

View File

@@ -36,51 +36,102 @@ class KeymapManager {
);
}
Future<String?> showManageProfileDialog(BuildContext context, String? currentProfile) async {
return showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: Text('Manage Profile: ${currentProfile ?? ''}'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (currentProfile != null && actionHandler.supportedApp is CustomApp)
ListTile(
leading: Icon(Icons.edit),
title: Text('Rename'),
onTap: () => Navigator.pop(context, 'rename'),
),
if (currentProfile != null)
ListTile(
leading: Icon(Icons.copy),
title: Text('Duplicate'),
onTap: () => Navigator.pop(context, 'duplicate'),
),
ListTile(
leading: Icon(Icons.file_upload),
title: Text('Import'),
onTap: () => Navigator.pop(context, 'import'),
),
if (currentProfile != null)
ListTile(
leading: Icon(Icons.share),
title: Text('Export'),
onTap: () => Navigator.pop(context, 'export'),
),
if (currentProfile != null)
ListTile(
leading: Icon(Icons.delete, color: Theme.of(context).colorScheme.error),
title: Text('Delete', style: TextStyle(color: Theme.of(context).colorScheme.error)),
onTap: () => Navigator.pop(context, 'delete'),
),
],
PopupMenuButton<String> getManageProfileDialog(BuildContext context, String? currentProfile) {
return PopupMenuButton(
itemBuilder: (context) => [
if (currentProfile != null && actionHandler.supportedApp is CustomApp)
PopupMenuItem(
child: Text('Rename'),
onTap: () async {
final newName = await _showRenameProfileDialog(
context,
currentProfile,
);
if (newName != null && newName.isNotEmpty && newName != currentProfile) {
await settings.duplicateCustomAppProfile(currentProfile, newName);
await settings.deleteCustomAppProfile(currentProfile);
final customApp = CustomApp(profileName: newName);
final savedKeymap = settings.getCustomAppKeymap(newName);
if (savedKeymap != null) {
customApp.decodeKeymap(savedKeymap);
}
actionHandler.supportedApp = customApp;
await settings.setApp(customApp);
}
},
),
if (currentProfile != null)
PopupMenuItem(
child: Text('Duplicate'),
onTap: () async {
final newName = await duplicate(
context,
currentProfile,
);
},
),
PopupMenuItem(
child: Text('Import'),
onTap: () async {
final jsonData = await _showImportDialog(context);
if (jsonData != null && jsonData.isNotEmpty) {
final success = await settings.importCustomAppProfile(jsonData);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Profile imported successfully'),
duration: Duration(seconds: 5),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to import profile. Invalid format.'),
duration: Duration(seconds: 5),
backgroundColor: Colors.red,
),
);
}
}
},
),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel'))],
),
if (currentProfile != null)
PopupMenuItem(
child: Text('Export'),
onTap: () {
final currentProfile = (actionHandler.supportedApp as CustomApp).profileName;
final jsonData = settings.exportCustomAppProfile(currentProfile);
if (jsonData != null) {
Clipboard.setData(ClipboardData(text: jsonData));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Profile "$currentProfile" exported to clipboard',
),
duration: Duration(seconds: 5),
),
);
}
},
),
if (currentProfile != null)
PopupMenuItem(
child: Text('Delete', style: TextStyle(color: Theme.of(context).colorScheme.error)),
onTap: () async {
final confirmed = await _showDeleteConfirmDialog(
context,
currentProfile,
);
if (confirmed == true) {
await settings.deleteCustomAppProfile(currentProfile);
}
},
),
],
);
}
Future<String?> showRenameProfileDialog(BuildContext context, String currentName) async {
Future<String?> _showRenameProfileDialog(BuildContext context, String currentName) async {
final controller = TextEditingController(text: currentName);
return showDialog<String>(
context: context,
@@ -99,7 +150,7 @@ class KeymapManager {
);
}
Future<String?> showDuplicateProfileDialog(BuildContext context, String currentName) async {
Future<String?> _showDuplicateProfileDialog(BuildContext context, String currentName) async {
final controller = TextEditingController(text: '$currentName (Copy)');
return showDialog<String>(
context: context,
@@ -118,7 +169,7 @@ class KeymapManager {
);
}
Future<bool?> showDeleteConfirmDialog(BuildContext context, String profileName) async {
Future<bool?> _showDeleteConfirmDialog(BuildContext context, String profileName) async {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
@@ -136,7 +187,7 @@ class KeymapManager {
);
}
Future<String?> showImportDialog(BuildContext context) async {
Future<String?> _showImportDialog(BuildContext context) async {
final controller = TextEditingController();
// Try to get data from clipboard
@@ -175,7 +226,7 @@ class KeymapManager {
}
Future<String?> duplicate(BuildContext context, String currentProfile) async {
final newName = await showDuplicateProfileDialog(context, currentProfile);
final newName = await _showDuplicateProfileDialog(context, currentProfile);
if (newName != null && newName.isNotEmpty) {
if (actionHandler.supportedApp is CustomApp) {
await settings.duplicateCustomAppProfile(currentProfile, newName);

View File

@@ -4,7 +4,6 @@ import 'package:dartx/dartx.dart';
import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/utils/requirements/multi.dart';
import 'package:window_manager/window_manager.dart';
@@ -203,26 +202,4 @@ class Settings {
return migratedData;
}
void setInGameActionForButton(ControllerButton button, InGameAction inGameAction) {
final key = 'ingameaction_${button.name}';
prefs.setString(key, inGameAction.name);
}
InGameAction? getInGameActionForButton(ControllerButton button) {
final key = 'ingameaction_${button.name}';
final actionName = prefs.getString(key);
if (actionName == null) return button.action;
return InGameAction.values.firstOrNullWhere((e) => e.name == actionName) ?? button.action;
}
void setInGameActionForButtonValue(ControllerButton button, InGameAction inGameAction, int value) {
final key = 'ingameaction_${button.name}_value';
prefs.setInt(key, value);
}
int? getInGameActionForButtonValue(ControllerButton button) {
final key = 'ingameaction_${button.name}_value';
return prefs.getInt(key);
}
}

View File

@@ -44,6 +44,7 @@ class ChangelogDialog extends StatelessWidget {
showDialog(
context: context,
useRootNavigator: true,
routeSettings: RouteSettings(name: '/changelog'),
builder: (context) => ChangelogDialog(entry: markdown),
);
}

View File

@@ -1,133 +0,0 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/bluetooth/devices/link/link.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/device.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/widgets/button_widget.dart';
class InGameActionsCustomizer extends StatefulWidget {
const InGameActionsCustomizer({super.key});
@override
State<InGameActionsCustomizer> createState() => _InGameActionsCustomizerState();
}
class _InGameActionsCustomizerState extends State<InGameActionsCustomizer> {
@override
Widget build(BuildContext context) {
final connectedDevice = connection.devices.firstOrNull;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Table(
border: TableBorder.symmetric(
borderRadius: BorderRadius.circular(9),
inside: BorderSide(
color: Theme.of(context).colorScheme.primaryContainer,
),
outside: BorderSide(
color: Theme.of(context).colorScheme.primaryContainer,
),
),
children: [
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Button on your ${connectedDevice?.name.screenshot ?? connectedDevice?.runtimeType ?? 'device'}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Action on MyWhoosh',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
for (final button in connectedDevice?.availableButtons ?? <ControllerButton>[]) ...[
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Row(
children: [
ButtonWidget(button: button),
],
),
),
Padding(
padding: const EdgeInsets.all(6),
child: Row(
children: [
if (MediaQuery.sizeOf(context).width < 1800)
Expanded(child: _buildDropdownButton(button, true))
else
_buildDropdownButton(button, false),
],
),
),
],
),
],
],
),
],
);
}
Widget _buildDropdownButton(ControllerButton button, bool expand) {
final value = WhooshLink.supportedActions.contains(settings.getInGameActionForButton(button))
? settings.getInGameActionForButton(button)
: null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButton<InGameAction>(
isExpanded: expand,
items: WhooshLink.supportedActions
.map(
(ingame) => DropdownMenuItem(
value: ingame,
child: Text(ingame.toString()),
),
)
.toList(),
padding: EdgeInsets.zero,
menuWidth: 250,
value: value,
onChanged: (action) {
settings.setInGameActionForButton(
button,
action!,
);
setState(() {});
},
),
if (value?.possibleValues != null)
DropdownButton<int>(
items: value!.possibleValues!
.map((val) => DropdownMenuItem<int>(value: val, child: Text(val.toString())))
.toList(),
value: settings.getInGameActionForButtonValue(button),
onChanged: (val) {
settings.setInGameActionForButtonValue(
button,
value,
val!,
);
setState(() {});
},
hint: Text('Value'),
),
],
);
}
}

View File

@@ -12,6 +12,7 @@ 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';
import '../bluetooth/devices/link/link.dart';
import '../pages/touch_area.dart';
class KeymapExplanation extends StatefulWidget {
@@ -133,6 +134,52 @@ class _ButtonEditor extends StatelessWidget {
@override
Widget build(BuildContext context) {
final actions = [
if (whooshLink.isConnected.value)
PopupMenuItem<PhysicalKeyboardKey>(
child: PopupMenuButton(
itemBuilder: (_) => WhooshLink.supportedActions.map(
(ingame) {
return PopupMenuItem(
value: ingame,
child: ingame.possibleValues != null
? PopupMenuButton(
itemBuilder: (c) => ingame.possibleValues!
.map(
(value) => PopupMenuItem(
value: value,
child: Text(value.toString()),
onTap: () {
keyPair.inGameAction = ingame;
keyPair.inGameActionValue = value;
onUpdate();
},
),
)
.toList(),
child: Row(
children: [
Expanded(child: Text(ingame.toString())),
Icon(Icons.arrow_right),
],
),
)
: Text(ingame.toString()),
onTap: () {
keyPair.inGameAction = ingame;
keyPair.inGameActionValue = null;
onUpdate();
},
);
},
).toList(),
child: Row(
children: [
Expanded(child: Text('MyWhoosh Link Action')),
Icon(Icons.arrow_right),
],
),
),
),
if (actionHandler.supportedModes.contains(SupportedMode.keyboard))
PopupMenuItem<PhysicalKeyboardKey>(
value: null,
@@ -227,12 +274,33 @@ class _ButtonEditor extends StatelessWidget {
),
),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: null,
onTap: () {
keyPair.isLongPress = !keyPair.isLongPress;
onUpdate();
},
child: ListTile(
title: const Text('Long Press Mode (vs. repeating)'),
trailing: Checkbox(
value: keyPair.isLongPress,
onChanged: (value) {
keyPair.isLongPress = value ?? false;
onUpdate();
Navigator.of(context).pop();
},
),
),
),
];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (keyPair.buttons.isNotEmpty && (keyPair.physicalKey != null || keyPair.touchPosition != Offset.zero))
if (keyPair.buttons.isNotEmpty &&
(keyPair.physicalKey != null || keyPair.touchPosition != Offset.zero || keyPair.inGameAction != null))
Expanded(
child: KeypairExplanation(
keyPair: keyPair,
@@ -246,23 +314,6 @@ class _ButtonEditor extends StatelessWidget {
enabled: true,
itemBuilder: (context) => [
if (actions.length > 1) ...actions,
PopupMenuItem<PhysicalKeyboardKey>(
value: null,
onTap: () {
keyPair.isLongPress = !keyPair.isLongPress;
onUpdate();
},
child: CheckboxListTile(
value: keyPair.isLongPress,
onChanged: (value) {
keyPair.isLongPress = value ?? false;
onUpdate();
Navigator.of(context).pop();
},
title: const Text('Long Press Mode (vs. repeating)'),
),
),
],
onSelected: (key) {
keyPair.physicalKey = key;