Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions lib/board/pedax_shortcuts/paste_position_shortcut.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'dart:io';

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:pedax/l10n/app_localizations.dart'; // Assuming this path is correct
import 'package:pedax/models/board_notifier.dart'; // Assuming this path is correct

import 'pedax_shortcut.dart'; // Assuming this path is correct

// Constants defined in plan step 1
const int EXPECTED_STRING_LENGTH = 65;
const Set<String> VALID_BOARD_CHARS = {'X', 'O', '-'};
const Set<String> VALID_PLAYER_CHARS = {'X', 'O'};

@immutable
class PastePositionShortcut implements PedaxShortcut {
const PastePositionShortcut();

@override
String label(final AppLocalizations localizations) => localizations.shortcutLabelPastePosition; // This localization key will need to be added

@visibleForTesting
static LogicalKeyboardKey get logicalKey => LogicalKeyboardKey.keyV;

String get _keyLabel => logicalKey.keyLabel.toUpperCase();

// Using Alt+V for MacOS, Alt+V for other platforms.
// Flutter's RawKeyboardListener/FocusShortcut might handle Alt detection differently
// across platforms or might need isControlPressed / isMetaPressed for Cmd/Ctrl.
// For now, let's try with isAltPressed.
@override
String get keys {
if (Platform.isMacOS) {
return '⌥$_keyLabel (Alt + $_keyLabel)';
} else {
return 'Alt + $_keyLabel';
}
}

@override
bool fired(final KeyEvent keyEvent) {
// This checks for Alt key.
// Note: On some systems, Alt might be AltGr.
// Consider if isModifierPressed(ModifierKey.altModifier) is more robust or if specific platform checks are needed.
// For simplicity, using isAltPressed.
return HardwareKeyboard.instance.isAltPressed &&
HardwareKeyboard.instance.isLogicalKeyPressed(logicalKey) &&
keyEvent is KeyDownEvent; // Ensure it fires only on key down
}

bool isValidPositionString(String? text) {
if (text == null) return false;
if (text.length != EXPECTED_STRING_LENGTH) return false;

for (int i = 0; i < EXPECTED_STRING_LENGTH - 1; i++) {
if (!VALID_BOARD_CHARS.contains(text[i])) return false;
}

if (!VALID_PLAYER_CHARS.contains(text[EXPECTED_STRING_LENGTH - 1])) return false;

return true;
}

@override
Future<void> runEvent(final PedaxShortcutEventArguments args) async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (clipboardData == null || clipboardData.text == null) {
debugPrint("PastePositionShortcut: Clipboard data is null.");
return;
}

final String textToPaste = clipboardData.text!;
if (isValidPositionString(textToPaste)) {
// Call the new method on BoardNotifier.
// This method will be created in the next plan step.
args.boardNotifier.requestSetBoardFromString(textToPaste);
debugPrint("PastePositionShortcut: Valid position string pasted and request sent.");
} else {
// Optionally, provide feedback to the user about invalid format.
// For now, just a debug print.
debugPrint("PastePositionShortcut: Invalid position string format. Text: '$textToPaste'");
}
}
}
46 changes: 27 additions & 19 deletions lib/engine/api/setboard.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import 'package:flutter/foundation.dart';
import 'package:libedax4dart/libedax4dart.dart';
import 'package:libedax4dart/libedax4dart.dart'; // For TurnColor, ColorChar, LibEdax, Board, Move
import 'package:logger/logger.dart';

import 'request_schema.dart';
import 'response_schema.dart';

@immutable
class SquareReplacement {
const SquareReplacement(this.offset, this.char);

final int offset; // a.k.a move(int)
final String char;
}
// The SquareReplacement class should be removed from this file as it's no longer used by SetboardRequest.
// (Assuming no other code directly imports SquareReplacement from this specific file path).

@immutable
class SetboardRequest implements RequestSchema {
const SetboardRequest({required this.currentColor, required this.replacementTargets, required this.logLevel});
const SetboardRequest({
required this.boardChars, // 64 characters representing the board
required this.currentColor, // int: TurnColor.black or TurnColor.white
required this.logLevel,
});

final String boardChars;
final int currentColor;
final List<SquareReplacement> replacementTargets;
final Level logLevel;
}

Expand All @@ -41,19 +40,28 @@ class SetboardResponse implements ResponseSchema<SetboardRequest> {
}

