diff --git a/lib/core/dialog_utils.dart b/lib/core/dialog_utils.dart new file mode 100644 index 0000000..856d563 --- /dev/null +++ b/lib/core/dialog_utils.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class DialogUtils { + static DialogButtonSet saveButtons( + BuildContext context, + Function() onSave, { + bool dismissOnSave = true, + }) { + return DialogButtonSet( + confirm: ElevatedButton( + child: const Text('Save'), + onPressed: () { + onSave(); + if (dismissOnSave) { + Navigator.of(context).pop(); + } + }, + ), + dismiss: TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } +} + +class DialogButtonSet { + final Widget confirm; + final Widget dismiss; + + DialogButtonSet({required this.confirm, required this.dismiss}); + + Widget row() { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + dismiss, + const SizedBox(width: 8), + confirm, + ], + ); + } +} + diff --git a/lib/core/features/action.dart b/lib/core/features/action.dart index cf61792..7c8b809 100644 --- a/lib/core/features/action.dart +++ b/lib/core/features/action.dart @@ -15,7 +15,7 @@ enum MUDActionTarget { class MUDAction { String content; MUDActionTarget target; - MUDAction(this.content, {this.target = MUDActionTarget.world}); + MUDAction(this.content, {this.target = MUDActionTarget.execute}); void invoke(GameStore store, Automation parent, List matches) { debugPrint('MUDAction.invoke: ${this.content}, $matches'); diff --git a/lib/core/features/game_button.dart b/lib/core/features/game_button.dart index 0a6638a..63f45c6 100644 --- a/lib/core/features/game_button.dart +++ b/lib/core/features/game_button.dart @@ -7,7 +7,7 @@ import 'action.dart'; import 'automation.dart'; class GameButtonData { - const GameButtonData({ + GameButtonData({ required this.id, required this.label, required this.pressAction, @@ -18,28 +18,28 @@ class GameButtonData { this.color, this.size = defaultSize, this.longPressAction, - this.swipeUpAction, - this.swipeDownAction, - this.swipeLeftAction, - this.swipeRightAction, + this.dragUpAction, + this.dragDownAction, + this.dragLeftAction, + this.dragRightAction, }); static const defaultSize = 60.0; final String id; - final GameButtonLabelData label; - final GameButtonLabelData? labelUp; - final GameButtonLabelData? labelDown; - final GameButtonLabelData? labelLeft; - final GameButtonLabelData? labelRight; - final Color? color; - final double? size; - final MUDAction pressAction; - final MUDAction? longPressAction; - final MUDAction? swipeUpAction; - final MUDAction? swipeDownAction; - final MUDAction? swipeLeftAction; - final MUDAction? swipeRightAction; + GameButtonLabelData label; + GameButtonLabelData? labelUp; + GameButtonLabelData? labelDown; + GameButtonLabelData? labelLeft; + GameButtonLabelData? labelRight; + Color? color; + double? size; + MUDAction pressAction; + MUDAction? longPressAction; + MUDAction? dragUpAction; + MUDAction? dragDownAction; + MUDAction? dragLeftAction; + MUDAction? dragRightAction; factory GameButtonData.empty() => GameButtonData( id: uuid(), @@ -69,18 +69,18 @@ class GameButtonData { longPressAction: json['longPressAction'] == null ? null : MUDAction.fromJson(json['longPressAction']), - swipeUpAction: json['swipeUpAction'] == null + dragUpAction: json['dragUpAction'] == null ? null - : MUDAction.fromJson(json['swipeUpAction']), - swipeDownAction: json['swipeDownAction'] == null + : MUDAction.fromJson(json['dragUpAction']), + dragDownAction: json['dragDownAction'] == null ? null - : MUDAction.fromJson(json['swipeDownAction']), - swipeLeftAction: json['swipeLeftAction'] == null + : MUDAction.fromJson(json['dragDownAction']), + dragLeftAction: json['dragLeftAction'] == null ? null - : MUDAction.fromJson(json['swipeLeftAction']), - swipeRightAction: json['swipeRightAction'] == null + : MUDAction.fromJson(json['dragLeftAction']), + dragRightAction: json['dragRightAction'] == null ? null - : MUDAction.fromJson(json['swipeRightAction']), + : MUDAction.fromJson(json['dragRightAction']), ); } @@ -95,10 +95,10 @@ class GameButtonData { double? size, MUDAction? pressAction, MUDAction? longPressAction, - MUDAction? swipeUpAction, - MUDAction? swipeDownAction, - MUDAction? swipeLeftAction, - MUDAction? swipeRightAction, + MUDAction? dragUpAction, + MUDAction? dragDownAction, + MUDAction? dragLeftAction, + MUDAction? dragRightAction, }) => GameButtonData( id: id ?? this.id, @@ -111,10 +111,10 @@ class GameButtonData { size: size ?? this.size, pressAction: pressAction ?? this.pressAction, longPressAction: longPressAction ?? this.longPressAction, - swipeUpAction: swipeUpAction ?? this.swipeUpAction, - swipeDownAction: swipeDownAction ?? this.swipeDownAction, - swipeLeftAction: swipeLeftAction ?? this.swipeLeftAction, - swipeRightAction: swipeRightAction ?? this.swipeRightAction, + dragUpAction: dragUpAction ?? this.dragUpAction, + dragDownAction: dragDownAction ?? this.dragDownAction, + dragLeftAction: dragLeftAction ?? this.dragLeftAction, + dragRightAction: dragRightAction ?? this.dragRightAction, ); Map toJson() => { @@ -128,41 +128,55 @@ class GameButtonData { 'color': color?.value, 'size': size, 'longPressAction': longPressAction?.toJson(), - 'swipeUpAction': swipeUpAction?.toJson(), - 'swipeDownAction': swipeDownAction?.toJson(), - 'swipeLeftAction': swipeLeftAction?.toJson(), - 'swipeRightAction': swipeRightAction?.toJson(), + 'dragUpAction': dragUpAction?.toJson(), + 'dragDownAction': dragDownAction?.toJson(), + 'dragLeftAction': dragLeftAction?.toJson(), + 'dragRightAction': dragRightAction?.toJson(), }; - MUDAction directionalAction(GameButtonDirection direction) { - switch (direction) { - case GameButtonDirection.up: - return swipeUpAction ?? pressAction; - case GameButtonDirection.down: - return swipeDownAction ?? pressAction; - case GameButtonDirection.left: - return swipeLeftAction ?? pressAction; - case GameButtonDirection.right: - return swipeRightAction ?? pressAction; - default: + MUDAction getActionOrDefault(GameButtonInteraction interaction) { + return getAction(interaction) ?? pressAction; + } + + MUDAction? getAction(GameButtonInteraction interaction) { + switch (interaction) { + case GameButtonInteraction.press: return pressAction; + case GameButtonInteraction.longPress: + return longPressAction; + case GameButtonInteraction.dragUp: + return dragUpAction; + case GameButtonInteraction.dragDown: + return dragDownAction; + case GameButtonInteraction.dragLeft: + return dragLeftAction; + case GameButtonInteraction.dragRight: + return dragRightAction; + default: + return null; } } - MUDAction? actionForDirection(GameButtonDirection direction) { + void setAction(GameButtonInteraction direction, MUDAction mudAction) { switch (direction) { - case GameButtonDirection.none: - return pressAction; - case GameButtonDirection.up: - return swipeUpAction; - case GameButtonDirection.down: - return swipeDownAction; - case GameButtonDirection.left: - return swipeLeftAction; - case GameButtonDirection.right: - return swipeRightAction; - default: - return null; + case GameButtonInteraction.press: + pressAction = mudAction; + break; + case GameButtonInteraction.longPress: + longPressAction = mudAction; + break; + case GameButtonInteraction.dragUp: + dragUpAction = mudAction; + break; + case GameButtonInteraction.dragDown: + dragDownAction = mudAction; + break; + case GameButtonInteraction.dragLeft: + dragLeftAction = mudAction; + break; + case GameButtonInteraction.dragRight: + dragRightAction = mudAction; + break; } } } @@ -180,7 +194,7 @@ class GameButton extends StatefulWidget { } class _GameButtonState extends State with GameStoreStateMixin { - late GameButtonDirection _direction; + late GameButtonInteraction _direction; final parentAutomation = Automation.empty(); Offset? _dragStart; Offset? _dragEnd; @@ -190,7 +204,7 @@ class _GameButtonState extends State with GameStoreStateMixin { @override void initState() { - _direction = GameButtonDirection.none; + _direction = GameButtonInteraction.press; super.initState(); } @@ -214,7 +228,7 @@ class _GameButtonState extends State with GameStoreStateMixin { ); } - double get _swipeMinDist => + double get _dragMinDist => (data.size ?? IconTheme.of(context).size ?? const IconThemeData.fallback().size!) / @@ -248,13 +262,13 @@ class _GameButtonState extends State with GameStoreStateMixin { GameButtonLabelData _currentDirectionIcon(BuildContext context) { switch (_direction) { - case GameButtonDirection.up: + case GameButtonInteraction.dragUp: return data.labelUp ?? data.label; - case GameButtonDirection.down: + case GameButtonInteraction.dragDown: return data.labelDown ?? data.label; - case GameButtonDirection.left: + case GameButtonInteraction.dragLeft: return data.labelLeft ?? data.label; - case GameButtonDirection.right: + case GameButtonInteraction.dragRight: return data.labelRight ?? data.label; default: return data.label; @@ -280,17 +294,17 @@ class _GameButtonState extends State with GameStoreStateMixin { void _onDragStart(DragStartDetails details) { _dragStart = details.globalPosition; setState(() { - _direction = GameButtonDirection.none; + _direction = GameButtonInteraction.press; }); } void _onDragUpdate(DragUpdateDetails details) { final pos = details.globalPosition; - final direction = _getRelativeDragDirection(_dragStart!, pos); + final interaction = _getRelativeDragDirection(_dragStart!, pos); - if (direction != _direction) { + if (interaction != _direction) { setState(() { - _direction = direction; + _direction = interaction; }); } } @@ -298,70 +312,71 @@ class _GameButtonState extends State with GameStoreStateMixin { void _onDragEnd(DragEndDetails details) { _callCurrentDirection(); setState(() { - _direction = GameButtonDirection.none; + _direction = GameButtonInteraction.press; }); } void _onPointerUp(PointerUpEvent event) { _callCurrentDirection(); setState(() { - _direction = GameButtonDirection.none; + _direction = GameButtonInteraction.press; }); } void _onPointerDown(PointerDownEvent event) { _dragStart = event.position; setState(() { - _direction = GameButtonDirection.none; + _direction = GameButtonInteraction.press; }); } void _onPointerMove(PointerMoveEvent event) { _dragEnd = event.position; - final direction = _getRelativeDragDirection(_dragStart!, _dragEnd!); + final interaction = _getRelativeDragDirection(_dragStart!, _dragEnd!); - if (direction != _direction) { + if (interaction != _direction) { setState(() { - _direction = direction; + _direction = interaction; }); } } - GameButtonDirection _getRelativeDragDirection(Offset start, Offset current) { + GameButtonInteraction _getRelativeDragDirection( + Offset start, Offset current) { final diff = start - current; - GameButtonDirection direction = _direction; + GameButtonInteraction interaction = _direction; - if (diff.distance > _swipeMinDist) { - // detect primary direction + if (diff.distance > _dragMinDist) { + // detect primary interaction // horizontal if (diff.dx.abs() > diff.dy.abs()) { - if (diff.dx > _swipeMinDist) { - direction = GameButtonDirection.left; + if (diff.dx > _dragMinDist) { + interaction = GameButtonInteraction.dragLeft; } else { - direction = GameButtonDirection.right; + interaction = GameButtonInteraction.dragRight; } // vertical } else { - if (diff.dy > _swipeMinDist) { - direction = GameButtonDirection.up; + if (diff.dy > _dragMinDist) { + interaction = GameButtonInteraction.dragUp; } else { - direction = GameButtonDirection.down; + interaction = GameButtonInteraction.dragDown; } } - // pos is within button - no direction + // pos is within button - no interaction } else { - direction = GameButtonDirection.none; + interaction = GameButtonInteraction.press; } - return direction; + return interaction; } void _callAction(MUDAction? action) { - final act = action ?? data.directionalAction(GameButtonDirection.none); + final act = action ?? data.getActionOrDefault(GameButtonInteraction.press); act.invoke(store, parentAutomation, []); } void _callCurrentDirection() { - _callAction(data.directionalAction(_direction)); + _callAction(data.getActionOrDefault(_direction)); } } @@ -416,11 +431,12 @@ class GameButtonLabelData { }; } -enum GameButtonDirection { - none, - up, - down, - left, - right, +enum GameButtonInteraction { + press, + longPress, + dragUp, + dragDown, + dragLeft, + dragRight, } diff --git a/lib/core/features/game_button_set.dart b/lib/core/features/game_button_set.dart index fb38692..bb9ceb4 100644 --- a/lib/core/features/game_button_set.dart +++ b/lib/core/features/game_button_set.dart @@ -274,8 +274,8 @@ final movementPreset = GameButtonSetData( labelUp: GameButtonLabelData(icon: Icons.exit_to_app), labelDown: GameButtonLabelData(icon: Icons.exit_to_app), pressAction: MUDAction('look'), - swipeUpAction: MUDAction('exits'), - swipeDownAction: MUDAction('exits'), + dragUpAction: MUDAction('exits'), + dragDownAction: MUDAction('exits'), ), GameButtonData( id: uuid(), diff --git a/lib/core/store.dart b/lib/core/store.dart index fa88021..2cd618a 100644 --- a/lib/core/store.dart +++ b/lib/core/store.dart @@ -29,6 +29,7 @@ class GameStore extends ChangeNotifier { bool isCompressed = false; final ZLibDecoder decoder = ZLibDecoder(); final msgSplitPattern = RegExp("($cr$lf)|($lf$cr)|$cr|$lf"); + final outgoingMsgSplitPattern = RegExp("($cr$lf)|($lf$cr)|$cr|$lf|$csp"); final ZLibCodec _decoder = ZLibCodec(); final StreamController> _rawStreamController = StreamController(); late Stream> _decodedStream; @@ -37,6 +38,10 @@ class GameStore extends ChangeNotifier { MUDProfile? _currentProfile; bool _clientReady = false; + // TODO move to settings + /// command separator + static const csp = ";"; + // features // TODO - move to MUDProfile and make that reactive final List triggers = []; @@ -280,21 +285,26 @@ class GameStore extends ChangeNotifier { _client.send(line + newline); } - void send(String line) { - if (isCompressed) { - debugPrint('sending bytes${isCompressed ? ' (compressed)' : ''}: $line'); - sendBytes(line.codeUnits + newline.codeUnits); - } else { - debugPrint('sending string: $line'); - sendString(line); + void send(String text) { + for (final line in text.trimRight().split(outgoingMsgSplitPattern)) { + if (isCompressed) { + debugPrint( + 'sending bytes${isCompressed ? ' (compressed)' : ''}: $line'); + sendBytes(line.codeUnits + newline.codeUnits); + } else { + debugPrint('sending string: $line'); + sendString(line); + } } } - void execute(String line) { - debugPrint('processing aliases for: $line'); - var sendLine = processAliases(line); - if (sendLine) { - sendString(line); + void execute(String text) { + debugPrint('processing aliases for: $text'); + for (final line in text.trimRight().split(outgoingMsgSplitPattern)) { + var sendLine = processAliases(line); + if (sendLine) { + sendString(line); + } } } @@ -408,3 +418,4 @@ mixin GameStoreStateMixin on State { } final gameStore = GameStore(); + diff --git a/lib/core/string_utils.dart b/lib/core/string_utils.dart index c8767f4..b3625d8 100644 --- a/lib/core/string_utils.dart +++ b/lib/core/string_utils.dart @@ -5,3 +5,16 @@ const _uuid = Uuid(); String uuid() { return _uuid.v4(); } + +/// split by any non-word character, or by camelCase/PascalCase +List splitIntoWords(String string) { + return string + .split(RegExp(r'[\W_]+|(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])')); +} + +String capitalize(String string) { + return splitIntoWords(string) + .map((word) => word[0].toUpperCase() + word.substring(1)) + .join(' '); +} + diff --git a/lib/pages/button_set_page.dart b/lib/pages/button_set_page.dart index 80bf854..e3d0c73 100644 --- a/lib/pages/button_set_page.dart +++ b/lib/pages/button_set_page.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +import '../core/dialog_utils.dart'; import '../core/features/action.dart'; import '../core/features/game_button.dart'; import '../core/features/game_button_set.dart'; -import '../core/platform_utils.dart'; +import '../core/string_utils.dart'; class GameButtonSetPage extends StatefulWidget { const GameButtonSetPage({super.key, required this.buttonSet}); @@ -133,7 +134,14 @@ class _ButtonSetEditorState extends State { onEdit: () { showDialog( context: context, - builder: (context) => ButtonEditorDialog(data: data), + builder: (context) => ButtonEditorDialog( + data: data, + onSave: (data) { + setState(() { + this.data.buttons[index] = data; + }); + }, + ), ); }, ) @@ -183,6 +191,7 @@ class FakeGameButton extends StatelessWidget { Widget build(BuildContext context) { return PopupMenuButton( offset: Offset(0, size), + tooltip: '', itemBuilder: (context) => const [ PopupMenuItem( value: 'edit', @@ -203,10 +212,10 @@ class FakeGameButton extends StatelessWidget { label: label, pressAction: MUDAction.empty(), longPressAction: MUDAction.empty(), - swipeUpAction: MUDAction.empty(), - swipeDownAction: MUDAction.empty(), - swipeLeftAction: MUDAction.empty(), - swipeRightAction: MUDAction.empty(), + dragUpAction: MUDAction.empty(), + dragDownAction: MUDAction.empty(), + dragLeftAction: MUDAction.empty(), + dragRightAction: MUDAction.empty(), ), ), ); @@ -217,9 +226,11 @@ class ButtonEditorDialog extends StatefulWidget { const ButtonEditorDialog({ super.key, this.data, + required this.onSave, }); final GameButtonData? data; + final void Function(GameButtonData data) onSave; @override State createState() => _ButtonEditorDialogState(); @@ -236,6 +247,11 @@ class _ButtonEditorDialogState extends State { @override Widget build(BuildContext context) { + const interactions = GameButtonInteraction.values; + final actions = DialogUtils.saveButtons(context, () { + widget.onSave(data); + }); + return Dialog( child: SizedBox( width: 600, @@ -244,14 +260,37 @@ class _ButtonEditorDialogState extends State { child: ListView( shrinkWrap: true, children: [ - for (final direction in GameButtonDirection.values) - TextFormField( - initialValue: - data.actionForDirection(direction)?.content ?? '', - decoration: InputDecoration( - label: Text("${direction.name} action"), - ), + Text( + 'Edit Button', + style: Theme.of(context).textTheme.titleLarge, + ), + for (final direction in interactions) + Row( + children: [ + Expanded( + child: TextFormField( + initialValue: data.getAction(direction)?.content ?? '', + decoration: InputDecoration( + label: Text(capitalize(direction.name)), + ), + onChanged: (value) { + data.setAction(direction, MUDAction(value)); + }, + ), + ), + const SizedBox(width: 16), + const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox( + width: 40, + height: 40, + child: Placeholder(), + ), + ), + ], ), + const SizedBox(height: 32), + actions.row(), ], ), ),