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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ coaches on macOS. Part of the DanceChess family.

## Features

- **Single-window workflow** — board + notation on top, your game list below.
- **One window per PGN, as tabs** — board + notation on top, that file's game list below; open files come back on launch.
Arrow keys browse games and step through moves without ever touching the
mouse; `Enter` dives into a game, `Esc` comes back.
- **PGN is the source of truth** — open any .pgn and its games *are* the
Expand Down
12 changes: 7 additions & 5 deletions app/Studio/Database/GameListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ struct GameListView: NSViewRepresentable {
let onDeleteRequest: ((Int64) -> Void)?
/// Right-click with several rows selected → Merge Selected Games.
var onMergeRequest: (([Int64]) -> Void)? = nil
/// The game to land on when the list first fills (the one last viewed
/// in this file); nil = row 0.
var initialSelection: Int64? = nil

func makeCoordinator() -> Coordinator { Coordinator(view: self) }

Expand Down Expand Up @@ -157,23 +160,22 @@ struct GameListView: NSViewRepresentable {
/// Quiets delegate callbacks during programmatic re-selection.
private var reselecting = false
private var didInitialSelect = false
/// Startup-only: restore the last-viewed game once, then never again
/// (a replaced list must not inherit it).
private var pendingInitialId: Int64? = UserDefaults.standard
.object(forKey: DatabaseStore.lastSelectedGameKey) as? Int64
/// Consumed once, when the list first fills.
private var pendingInitialId: Int64?

init(view: GameListView) {
self.view = view
self.count = view.count
self.revision = view.revision
self.generation = view.generation
self.pendingInitialId = view.initialSelection
}

func resetForNewList() {
selectedId = nil
selectedRow = -1
didInitialSelect = false
pendingInitialId = nil // only the startup list restores it
pendingInitialId = view.initialSelection
}

func deselectQuietly() {
Expand Down
18 changes: 12 additions & 6 deletions app/Studio/Engine/OpeningTreePanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import DanceChessCore
@Observable
@MainActor
final class OpeningTreeModel {
/// The window's list: the local statistics and the games-reaching-
/// this-position filter are about this file.
let store: DatabaseStore

init(store: DatabaseStore) { self.store = store }

private(set) var visible = false
private(set) var rows: [TreeMove] = []
private(set) var source: ReferenceSource = AppSettings.shared.referenceSource
Expand Down Expand Up @@ -49,7 +55,7 @@ final class OpeningTreeModel {
errorText = nil
openingName = nil
noveltyText = nil
DatabaseStore.shared.setPositionFilter(nil)
store.setPositionFilter(nil)
}

func setSource(_ source: ReferenceSource) {
Expand All @@ -72,7 +78,7 @@ final class OpeningTreeModel {
self.fen = fen
self.parentFen = parentFen
self.san = san
DatabaseStore.shared.setPositionFilter(fen)
store.setPositionFilter(fen)
generation += 1
let gen = generation
pending?.cancel()
Expand All @@ -82,9 +88,9 @@ final class OpeningTreeModel {
loading = false
errorText = nil
openingName = nil
rows = (try? DatabaseStore.shared.db?.openingTree(fen: fen)) ?? []
rows = (try? store.db?.openingTree(fen: fen)) ?? []
if let parentFen, let san,
let parentRows = try? DatabaseStore.shared.db?.openingTree(fen: parentFen) {
let parentRows = try? store.db?.openingTree(fen: parentFen) {
noveltyText = Self.novelty(san: san, in: parentRows,
total: parentRows.reduce(0) { $0 + $1.games },
source: source)
Expand Down Expand Up @@ -174,10 +180,10 @@ struct OpeningTreePanel: View {
@State private var showSettings = false

private var statusText: String {
let local = "\(DatabaseStore.shared.matchedCount) in list"
let local = "\(tree.store.matchedCount) in list"
switch tree.source {
case .database:
return "\(DatabaseStore.shared.matchedCount) games reach this position"
return "\(tree.store.matchedCount) games reach this position"
default:
var parts = ["\(tree.onlineTotal) games"]
if let name = tree.openingName { parts.append(name) }
Expand Down
23 changes: 15 additions & 8 deletions app/Studio/GameArea.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,18 +209,18 @@ enum SavePrompt {
static func run(for session: GameSession, canCancel: Bool) -> Bool {
guard !suppressPrompts else { return true }
let isNewGame = session.sourceGameId < 0
guard let store = session.store, store.canWriteBack else { return true }
if isNewGame {
// scratch entry: worth rescuing only if it has moves and a
// single-file list to land in
guard session.hasMoves, DatabaseStore.shared.canWriteBack else { return true }
// scratch entry: worth rescuing only if it has moves
guard session.hasMoves else { return true }
} else {
guard session.isModified else { return true }
}
let alert = NSAlert()
alert.messageText = "Unsaved Changes"
alert.informativeText = isNewGame
? "Save this new game into “\(DatabaseStore.shared.sourceName ?? "the list")”?"
: "Save the changes to “\(session.gameTitle)” to the database?"
? "Save this new game into “\(store.sourceName ?? "the list")”?"
: "Save the changes to “\(session.gameTitle)” to “\(store.sourceName ?? "the list")”?"
alert.addButton(withTitle: "Save")
alert.addButton(withTitle: "Don't Save")
if canCancel { alert.addButton(withTitle: "Cancel") }
Expand All @@ -236,9 +236,12 @@ enum SavePrompt {

@discardableResult
static func save(_ session: GameSession) -> Bool {
guard let store = session.store else {
session.errorText = "This game's file is no longer open"
return false
}
do {
try DatabaseStore.shared.updateGame(id: session.sourceGameId,
pgn: session.game.toPgn())
try store.updateGame(id: session.sourceGameId, pgn: session.game.toPgn())
session.markSaved()
return true
} catch {
Expand All @@ -250,8 +253,12 @@ enum SavePrompt {
/// Appends a newly entered (scratch) game to the open list + PGN file.
@discardableResult
static func appendNewGame(_ session: GameSession) -> Bool {
guard let store = session.store else {
session.errorText = "No open file to save into"
return false
}
do {
let id = try DatabaseStore.shared.addGame(pgn: session.game.toPgn())
let id = try store.addGame(pgn: session.game.toPgn())
session.attachToDatabase(id: id)
return true
} catch {
Expand Down
20 changes: 13 additions & 7 deletions app/Studio/GameWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import DanceChessCore
/// The single-window flow lives in MainWindow; this one keeps its own
/// Open/Paste PGN toolbar for scratch use.
struct GameWindow: View {
/// A `games.id` from the default database, or -1 for a blank board.
var gameId: Int64 = -1
/// Which file's game, or a blank board (`path` nil / `id` -1). Saving
/// needs that file's window to still be open; otherwise the board is
/// scratch.
var ref: GameRef

@State private var session = GameSession()
@State private var engine = EngineSession()
Expand All @@ -33,8 +35,12 @@ struct GameWindow: View {
})
.onAppear {
keyMonitor.install { handleKey($0) }
if gameId >= 0, let pgn = DatabaseStore.shared.pgn(for: gameId) {
session.loadPgn(pgn, sourceId: gameId)
if let path = ref.path,
let store = OpenStores.shared.store(for: URL(fileURLWithPath: path)) {
session.store = store
if ref.id >= 0, let pgn = store.pgn(for: ref.id) {
session.loadPgn(pgn, sourceId: ref.id)
}
}
}
.onDisappear {
Expand All @@ -53,12 +59,12 @@ struct GameWindow: View {
.toolbar {
ToolbarItemGroup {
Button("Save", systemImage: "square.and.arrow.down") { saveGame() }
.disabled(!DatabaseStore.shared.canWriteBack)
.disabled(!(session.store?.canWriteBack ?? false))
.help("Save this game into the open list and its PGN file (⌘S)")
Button("Game Info", systemImage: "square.and.pencil") {
showSaveSheet = true
}
.disabled(!DatabaseStore.shared.canWriteBack)
.disabled(!(session.store?.canWriteBack ?? false))
.help("Edit the game's players, result, event… (⌘I)")
Button("Open PGN", systemImage: "folder") { showImporter = true }
.help("Load a PGN into this board (scratch, not the list)")
Expand Down Expand Up @@ -92,7 +98,7 @@ struct GameWindow: View {
}
.sheet(isPresented: $showSaveSheet) {
GameInfoSheet(session: session,
listName: DatabaseStore.shared.sourceName ?? "list") {
listName: session.store?.sourceName ?? "list") {
if session.sourceGameId >= 0 {
SavePrompt.save(session)
} else {
Expand Down
Loading
Loading