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
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public nonisolated struct SidebarLayout: Hashable, Sendable {
rows.append(SidebarRow(
key: .section(section.id), y: y, height: m.sectionHeaderHeight, section: section.id,
group: nil, siblingIndex: 0, parentIndex: nil, isLastInGroup: false,
isCollapsed: collapsed, childCount: nodes.count, groupColor: nil
isCollapsed: collapsed, childCount: nodes.count, groupColor: nil,
titlesProjects: section.machine != nil && machineCount == 1
))
y += m.sectionHeaderHeight + m.rowSpacing
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ public nonisolated struct SidebarLayoutOptions: Hashable, Sendable {
/// Live gap to open, and its height.
public var gap: DropPosition?
public var gapHeight: CGFloat = 0
/// Show the machine header even when only one machine is listed.
/// Off by default: headers appear once a Cloud or SSH machine joins.
/// Show the machine header even when only one machine is listed,
/// titled "Projects" (the sidebar list sets it). Off in bare layouts.
public var showsSoleMachineHeader = false
/// Include tab rows beneath each visible workspace.
public var showWorkspaceTabs = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public nonisolated struct SidebarRow: Hashable, Sendable {
public var groupColor: GroupColor?
/// Kind of tab represented by this row, when applicable.
public var tabKind: SidebarTabKind? = nil
/// A machine header standing for the only machine: titled "Projects".
public var titlesProjects = false

public var maxY: CGFloat { y + height }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2097,6 +2097,137 @@
}
}
},
"sidebar.section.projects": {
"extractionState": "manual",
"localizations": {
"ar": {
"stringUnit": {
"state": "translated",
"value": "المشاريع"
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Projekti"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Projekter"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Projekte"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Projects"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Proyectos"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Projets"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Progetti"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "プロジェクト"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "គម្រោង"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "프로젝트"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Prosjekter"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Projekty"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Projetos"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Проекты"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "โปรเจกต์"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Projeler"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Проєкти"
}
},
"vi": {
"stringUnit": {
"state": "translated",
"value": "Dự án"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "项目"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "專案"
}
}
}
},
"sidebar.room.newName": {
"extractionState": "manual",
"localizations": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum Strings {
static var newWorkspace: String { String(localized: "sidebar.newWorkspace", defaultValue: "New Workspace", bundle: .module) }
static var rename: String { String(localized: "sidebar.rename", defaultValue: "Rename", bundle: .module) }
static var pinned: String { String(localized: "sidebar.section.pinned", defaultValue: "Pinned", bundle: .module) }
static var projects: String { String(localized: "sidebar.section.projects", defaultValue: "Projects", bundle: .module) }
static var pinnedEmpty: String { String(localized: "sidebar.section.pinned.empty", defaultValue: "Drop here to pin", bundle: .module) }
static var sectionEmpty: String { String(localized: "sidebar.section.empty", defaultValue: "No workspaces", bundle: .module) }
static var statusConnected: String { String(localized: "sidebar.machine.connected", defaultValue: "Connected", bundle: .module) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,21 @@ final class SectionHeaderRowView: SidebarRowView {
// The section's kind, not its nodes: comparing 1,000 children per
// reload would defeat the point.
var kind: SidebarSection.Kind
var titlesProjects: Bool
var collapsed: Bool
var fontSize: CGFloat
var iconSize: CGFloat
}

func configure(_ section: SidebarSection, row: SidebarRow) {
let content = Content(
kind: section.kind, collapsed: row.isCollapsed,
kind: section.kind, titlesProjects: row.titlesProjects, collapsed: row.isCollapsed,
fontSize: SidebarStyle.headerFont.pointSize, iconSize: Metrics.smallIconSize
)
guard needsConfigure(content) else { return }
collapsed = row.isCollapsed
let symbol: String
let title: String
var title: String
switch section.kind {
case .pinned:
symbol = "pin.fill"
Expand Down Expand Up @@ -91,7 +92,16 @@ final class SectionHeaderRowView: SidebarRowView {
setAccessibilityLabel(label)
setAccessibilityHelp(machine.detail)
}
if section.kind == .pinned { setAccessibilityLabel(title) }
// The only machine needs no name or status: the header heads the
// workspace list, apart from the destinations above it.
if row.titlesProjects {
title = Strings.projects
statusTone = nil
badgeText = nil
toolTip = nil
setAccessibilityHelp(nil)
}
if section.kind == .pinned || row.titlesProjects { setAccessibilityLabel(title) }
glyph.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)?
.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: Metrics.smallIconSize - Metrics.space1, weight: .semibold))
name.stringValue = title
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ final class SidebarListView: NSView {
var o = SidebarLayoutOptions()
o.filterMatches = model.filterMatches
o.showWorkspaceTabs = model.showWorkspaceTabs
o.showsSoleMachineHeader = true
if includeGap, case let .newWorkspace(section, group, index)? = external?.proposal {
o.gap = DropPosition(section: section, group: group, index: index)
o.gapHeight = metrics.rowHeight
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import AppKit
import Testing
@testable import CmuxNextSidebar

/// With one machine the workspace list sits under a "Projects" header, so
/// Home and the App Store above it read as destinations, not workspaces.
/// A second machine brings back the machine names.
@MainActor @Suite struct SidebarProjectsHeaderTests {
func list(_ sections: [SidebarSection]) -> SidebarListView {
let sidebar = SidebarView(model: SidebarModel(sections: sections, activeWorkspaceID: id("a")))
sidebar.frame = NSRect(x: 0, y: 0, width: 260, height: 600)
sidebar.layoutSubtreeIfNeeded()
sidebar.list.reload(animated: false)
return sidebar.list
}

var localOnly: [SidebarSection] {
[SidebarSection(kind: .machine(SidebarMachine(id: .local, name: "This Mac", kind: .local)),
nodes: [.workspace(w("a")), .workspace(w("b"))])]
}

@Test func soleMachineListsUnderProjects() {
let list = list(localOnly)
#expect(list.displayed.rows.map(\.key) == [.section(local), .workspace(id("a")), .workspace(id("b"))])
#expect(list.displayed.row(for: .section(local))?.titlesProjects == true)
let header = list.rowViews[.section(local)] as? SectionHeaderRowView
#expect(header?.accessibilityLabel() == "Projects")
}

@Test func projectsCollapses() {
var sections = localOnly
sections[0].isCollapsed = true
#expect(list(sections).displayed.rows.map(\.key) == [.section(local)])
}

@Test func secondMachineKeepsMachineNames() {
let list = list(fixture())
#expect(list.displayed.row(for: .section(local))?.titlesProjects == false)
#expect(list.displayed.row(for: .section(cloudSection))?.titlesProjects == false)
let header = list.rowViews[.section(local)] as? SectionHeaderRowView
#expect(header?.accessibilityLabel() != "Projects")
}
}
Loading