crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 97f69a88839beca6da39c9774ee50b64c81a9416
parent 57bfdf38421d7fd20f202f1cb285c8f4b0625eac
Author: Michael Camilleri <[email protected]>
Date:   Sat, 30 May 2026 13:46:29 +0900

Wire up undo/redo controls in the puzzle grid

The move journal already exposed undo/redo (canUndo/canRedo/undo/redo on
GameMutator), but nothing surfaced it to the player. This commit adds the
controls across the three input surfaces: entry menu, built-in keyboard
and external keyboard.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Views/HardwareKeyboardInputView.swift | 7+++++++
MCrossmate/Views/KeyboardView.swift | 49+++++++++++++++++++++++++++++++++----------------
MCrossmate/Views/PuzzleView.swift | 28++++++++++++++++++++++++++++
3 files changed, 68 insertions(+), 16 deletions(-)

diff --git a/Crossmate/Views/HardwareKeyboardInputView.swift b/Crossmate/Views/HardwareKeyboardInputView.swift @@ -35,7 +35,14 @@ struct HardwareKeyboardInputView: UIViewRepresentable { ) } + let undo = UIKeyCommand(input: "z", modifierFlags: .command, action: #selector(handleKeyCommand(_:))) + undo.discoverabilityTitle = "Undo" + let redo = UIKeyCommand(input: "z", modifierFlags: [.command, .shift], action: #selector(handleKeyCommand(_:))) + redo.discoverabilityTitle = "Redo" + return letters + [ + undo, + redo, UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))), diff --git a/Crossmate/Views/KeyboardView.swift b/Crossmate/Views/KeyboardView.swift @@ -164,27 +164,23 @@ struct KeyboardView: View { .keyWidthMultiplier(metaKeyWidthMultiplier) .popover(isPresented: $showingOverflow) { VStack(alignment: .leading, spacing: 0) { - Button { - showingOverflow = false + overflowItem("Undo", systemImage: "arrow.uturn.backward", isEnabled: session.mutator.canUndo) { + session.mutator.undo() + } + + overflowItem("Redo", systemImage: "arrow.uturn.forward", isEnabled: session.mutator.canRedo) { + session.mutator.redo() + } + + Divider() + + overflowItem("Toggle Draft", systemImage: "pencil") { session.togglePencil() - } label: { - Text("Toggle Draft") - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16) - .padding(.vertical, 12) } - .buttonStyle(.plain) - Button { - showingOverflow = false + overflowItem("Enter Rebus", systemImage: "text.cursor") { session.startRebus() - } label: { - Text("Enter Rebus") - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16) - .padding(.vertical, 12) } - .buttonStyle(.plain) } .frame(minWidth: 160) .presentationCompactAdaptation(.popover) @@ -192,6 +188,27 @@ struct KeyboardView: View { } } + /// One row in the overflow popover. Dismisses the popover, then runs the + /// action — disabled rows (e.g. Undo with nothing to undo) are greyed out. + private func overflowItem( + _ title: String, + systemImage: String, + isEnabled: Bool = true, + action: @escaping () -> Void + ) -> some View { + Button { + showingOverflow = false + action() + } label: { + Label(title, systemImage: systemImage) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + .buttonStyle(.plain) + .disabled(!isEnabled) + } + private func actionKey( systemImage: String, accessibilityLabel: String? = nil, diff --git a/Crossmate/Views/PuzzleView.swift b/Crossmate/Views/PuzzleView.swift @@ -365,6 +365,18 @@ struct PuzzleView: View { private func handleHardwareKeyboardEvent(_ event: HardwareKeyboardEvent) -> Bool { guard !isSolved, !isInputBlocked else { return false } + // Cmd+Z undoes, Shift-Cmd-Z redoes. Caught before the letter switch so + // the modified press isn't read as typing a "Z". + if event.keyCode == .keyboardZ, event.modifierFlags.contains(.command) { + guard !session.isRebusActive else { return false } + if event.modifierFlags.contains(.shift) { + session.mutator.redo() + } else { + session.mutator.undo() + } + return true + } + switch event.keyCode { case .keyboardA, .keyboardB, .keyboardC, .keyboardD, .keyboardE, .keyboardF, .keyboardG, .keyboardH, .keyboardI, .keyboardJ, @@ -815,6 +827,22 @@ private struct PuzzleToolbarModifier: ViewModifier { private var entryMenu: some View { Menu { Section { + Button { + session.mutator.undo() + } label: { + Label("Undo", systemImage: "arrow.uturn.backward") + } + .disabled(!session.mutator.canUndo) + + Button { + session.mutator.redo() + } label: { + Label("Redo", systemImage: "arrow.uturn.forward") + } + .disabled(!session.mutator.canRedo) + } + + Section { Button("Enter Rebus") { session.startRebus() } Button("Toggle Direction") { session.toggleDirection() } }