Skip to content

Commit

Permalink
Add option to change the menu bar icon
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanzhong committed Jan 9, 2024
1 parent edf47b2 commit 4f3c12e
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 18 deletions.
4 changes: 4 additions & 0 deletions LunarBar.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
8714291C2B3DBFDF003FA2CB /* AppMainVC+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8714291B2B3DBFDF003FA2CB /* AppMainVC+Menu.swift */; };
8732DDF32B3E896D004B72DE /* HolidayManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732DDF22B3E896D004B72DE /* HolidayManagerTests.swift */; };
873424D02B35357B00C364BF /* DateGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873424CF2B35357B00C364BF /* DateGridCell.swift */; };
873AD8F62B4C3DAB001C90B1 /* AppIconFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873AD8F52B4C3DAB001C90B1 /* AppIconFactory.swift */; };
8740E2DE2B35DC06004A06C2 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8740E2DD2B35DC06004A06C2 /* InfoPlist.xcstrings */; };
8740E2E22B35DCB7004A06C2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8740E2E12B35DCB7004A06C2 /* Localizable.xcstrings */; };
8740E2FF2B37C841004A06C2 /* HolidayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8740E2FE2B37C841004A06C2 /* HolidayManager.swift */; };
Expand Down Expand Up @@ -51,6 +52,7 @@
8732DDF02B3E896D004B72DE /* LunarBarMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LunarBarMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8732DDF22B3E896D004B72DE /* HolidayManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HolidayManagerTests.swift; sourceTree = "<group>"; };
873424CF2B35357B00C364BF /* DateGridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateGridCell.swift; sourceTree = "<group>"; };
873AD8F52B4C3DAB001C90B1 /* AppIconFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconFactory.swift; sourceTree = "<group>"; };
8740E2D82B35DB93004A06C2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8740E2DD2B35DC06004A06C2 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
8740E2E12B35DCB7004A06C2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -104,6 +106,7 @@
isa = PBXGroup;
children = (
8782F46E2B33F627008B1912 /* AppDefinitions.swift */,
873AD8F52B4C3DAB001C90B1 /* AppIconFactory.swift */,
871429182B3DAF4A003FA2CB /* AppLocalizer.swift */,
8740E31A2B39210F004A06C2 /* AppPreferences.swift */,
);
Expand Down Expand Up @@ -368,6 +371,7 @@
87F81C542B43EBDE0071CA30 /* main.swift in Sources */,
87DA5AFF2B3433D400CE2C1A /* WeekdayView.swift in Sources */,
8740E2FF2B37C841004A06C2 /* HolidayManager.swift in Sources */,
873AD8F62B4C3DAB001C90B1 /* AppIconFactory.swift in Sources */,
8740E30F2B37FB36004A06C2 /* CalendarManager.swift in Sources */,
8714291C2B3DBFDF003FA2CB /* AppMainVC+Menu.swift in Sources */,
8740E30B2B37F969004A06C2 /* EventView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
import AppKit

public extension NSAppearance {
var isDarkMode: Bool {
switch name {
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
return true
default:
return false
}
}

func resolvedName(isDarkMode: Bool) -> NSAppearance.Name {
switch name {
case .aqua, .darkAqua:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// NSBezierPath+Extension.swift
//
// Created by cyan on 2024/1/8.
//

import AppKit

public extension NSBezierPath {
/**
Backward compatibility of `cgPath`.
*/
var toCGPath: CGPath {
if #available(macOS 14.0, *) {
return cgPath
}

let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)

for index in 0..<elementCount {
let type = element(at: index, associatedPoints: &points)
switch type {
case .moveTo: path.move(to: points[0])
case .lineTo: path.addLine(to: points[0])
case .curveTo: path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath: path.closeSubpath()
default: fatalError("Unknown element \(type)")
}
}

return path
}

/**
Backward compatibility of `NSBezierPath(cgPath:)`.
*/
static func from(cgPath: CGPath) -> NSBezierPath {
if #available(macOS 14.0, *) {
return NSBezierPath(cgPath: cgPath)
}

