Skip to content
Merged
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
4 changes: 2 additions & 2 deletions ComputerSolitaire.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = crapshack.ComputerSolitaire;
PRODUCT_NAME = "Computer Solitaire";
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -295,7 +295,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = crapshack.ComputerSolitaire;
PRODUCT_NAME = "Computer Solitaire";
REGISTER_APP_GROUPS = YES;
Expand Down
1 change: 0 additions & 1 deletion ComputerSolitaire/CommandNotifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ import Foundation
extension Notification.Name {
static let openSettings = Notification.Name("openSettings")
static let openRulesAndScoring = Notification.Name("openRulesAndScoring")
static let openStatistics = Notification.Name("openStatistics")
}
1 change: 1 addition & 0 deletions ComputerSolitaire/ComputerSolitaireApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct ComputerSolitaireApp: App {
Label("Rules & Scoring", systemImage: "book")
}
}
GameMenuCommands()
#endif
CommandGroup(replacing: .appSettings) {
Button {
Expand Down
100 changes: 100 additions & 0 deletions ComputerSolitaire/GameMenuCommands.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import SwiftUI

#if os(macOS)
struct GameMenuActions {
var newGame: () -> Void
var redeal: () -> Void
var undo: () -> Void
var autoFinish: () -> Void
var hint: () -> Void
var showStatistics: () -> Void
}

struct GameMenuState {
var canUndo: Bool
var canAutoFinish: Bool
var canHint: Bool
var isHintVisible: Bool
var isAutoFinishing: Bool
}

private struct GameMenuActionsFocusedKey: FocusedValueKey {
typealias Value = GameMenuActions
}

private struct GameMenuStateFocusedKey: FocusedValueKey {
typealias Value = GameMenuState
}

extension FocusedValues {
var gameMenuActions: GameMenuActions? {
get { self[GameMenuActionsFocusedKey.self] }
set { self[GameMenuActionsFocusedKey.self] = newValue }
}

var gameMenuState: GameMenuState? {
get { self[GameMenuStateFocusedKey.self] }
set { self[GameMenuStateFocusedKey.self] = newValue }
}
}

struct GameMenuCommands: Commands {
@FocusedValue(\.gameMenuActions) private var actions
@FocusedValue(\.gameMenuState) private var state

var body: some Commands {
CommandMenu("Game") {
Button {
actions?.newGame()
} label: {
Label("New Game", systemImage: "plus")
}
.keyboardShortcut("n", modifiers: [.command, .shift])
.disabled(actions == nil)

Button {
actions?.redeal()
} label: {
Label("Redeal", systemImage: "arrow.clockwise")
}
.disabled(actions == nil)

Divider()

Button {
actions?.undo()
} label: {
Label("Undo", systemImage: "arrow.uturn.backward")
}
.keyboardShortcut("z", modifiers: [.command, .option])
.disabled(!(state?.canUndo ?? false))

Button {
actions?.autoFinish()
} label: {
Label(state?.isAutoFinishing == true ? "Stop Auto Finish" : "Auto Finish", systemImage: "bolt")
}
.disabled(!(state?.canAutoFinish ?? false))

if state?.isHintVisible ?? false {
Button {
actions?.hint()
} label: {
Label("Hint", systemImage: "lightbulb")
}
.keyboardShortcut("h", modifiers: [.command, .shift])
.disabled(!(state?.canHint ?? false))
}

Divider()

Button {
actions?.showStatistics()
} label: {
Label("Statistics…", systemImage: "chart.bar")
}
.disabled(actions == nil)
}
}
}
#endif
79 changes: 60 additions & 19 deletions ComputerSolitaire/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ struct ContentView: View {

private func applyToolbar(to view: AnyView) -> AnyView {
AnyView(
view.toolbar {
view
.toolbar {
#if os(iOS)
ToolbarItemGroup(placement: .bottomBar) {
Menu {
Expand Down Expand Up @@ -216,15 +217,23 @@ struct ContentView: View {
}
#endif
#if os(macOS)
ToolbarItemGroup(placement: .automatic) {
Button("New Game") {
ToolbarSpacer(.flexible)
ToolbarItemGroup(placement: .primaryAction) {
Button {
startNewGameFromUI()
} label: {
Label("New Game", systemImage: "plus")
.labelStyle(.titleAndIcon)
}
Button("Redeal") {
Button {
redealFromUI()
} label: {
Label("Redeal", systemImage: "arrow.clockwise")
.labelStyle(.titleAndIcon)
}
}
ToolbarItem(placement: .automatic) {
ToolbarSpacer(.fixed)
ToolbarItemGroup(placement: .primaryAction) {
Button {
stopAutoFinish()
beginUndoAnimationIfNeeded()
Expand All @@ -234,37 +243,28 @@ struct ContentView: View {
.labelStyle(.iconOnly)
.help("Undo")
.disabled(isUndoDisabled)
}
ToolbarItem(placement: .automatic) {
Button {
startAutoFinish()
} label: {
Label("Auto Finish", systemImage: "bolt")
}
.help("Auto Finish")
.disabled(isAutoFinishDisabled)
}
if isHintButtonVisible {
ToolbarItem(placement: .automatic) {
if isHintButtonVisible {
Button {
triggerHint()
} label: {
Label("Hint", systemImage: "lightbulb")
}
.help("Hint")
.keyboardShortcut("h", modifiers: [])
.disabled(isHintDisabled)
}
}
ToolbarItem(placement: .primaryAction) {
Button {
isShowingStats = true
} label: {
Label("Statistics", systemImage: "chart.bar")
}
.help("Statistics")
}
ToolbarItem(placement: .primaryAction) {
Button {
isShowingSettings = true
} label: {
Expand Down Expand Up @@ -306,17 +306,18 @@ struct ContentView: View {
}

private func applyObservers(to view: AnyView) -> AnyView {
AnyView(
let commandObservedView = AnyView(
view
.onReceive(NotificationCenter.default.publisher(for: .openSettings)) { _ in
isShowingSettings = true
}
.onReceive(NotificationCenter.default.publisher(for: .openRulesAndScoring)) { _ in
presentRulesAndScoring(initialSection: .rules)
}
.onReceive(NotificationCenter.default.publisher(for: .openStatistics)) { _ in
isShowingStats = true
}
)

let gameStateObservedView = AnyView(
commandObservedView
.onChange(of: drawModeRawValue) { (_, newValue: Int) in
let mode = DrawMode(rawValue: newValue) ?? .three
viewModel.updateDrawMode(mode)
Expand Down Expand Up @@ -362,6 +363,10 @@ struct ContentView: View {
processPendingAutoMoveIfPossible()
queueAutoFinishStepIfPossible()
}
)

return AnyView(
gameStateObservedView
.onChange(of: scenePhase) { _, _ in
syncLifecyclePauseState()
}
Expand All @@ -377,6 +382,10 @@ struct ContentView: View {
winCelebration.cancelTask()
persistGameNow()
}
#if os(macOS)
.focusedSceneValue(\.gameMenuActions, gameMenuActions)
.focusedSceneValue(\.gameMenuState, gameMenuState)
#endif
)
}

Expand Down Expand Up @@ -656,6 +665,38 @@ struct ContentView: View {
|| isWinCascadeAnimating
}

#if os(macOS)
private var gameMenuActions: GameMenuActions {
GameMenuActions(
newGame: startNewGameFromUI,
redeal: redealFromUI,
undo: {
stopAutoFinish()
beginUndoAnimationIfNeeded()
},
autoFinish: {
if isAutoFinishing {
stopAutoFinish()
} else {
startAutoFinish()
}
},
hint: triggerHint,
showStatistics: { isShowingStats = true }
)
}

private var gameMenuState: GameMenuState {
GameMenuState(
canUndo: !isUndoDisabled,
canAutoFinish: isAutoFinishing || !isAutoFinishDisabled,
canHint: !isHintDisabled,
isHintVisible: isHintButtonVisible,
isAutoFinishing: isAutoFinishing
)
}
#endif

private func triggerHint() {
guard !isHintDisabled else { return }
stopAutoFinish()
Expand Down