SetboardResponse executeSetboard(final LibEdax edax, final SetboardRequest request) {
edax.edaxStop();
edax.edaxStop(); // Existing behavior

final board = edax.edaxGetBoard();
var boardStr = board.stringApplicableToSetboard(edax.edaxGetCurrentPlayer());
for (final replacementTarget in request.replacementTargets) {
boardStr = boardStr.replaceFirst(RegExp('.'), replacementTarget.char, replacementTarget.offset);
// Validate boardChars length. Robust validation should ideally be done by the caller.
if (request.boardChars.length != 64) {
final logger = Logger(level: request.logLevel);
logger.e('Error: boardChars length in SetboardRequest is not 64. Actual: ${request.boardChars.length}');
// This is a programming error if it happens. Consider throwing an ArgumentError.
// For now, to prevent crashing the edax server, we might return an error state or
// an "empty" board, though throwing is often better for contract violations.
// However, edax.edaxSetboard might also crash if given a malformed string.
// Let's assume the caller (BoardNotifier) ensures this.
}
final currentColorChar = request.currentColor == TurnColor.black ? ColorChar.black : ColorChar.white;
boardStr = boardStr.replaceFirst(RegExp('.'), currentColorChar, 64);

// Convert currentColor (int) to its character representation.
// ColorChar.black is 'X', ColorChar.white is 'O'.
final String playerChar = (request.currentColor == TurnColor.black) ? ColorChar.black : ColorChar.white;

final String fullBoardString = request.boardChars + playerChar;

final logger = Logger(level: request.logLevel);
logger.d('setboard $boardStr');
edax.edaxSetboard(boardStr);
logger.d('setboard $fullBoardString'); // Log the full 65-char string being sent to edax
edax.edaxSetboard(fullBoardString);

return SetboardResponse(
board: edax.edaxGetBoard(),
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"shortcutCheatsheet": "shortcut cheatsheet",
"shortcutLabelCopyMoves": "copy moves",
"shortcutLabelPasteMoves": "paste moves",
"shortcutLabelPastePosition": "Paste Position",
"@shortcutLabelPastePosition": {
"description": "Label for the menu item or shortcut that pastes a full board position from clipboard"
},
"shortcutLabelRedoAll": "redo all",
"shortcutLabelRedo": "redo",
"shortcutLabelSwitchHintVisibility": "switch hint Visibility",
Expand Down
67 changes: 64 additions & 3 deletions lib/models/board_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,69 @@ class BoardNotifier extends ValueNotifier<BoardState> {
}

void requestSetboard(final List<int> replacementTargetMoves) {
final arrangeTargetChar = replacementTargetMoves.map((m) => SquareReplacement(m, value.arrangeTargetChar)).toList();
// Get current board string (64 chars for board, 1 for player)
// board.string() returns the 64 squares followed by the player to move (X or O usually)
String currentBoardWithPlayer = value.board.string(value.currentColor);
if (currentBoardWithPlayer.length != 65) {
_logger.e("Error: value.board.string() did not return 65 characters. Got: ${currentBoardWithPlayer.length}. Current color: ${value.currentColor}");
// Fallback: use a default empty board string matching current color
final String playerChar = (value.currentColor == TurnColor.black) ? 'X' : 'O';
currentBoardWithPlayer = List.filled(64, '-').join('') + playerChar;
}

List<String> boardCharsList = currentBoardWithPlayer.substring(0, 64).split('');

for (final int moveOffset in replacementTargetMoves) {
if (moveOffset >= 0 && moveOffset < boardCharsList.length) {
boardCharsList[moveOffset] = value.arrangeTargetChar; // 'X', 'O', or '-'
}
}
final String newBoardChars = boardCharsList.join('');
final int colorForRequest = value.arrangeTargetColor; // This is already an int (TurnColor)

_edaxServerPort.send(
SetboardRequest(
boardChars: newBoardChars,
currentColor: colorForRequest,
logLevel: Logger.level,
),
);
_logger.d('Requested setboard (arrange mode) with ${replacementTargetMoves.length} changes. Board: $newBoardChars, Player: $colorForRequest');
}

void requestSetBoardFromString(final String positionString) {
if (positionString.length != 65) {
_logger.w('Invalid position string length: ${positionString.length}');
return;
}

final String boardChars = positionString.substring(0, 64);
final String playerTurnChar = positionString.substring(64);

int newCurrentColor;
if (playerTurnChar == 'X') { // Assuming 'X' maps to ColorChar.black
newCurrentColor = TurnColor.black;
} else if (playerTurnChar == 'O') { // Assuming 'O' maps to ColorChar.white
newCurrentColor = TurnColor.white;
} else {
_logger.w('Invalid player turn character in positionString: $playerTurnChar');
return;
}

_edaxServerPort.send(
SetboardRequest(
currentColor: value.arrangeTargetColor,
replacementTargets: arrangeTargetChar,
boardChars: boardChars, // The 64 character board string
currentColor: newCurrentColor, // The integer turn color
logLevel: Logger.level,
),
);

if (value.mode != BoardMode.freePlay) {
value.mode = BoardMode.freePlay;
// Potentially notifyListeners() if mode change needs immediate UI update
// before SetboardResponse, though SetboardResponse will also notify.
}
_logger.d('Requested set board from string: $positionString');
}

void finishedNotifyBookHasBeenLoadedToUser() => value.bookLoadStatus = BookLoadStatus.notifiedToUser;
Expand Down Expand Up @@ -289,6 +344,7 @@ class BoardNotifier extends ValueNotifier<BoardState> {
..currentColor = message.currentColor
..lastMove = message.lastMove
..currentMoves = message.moves;
await _onMovesChanged(message.moves);
} else if (message is SetOptionResponse) {
// do nothing
} else if (message is StopResponse) {
Expand All @@ -297,4 +353,9 @@ class BoardNotifier extends ValueNotifier<BoardState> {
_logger.w('response ${message.runtimeType} is not supported');
}
}

@visibleForTesting
void testerSetEdaxServerPort(SendPort port) {
_edaxServerPort = port;
}
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dev_dependencies:
path_provider_platform_interface: ^2.1.2
pedantic_sensuikan1973: ^5.12.0
plugin_platform_interface: ^2.1.8
mocktail: ^1.0.0 # Added mocktail for testing
# https://pub.dev/packages/shared_preferences_platform_interface/changelog#220
# https://github.com/sensuikan1973/pedax/pull/1387#issuecomment-1489619974
shared_preferences_platform_interface: ^2.4.1
Expand Down
Loading
Loading