let path = NSBezierPath()
cgPath.applyWithBlock { (pointer: UnsafePointer<CGPathElement>) in
let element = pointer.pointee
let points = element.points

switch element.type {
case .moveToPoint:
path.move(to: points.pointee)
case .addLineToPoint:
path.line(to: points.pointee)
case .addQuadCurveToPoint:
let qp0 = path.currentPoint
let qp1 = points.pointee
let qp2 = points.successor().pointee
let m = 2.0 / 3.0

let cp1 = CGPoint(
x: qp0.x + ((qp1.x - qp0.x) * m),
y: qp0.y + ((qp1.y - qp0.y) * m)
)

let cp2 = CGPoint(
x: qp2.x + ((qp1.x - qp2.x) * m),
y: qp2.y + ((qp1.y - qp2.y) * m)
)

path.curve(to: qp2, controlPoint1: cp1, controlPoint2: cp2)
case .addCurveToPoint:
let cp1 = points.pointee
let cp2 = points.advanced(by: 1).pointee
let target = points.advanced(by: 2).pointee

path.curve(
to: points.advanced(by: 2).pointee,
controlPoint1: cp1,
controlPoint2: cp2
)
case .closeSubpath:
path.close()
@unknown default:
fatalError("Unknown type \(element.type)")
}
}

return path
}

/**
Create bezier path from text with specified font.
*/
static func from(text: String, font: NSFont) -> NSBezierPath {
let coreTextFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil)
let attributedText = NSAttributedString(string: text, attributes: [.font: coreTextFont])

let glyphRuns = CTLineGetGlyphRuns(CTLineCreateWithAttributedString(attributedText)) as? [CTRun]
let letterPaths = CGMutablePath()

glyphRuns?.forEach { run in
for index in 0..<CTRunGetGlyphCount(run) {
let range = CFRangeMake(index, 1)
var glyphs = [CGGlyph](repeating: 0, count: range.length)
var position = CGPoint()

CTRunGetGlyphs(run, range, &glyphs)
CTRunGetPositions(run, range, &position)

glyphs.compactMap { CTFontCreatePathForGlyph(coreTextFont, $0, nil) }.forEach {
let transform = CGAffineTransform(translationX: position.x, y: position.y)
letterPaths.addPath($0, transform: transform)
}
}
}

let bezierPath = NSBezierPath.from(cgPath: letterPaths)

// The path is upside down, transform the coordinate system
bezierPath.transform(using: AffineTransform(scaleByX: 1.0, byY: -1.0))
bezierPath.transform(using: AffineTransform(translationByX: 0, byY: letterPaths.boundingBox.height))

return bezierPath
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import AppKit

public extension NSImage {
var asTemplate: NSImage {
self.isTemplate = true
return self
}

static func with(
symbolName: String,
pointSize: Double,
Expand Down Expand Up @@ -38,4 +43,17 @@ public extension NSImage {

return view.snapshotImage
}

func resized(with size: CGSize) -> NSImage {
let frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
guard let representation = bestRepresentation(for: frame, context: nil, hints: nil) else {
return self
}

let image = NSImage(size: size, flipped: false) { _ in
representation.draw(in: frame)
}

return image
}
}
34 changes: 34 additions & 0 deletions LunarBarMac/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,23 @@
}
}
},
"Calendar Icon" : {
"comment" : "[Menu] Use a calendar icon as the menu bar icon",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "日历图标"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "日曆圖像"
}
}
}
},
"Calendars" : {
"comment" : "[Menu] Show or hide system calendars",
"localizations" : {
Expand Down Expand Up @@ -317,6 +334,23 @@
}
}
},
"Current Date" : {
"comment" : "[Menu] Use the current date as the menu bar icon",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "当前日期"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "當前日期"
}
}
}
},
"Customization Tips" : {
"comment" : "[Menu] View tips of customizing public holidays",
"localizations" : {
Expand Down
27 changes: 25 additions & 2 deletions LunarBarMac/Sources/Main/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
item.autosaveName = Bundle.main.bundleName
item.behavior = .terminationOnRemoval
item.button?.image = .with(symbolName: Icons.calendar, pointSize: 16)

return item
}()
Expand All @@ -25,7 +24,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Prepare public holiday data
_ = HolidayManager.default

// Attach the status item to menu bar
// Update the icon and attach it to the menu bar
updateMenuBarIcon()
statusItem.isVisible = true

// We don't rely on the button's target-action,
Expand Down Expand Up @@ -82,6 +82,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}

