feat: added week numbers toggle to Clock calendar (#2933)

Provide an optional week number column in the Clock popup calendar while
keeping locale-based numbering and preserving header layout.

Co-authored-by: Spencer <your_github_username@users.noreply.github.com>
This commit is contained in:
Spencer Janyk
2026-02-22 02:09:19 -08:00
committed by GitHub
parent f3b633d259
commit 157477ad29
2 changed files with 108 additions and 10 deletions

View File

@@ -18,6 +18,7 @@ internal class Popup: PopupWrapper {
private var calendarView: CalendarView? = nil private var calendarView: CalendarView? = nil
private var calendarState: Bool = true private var calendarState: Bool = true
private var weekNumbersState: Bool = false
public init(_ module: ModuleType) { public init(_ module: ModuleType) {
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0)) super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
@@ -25,8 +26,9 @@ internal class Popup: PopupWrapper {
self.orientation = .vertical self.orientation = .vertical
self.spacing = Constants.Popup.margins self.spacing = Constants.Popup.margins
self.calendarView = CalendarView(self.frame.width)
self.calendarState = Store.shared.bool(key: "\(self.title)_calendar", defaultValue: self.calendarState) self.calendarState = Store.shared.bool(key: "\(self.title)_calendar", defaultValue: self.calendarState)
self.weekNumbersState = Store.shared.bool(key: "\(self.title)_calendarWeekNumbers", defaultValue: self.weekNumbersState)
self.calendarView = CalendarView(self.frame.width, showWeekNumbers: self.weekNumbersState)
self.orderTableView.reorderCallback = { [weak self] in self.orderTableView.reorderCallback = { [weak self] in
self?.rearrange() self?.rearrange()
@@ -94,6 +96,10 @@ internal class Popup: PopupWrapper {
PreferencesRow(localizedString("Calendar"), component: switchView( PreferencesRow(localizedString("Calendar"), component: switchView(
action: #selector(self.toggleCalendarState), action: #selector(self.toggleCalendarState),
state: self.calendarState state: self.calendarState
)),
PreferencesRow(localizedString("Show week numbers"), component: switchView(
action: #selector(self.toggleWeekNumbersState),
state: self.weekNumbersState
)) ))
])) ]))
@@ -126,10 +132,19 @@ internal class Popup: PopupWrapper {
} }
self.recalculateHeight() self.recalculateHeight()
} }
@objc private func toggleWeekNumbersState(_ sender: NSControl) {
self.weekNumbersState = controlState(sender)
Store.shared.set(key: "\(self.title)_calendarWeekNumbers", value: self.weekNumbersState)
self.calendarView?.setShowWeekNumbers(self.weekNumbersState)
self.recalculateHeight()
}
} }
private class CalendarView: NSStackView { private class CalendarView: NSStackView {
private let itemSize: CGSize private var itemSize: CGSize
private var showWeekNumbers: Bool
private var navigationHeightConstraint: NSLayoutConstraint?
private var year: Int private var year: Int
private var month: Int private var month: Int
@@ -158,11 +173,9 @@ private class CalendarView: NSStackView {
private var grid: NSGridView = NSGridView() private var grid: NSGridView = NSGridView()
private var current: NSTextField = NSTextField() private var current: NSTextField = NSTextField()
init(_ width: CGFloat) { init(_ width: CGFloat, showWeekNumbers: Bool) {
self.itemSize = NSSize( self.showWeekNumbers = showWeekNumbers
width: (width-(Constants.Popup.margins*2))/7, self.itemSize = NSSize.zero
height: (width-(Constants.Popup.spacing*2))/8 - 4
)
self.year = Calendar.current.component(.year, from: Date()) self.year = Calendar.current.component(.year, from: Date())
self.month = Calendar.current.component(.month, from: Date()) self.month = Calendar.current.component(.month, from: Date())
self.day = Calendar.current.component(.day, from: Date()) self.day = Calendar.current.component(.day, from: Date())
@@ -182,6 +195,7 @@ private class CalendarView: NSStackView {
self.wantsLayer = true self.wantsLayer = true
self.layer?.cornerRadius = 2 self.layer?.cornerRadius = 2
self.updateItemSize()
self.addArrangedSubview(self.navigation()) self.addArrangedSubview(self.navigation())
self.setup() self.setup()
} }
@@ -200,6 +214,13 @@ private class CalendarView: NSStackView {
self.setup() self.setup()
} }
public func setShowWeekNumbers(_ state: Bool) {
guard self.showWeekNumbers != state else { return }
self.showWeekNumbers = state
self.updateItemSize()
self.setup()
}
override func updateLayer() { override func updateLayer() {
self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor
} }
@@ -210,11 +231,21 @@ private class CalendarView: NSStackView {
let grid = NSGridView() let grid = NSGridView()
grid.rowSpacing = 0 grid.rowSpacing = 0
grid.columnSpacing = 0 grid.columnSpacing = 0
grid.addRow(with: self.weekDays.map { headerItem($0) })
var headerRow: [NSView] = []
if self.showWeekNumbers {
headerRow.append(self.weekNumberHeaderItem())
}
headerRow.append(contentsOf: self.weekDays.map { headerItem($0) })
grid.addRow(with: headerRow)
let weeks = self.generateDays(for: self.month, in: self.year) let weeks = self.generateDays(for: self.month, in: self.year)
for week in weeks { for week in weeks {
let labels = week.map { rowItem($0) } var labels: [NSView] = []
if self.showWeekNumbers {
labels.append(self.weekNumberItem(week))
}
labels.append(contentsOf: week.map { rowItem($0) })
grid.addRow(with: labels) grid.addRow(with: labels)
} }
@@ -226,14 +257,21 @@ private class CalendarView: NSStackView {
private func navigation() -> NSView { private func navigation() -> NSView {
let view = NSStackView() let view = NSStackView()
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true view.distribution = .fill
view.alignment = .centerY
self.navigationHeightConstraint = view.heightAnchor.constraint(greaterThanOrEqualToConstant: max(self.itemSize.height, 24))
self.navigationHeightConstraint?.isActive = true
view.orientation = .horizontal view.orientation = .horizontal
let details = NSTextField(labelWithString: "\(Calendar.current.standaloneMonthSymbols[self.month-1]) \(self.year)") let details = NSTextField(labelWithString: "\(Calendar.current.standaloneMonthSymbols[self.month-1]) \(self.year)")
details.font = .systemFont(ofSize: 16, weight: .medium) details.font = .systemFont(ofSize: 16, weight: .medium)
details.lineBreakMode = .byTruncatingTail
details.maximumNumberOfLines = 1
details.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
self.current = details self.current = details
let buttons = NSStackView() let buttons = NSStackView()
buttons.orientation = .horizontal buttons.orientation = .horizontal
buttons.setContentCompressionResistancePriority(.required, for: .horizontal)
let prev = NSButton() let prev = NSButton()
prev.bezelStyle = .regularSquare prev.bezelStyle = .regularSquare
@@ -277,6 +315,15 @@ private class CalendarView: NSStackView {
return view return view
} }
private func updateItemSize() {
let columns: CGFloat = self.showWeekNumbers ? 8 : 7
self.itemSize = NSSize(
width: (self.frame.width-(Constants.Popup.margins*2))/columns,
height: (self.frame.width-(Constants.Popup.spacing*2))/8 - 4
)
self.navigationHeightConstraint?.constant = max(self.itemSize.height, 24)
}
private func headerItem(_ value: String) -> NSView { private func headerItem(_ value: String) -> NSView {
let view = NSTextField() let view = NSTextField()
let cell = VerticallyCenteredTextFieldCell(textCell: value) let cell = VerticallyCenteredTextFieldCell(textCell: value)
@@ -306,6 +353,55 @@ private class CalendarView: NSStackView {
return view return view
} }
private func weekNumberHeaderItem() -> NSView {
let view = NSTextField()
let cell = VerticallyCenteredTextFieldCell(textCell: localizedString("Wk"))
view.cell = cell
view.alignment = .center
view.textColor = .secondaryLabelColor
view.font = .systemFont(ofSize: 11)
view.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
self.addRightBorder(view)
return view
}
private func weekNumberItem(_ week: [DateComponents]) -> NSView {
let calendar = Calendar.current
let firstDate = week.compactMap { calendar.date(from: $0) }.first ?? Date()
let weekNumber = calendar.component(.weekOfYear, from: firstDate)
let view = NSTextField()
let cell = VerticallyCenteredTextFieldCell(textCell: "\(weekNumber)")
view.cell = cell
view.alignment = .center
view.textColor = .secondaryLabelColor
view.font = .systemFont(ofSize: 11)
view.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
self.addRightBorder(view)
return view
}
private func addRightBorder(_ view: NSView) {
let border = NSView()
border.wantsLayer = true
let borderColor = self.isDarkMode
? NSColor.white.withAlphaComponent(0.4)
: NSColor.separatorColor
border.layer?.backgroundColor = borderColor.cgColor
border.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(border)
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
NSLayoutConstraint.activate([
border.trailingAnchor.constraint(equalTo: view.trailingAnchor),
border.topAnchor.constraint(equalTo: view.topAnchor),
border.bottomAnchor.constraint(equalTo: view.bottomAnchor),
border.widthAnchor.constraint(equalToConstant: lineWidth)
])
}
private func todayItem() -> NSView { private func todayItem() -> NSView {
let view = NSView() let view = NSView()

View File

@@ -471,6 +471,8 @@
"Time zone" = "Time zone"; "Time zone" = "Time zone";
"Local" = "Local"; "Local" = "Local";
"Calendar" = "Calendar"; "Calendar" = "Calendar";
"Show week numbers" = "Show week numbers";
"Wk" = "Wk";
"Local time" = "Local time"; "Local time" = "Local time";
"Add new clock" = "Add new clock"; "Add new clock" = "Add new clock";
"Delete selected clock" = "Delete selected clock"; "Delete selected clock" = "Delete selected clock";