NotificationCenter.default.addObserver(
self,
selector: #selector(calendarDayDidChange(_:)),
name: .NSCalendarDayChanged,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(windowDidResignKey(_:)),
Expand All @@ -95,6 +102,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
openPanel()
return false
}

func updateMenuBarIcon() {
switch AppPreferences.General.menuBarIcon {
case .calendar:
statusItem.button?.image = AppIconFactory.createCalendarIcon()
case .date:
statusItem.button?.image = AppIconFactory.createDateIcon()
}
}
}

// MARK: - NSPopoverDelegate
Expand All @@ -108,6 +124,13 @@ extension AppDelegate: NSPopoverDelegate {
// MARK: - Private

private extension AppDelegate {
// periphery:ignore:parameters notification
@objc func calendarDayDidChange(_ notification: Notification) {
DispatchQueue.main.async {
self.updateMenuBarIcon()
}
}

@objc func windowDidResignKey(_ notification: Notification) {
guard (notification.object as? NSWindow)?.contentViewController is AppMainVC else {
return
Expand Down
53 changes: 39 additions & 14 deletions LunarBarMac/Sources/Main/AppMainVC+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,29 +118,54 @@ private extension AppMainVC {

var menuItemAppearance: NSMenuItem {
let menu = NSMenu()
let current = AppPreferences.General.appearance

// Dark mode preference
// Icon styles
menu.addItem({
let item = NSMenuItem(title: Localized.UI.menuTitleCalendarIcon)
item.image = .with(symbolName: Icons.calendar, pointSize: 14)
item.setOn(AppPreferences.General.menuBarIcon == .calendar)

menu.addItem(withTitle: Localized.UI.menuTitleSystem) { [weak self] in
self?.updateAppearance(.system)
}
.setOn(current == .system)
item.addAction {
AppPreferences.General.menuBarIcon = .calendar
}

menu.addItem(withTitle: Localized.UI.menuTitleLight) { [weak self] in
self?.updateAppearance(.light)
}
.setOn(current == .light)
return item
}())

menu.addItem(withTitle: Localized.UI.menuTitleDark) { [weak self] in
self?.updateAppearance(.dark)
menu.addItem({
let item = NSMenuItem(title: Localized.UI.menuTitleCurrentDate)
item.setOn(AppPreferences.General.menuBarIcon == .date)

if let image = AppIconFactory.createDateIcon() {
item.image = image.resized(with: CGSize(width: 17, height: 12))
} else {
Logger.assertFail("Failed to create the icon")
}

item.addAction {
AppPreferences.General.menuBarIcon = .date
}

return item
}())

menu.addSeparator()

// Dark mode preferences
[
(Localized.UI.menuTitleSystem, Appearance.system),
(Localized.UI.menuTitleLight, Appearance.light),
(Localized.UI.menuTitleDark, Appearance.dark),
].forEach { (title: String, appearance: Appearance) in
menu.addItem(withTitle: title) { [weak self] in
self?.updateAppearance(appearance)
}
.setOn(AppPreferences.General.appearance == appearance)
}
.setOn(current == .dark)

menu.addSeparator()

// Accessibility options

menu.addItem(withTitle: Localized.UI.menuTitleReduceMotion) { [weak self] in
AppPreferences.Accessibility.reduceMotion.toggle()
self?.popover?.animates = !AppPreferences.Accessibility.reduceMotion
Expand Down
5 changes: 4 additions & 1 deletion LunarBarMac/Sources/Main/AppMainVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ extension AppMainVC {
// MARK: - Updating

func updateAppearance(_ appearance: Appearance = AppPreferences.General.appearance) {
view.window?.appearance = appearance.resolved()
AppPreferences.General.appearance = appearance

// Override both since in some contexts we don't have a window
NSApp.appearance = appearance.resolved()
view.window?.appearance = NSApp.appearance
}

func updateCalendar(targetDate: Date = .now) {
Expand Down
Loading

0 comments on commit 4f3c12e

Please sign in to comment.