You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
19 KiB
504 lines
19 KiB
// Copyright © 2015 Abhishek Banthia |
|
|
|
import Cocoa |
|
|
|
extension Notification.Name { |
|
static let themeDidChangeNotification = Notification.Name(rawValue: "ThemeDidChangeNotification") |
|
} |
|
|
|
class Themer: NSObject { |
|
// Adding a new theme should automatically cause the compiler to complain asking to make the switches in this class to be more exhaustive |
|
enum Theme: Int { |
|
case light = 0 |
|
case dark |
|
case system |
|
case solarizedLight |
|
case solarizedDark |
|
} |
|
|
|
private static var sharedInstance = Themer(index: UserDefaults.standard.integer(forKey: UserDefaultKeys.themeKey)) |
|
private var effectiveApperanceObserver: NSKeyValueObservation? |
|
private var themeIndex: Theme { |
|
didSet { |
|
NotificationCenter.default.post(name: .themeDidChangeNotification, object: nil) |
|
} |
|
} |
|
|
|
init(index: Int) { |
|
switch index { |
|
case 0: |
|
themeIndex = Theme.light |
|
case 1: |
|
themeIndex = Theme.dark |
|
case 2: |
|
themeIndex = Theme.system |
|
case 3: |
|
themeIndex = Theme.solarizedLight |
|
case 4: |
|
themeIndex = Theme.solarizedDark |
|
default: |
|
themeIndex = Theme.light |
|
} |
|
|
|
super.init() |
|
|
|
setAppAppearance() |
|
|
|
DistributedNotificationCenter.default.addObserver(self, |
|
selector: #selector(respondToInterfaceStyle), |
|
name: .interfaceStyleDidChange, |
|
object: nil) |
|
|
|
effectiveApperanceObserver = NSApp.observe(\.effectiveAppearance) { _, _ in |
|
NotificationCenter.default.post(name: .themeDidChangeNotification, object: nil) |
|
} |
|
} |
|
} |
|
|
|
extension Themer { |
|
class func shared() -> Themer { |
|
return sharedInstance |
|
} |
|
|
|
func set(theme: Int) { |
|
if themeIndex.rawValue == theme { |
|
return |
|
} |
|
|
|
switch theme { |
|
case 0: |
|
themeIndex = Theme.light |
|
case 1: |
|
themeIndex = Theme.dark |
|
case 2: |
|
themeIndex = Theme.system |
|
case 3: |
|
themeIndex = Theme.solarizedLight |
|
case 4: |
|
themeIndex = Theme.solarizedDark |
|
default: |
|
themeIndex = Theme.light |
|
} |
|
|
|
setAppAppearance() |
|
} |
|
|
|
@objc func respondToInterfaceStyle() { |
|
OperationQueue.main.addOperation { |
|
self.setAppAppearance() |
|
} |
|
} |
|
|
|
// MARK: Color |
|
|
|
func sliderKnobColor() -> NSColor { |
|
switch themeIndex { |
|
case .light, .solarizedLight: |
|
return NSColor(deviceRed: 255.0, green: 255.0, blue: 255, alpha: 0.9) |
|
case .dark, .solarizedDark: |
|
return NSColor(deviceRed: 0.0, green: 0.0, blue: 0, alpha: 0.9) |
|
case .system: |
|
return retrieveCurrentSystem() == .light ? NSColor(deviceRed: 255.0, green: 255.0, blue: 255, alpha: 0.9) : NSColor(deviceRed: 0.0, green: 0.0, blue: 0, alpha: 0.9) |
|
} |
|
} |
|
|
|
func sliderRightColor() -> NSColor { |
|
switch themeIndex { |
|
case .dark: |
|
return NSColor.white |
|
case .system: |
|
return retrieveCurrentSystem() == .dark ? NSColor.white : NSColor.gray |
|
default: |
|
return NSColor.gray |
|
} |
|
} |
|
|
|
func mainBackgroundColor() -> NSColor { |
|
switch themeIndex { |
|
case .light: |
|
return NSColor.white |
|
case .dark: |
|
return NSColor(deviceRed: 42.0 / 255.0, green: 42.0 / 255.0, blue: 42.0 / 255.0, alpha: 1.0) |
|
case .system: |
|
return retrieveCurrentSystem() == .light ? NSColor.white : NSColor.windowBackgroundColor |
|
case .solarizedLight: |
|
return NSColor(deviceRed: 253.0 / 255.0, green: 246.0 / 255.0, blue: 227.0 / 255.0, alpha: 1.0) |
|
case .solarizedDark: |
|
return NSColor(deviceRed: 7.0 / 255.0, green: 54.0 / 255.0, blue: 66.0 / 255.0, alpha: 1.0) |
|
} |
|
} |
|
|
|
func textBackgroundColor() -> NSColor { |
|
switch themeIndex { |
|
case .light: |
|
return NSColor(deviceRed: 241.0 / 255.0, green: 241.0 / 255.0, blue: 241.0 / 255.0, alpha: 1.0) |
|
case .dark: |
|
return NSColor(deviceRed: 42.0 / 255.0, green: 55.0 / 255.0, blue: 62.0 / 255.0, alpha: 1.0) |
|
case .system: |
|
return retrieveCurrentSystem() == .light ? NSColor(deviceRed: 241.0 / 255.0, green: 241.0 / 255.0, blue: 241.0 / 255.0, alpha: 1.0) : NSColor.controlBackgroundColor |
|
case .solarizedLight: |
|
return NSColor(deviceRed: 238.0 / 255.0, green: 232.0 / 255.0, blue: 213.0 / 255.0, alpha: 1.0) |
|
case .solarizedDark: |
|
return NSColor(deviceRed: 0.0 / 255.0, green: 43.0 / 255.0, blue: 54.0 / 255.0, alpha: 1.0) |
|
} |
|
} |
|
|
|
func mainTextColor() -> NSColor { |
|
switch themeIndex { |
|
case .light: |
|
return NSColor.black |
|
case .dark: |
|
return NSColor.white |
|
case .system: |
|
return NSColor.textColor |
|
case .solarizedLight: |
|
return NSColor.black |
|
case .solarizedDark: |
|
return NSColor.white |
|
} |
|
} |
|
|
|
// MARK: Images |
|
|
|
func shutdownImage() -> NSImage { |
|
if let symbolImageForShutdown = symbolImage(for: "ellipsis.circle") { |
|
return symbolImageForShutdown |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("PowerIcon"))!, |
|
NSImage(named: NSImage.Name("PowerIcon-White"))!, |
|
NSImage(named: NSImage.Name("Power"))!, |
|
NSImage(named: NSImage.Name("PowerIcon"))!, |
|
NSImage(named: NSImage.Name("PowerIcon-White"))!) |
|
} |
|
|
|
func preferenceImage() -> NSImage { |
|
if let symbolImageForPreference = symbolImage(for: "plus") { |
|
return symbolImageForPreference |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Settings"))!, |
|
NSImage(named: NSImage.Name("Settings-White"))!, |
|
NSImage(named: NSImage.actionTemplateName)!, |
|
NSImage(named: NSImage.Name("Settings"))!, |
|
NSImage(named: NSImage.Name("Settings-White"))!) |
|
} |
|
|
|
func pinImage() -> NSImage { |
|
if let pinImage = symbolImage(for: "macwindow.on.rectangle") { |
|
return pinImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Float"))!, |
|
NSImage(named: NSImage.Name("Float-White"))!, |
|
NSImage(named: NSImage.Name("Pin"))!, |
|
NSImage(named: NSImage.Name("Float"))!, |
|
NSImage(named: NSImage.Name("Float-White"))!) |
|
} |
|
|
|
func sunriseImage() -> NSImage { |
|
if let symbolImage = symbolImage(for: "sunrise.fill") { |
|
return symbolImage |
|
} |
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Sunrise"))!, |
|
NSImage(named: NSImage.Name("WhiteSunrise"))!, |
|
NSImage(named: NSImage.Name("Sunrise Dynamic"))!, |
|
NSImage(named: NSImage.Name("Sunrise"))!, |
|
NSImage(named: NSImage.Name("WhiteSunrise"))!) |
|
} |
|
|
|
func sunsetImage() -> NSImage { |
|
if let symbolImage = symbolImage(for: "sunset.fill") { |
|
return symbolImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Sunset"))!, |
|
NSImage(named: NSImage.Name("WhiteSunset"))!, |
|
NSImage(named: NSImage.Name("Sunset Dynamic"))!, |
|
NSImage(named: NSImage.Name("Sunset"))!, |
|
NSImage(named: NSImage.Name("WhiteSunset"))!) |
|
} |
|
|
|
func remove() -> NSImage { |
|
if let symbolImage = symbolImage(for: "xmark") { |
|
return symbolImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Remove"))!, |
|
NSImage(named: NSImage.Name("WhiteRemove"))!, |
|
NSImage(named: NSImage.Name("Remove Dynamic"))!, |
|
NSImage(named: NSImage.Name("Remove"))!, |
|
NSImage(named: NSImage.Name("WhiteRemove"))!) |
|
} |
|
|
|
func removeImage() -> NSImage { |
|
if let symbolImage = symbolImage(for: "xmark.circle") { |
|
return symbolImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Remove"))!, |
|
NSImage(named: NSImage.Name("WhiteRemove"))!, |
|
NSImage(named: NSImage.Name("Remove Dynamic"))!, |
|
NSImage(named: NSImage.Name("Remove"))!, |
|
NSImage(named: NSImage.Name("WhiteRemove"))!) |
|
} |
|
|
|
func removeAlternateImage() -> NSImage { |
|
if let symbolImage = symbolImage(for: "xmark.circle.fill") { |
|
return symbolImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Remove"))!, |
|
NSImage(named: NSImage.Name("WhiteRemove"))!, |
|
NSImage(named: NSImage.Name("Remove Dynamic"))!, |
|
NSImage(named: NSImage.Name("Remove"))!, |
|
NSImage(named: NSImage.Name("WhiteRemove"))!) |
|
} |
|
|
|
func extraOptionsImage() -> NSImage { |
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Extra"))!, |
|
NSImage(named: NSImage.Name("ExtraWhite"))!, |
|
NSImage(named: NSImage.Name("Extra Dynamic"))!, |
|
NSImage(named: NSImage.Name("Extra"))!, |
|
NSImage(named: NSImage.Name("ExtraWhite"))!) |
|
} |
|
|
|
func menubarOnboardingImage() -> NSImage { |
|
switch themeIndex { |
|
case .system: |
|
return NSImage(named: NSImage.Name("Dynamic Menubar"))! |
|
case .light, .solarizedLight: |
|
return NSImage(named: NSImage.Name("Light Menubar"))! |
|
case .dark, .solarizedDark: |
|
return NSImage(named: NSImage.Name("Dark Menubar"))! |
|
} |
|
} |
|
|
|
func extraOptionsHighlightedImage() -> NSImage { |
|
return fallbackImageProvider(NSImage(named: NSImage.Name("ExtraHighlighted"))!, |
|
NSImage(named: NSImage.Name("ExtraWhiteHighlighted"))!, |
|
NSImage(named: NSImage.Name("ExtraHighlighted Dynamic"))!, |
|
NSImage(named: NSImage.Name("ExtraHighlighted"))!, |
|
NSImage(named: NSImage.Name("ExtraWhiteHighlighted"))!) |
|
} |
|
|
|
func copyImage() -> NSImage { |
|
if let copyImage = symbolImage(for: "doc.on.doc") { |
|
return copyImage |
|
} |
|
|
|
return NSImage() |
|
} |
|
|
|
func highlightedCopyImage() -> NSImage? { |
|
if let copyImage = symbolImage(for: "doc.on.doc.fill") { |
|
return copyImage |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func sharingImage() -> NSImage { |
|
if let sharingImage = symbolImage(for: "doc.on.doc") { |
|
return sharingImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Sharing"))!, |
|
NSImage(named: NSImage.Name("SharingDarkIcon"))!, |
|
NSImage(named: NSImage.Name("Sharing Dynamic"))!, |
|
NSImage(named: NSImage.Name("Sharing"))!, |
|
NSImage(named: NSImage.Name("SharingDarkIcon"))!) |
|
} |
|
|
|
func sharingImageAlternate() -> NSImage { |
|
if let sharingImage = symbolImage(for: "doc.on.doc.fill") { |
|
return sharingImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Sharing"))!, |
|
NSImage(named: NSImage.Name("SharingDarkIcon"))!, |
|
NSImage(named: NSImage.Name("Sharing Dynamic"))!, |
|
NSImage(named: NSImage.Name("Sharing"))!, |
|
NSImage(named: NSImage.Name("SharingDarkIcon"))!) |
|
} |
|
|
|
func currentLocationImage() -> NSImage { |
|
if let symbolImage = symbolImage(for: "location.fill") { |
|
return symbolImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("CurrentLocation"))!, |
|
NSImage(named: NSImage.Name("CurrentLocationWhite"))!, |
|
NSImage(named: NSImage.Name("CurrentLocationDynamic"))!, |
|
NSImage(named: NSImage.Name("CurrentLocation"))!, |
|
NSImage(named: NSImage.Name("CurrentLocationWhite"))!) |
|
} |
|
|
|
func popoverAppearance() -> NSAppearance { |
|
switch themeIndex { |
|
case .light, .solarizedLight: |
|
return NSAppearance(named: NSAppearance.Name.vibrantLight)! |
|
case .dark, .solarizedDark: |
|
return NSAppearance(named: NSAppearance.Name.vibrantDark)! |
|
case .system: |
|
return NSAppearance.current |
|
} |
|
} |
|
|
|
func addImage() -> NSImage { |
|
if let symbolImageForPreference = symbolImage(for: "plus") { |
|
return symbolImageForPreference |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Add Icon"))!, |
|
NSImage(named: NSImage.Name("Add White"))!, |
|
NSImage(named: .addDynamicIcon)!, |
|
NSImage(named: NSImage.Name("Add Icon"))!, |
|
NSImage(named: NSImage.Name("Add White"))!) |
|
} |
|
|
|
func privacyTabImage() -> NSImage { |
|
if let privacyTabSFImage = symbolImage(for: "lock") { |
|
return privacyTabSFImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Privacy"))!, |
|
NSImage(named: NSImage.Name("Privacy Dark"))!, |
|
NSImage(named: .permissionTabIcon)!, |
|
NSImage(named: NSImage.Name("Privacy"))!, |
|
NSImage(named: NSImage.Name("Privacy Dark"))!) |
|
} |
|
|
|
func appearanceTabImage() -> NSImage { |
|
if let appearanceTabImage = symbolImage(for: "eye") { |
|
return appearanceTabImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Appearance"))!, |
|
NSImage(named: NSImage.Name("Appearance Dark"))!, |
|
NSImage(named: .appearanceTabIcon)!, |
|
NSImage(named: NSImage.Name("Appearance"))!, |
|
NSImage(named: NSImage.Name("Appearance Dark"))!) |
|
} |
|
|
|
func calendarTabImage() -> NSImage { |
|
if let calendarTabImage = symbolImage(for: "calendar") { |
|
return calendarTabImage |
|
} |
|
|
|
return fallbackImageProvider(NSImage(named: NSImage.Name("Calendar Tab Icon"))!, |
|
NSImage(named: NSImage.Name("Calendar Tab Dark"))!, |
|
NSImage(named: .calendarTabIcon)!, |
|
NSImage(named: NSImage.Name("Calendar Tab Icon"))!, |
|
NSImage(named: NSImage.Name("Calendar Tab Dark"))!) |
|
} |
|
|
|
func generalTabImage() -> NSImage? { |
|
return symbolImage(for: "gearshape") |
|
} |
|
|
|
func aboutTabImage() -> NSImage? { |
|
return symbolImage(for: "info.circle") |
|
} |
|
|
|
func videoCallImage() -> NSImage? { |
|
if #available(macOS 11.0, *) { |
|
let symbolConfig = NSImage.SymbolConfiguration(pointSize: 20, weight: .regular) |
|
return symbolImage(for: "video.circle.fill")?.withSymbolConfiguration(symbolConfig) |
|
} else { |
|
return nil |
|
} |
|
} |
|
|
|
func filledTrashImage() -> NSImage? { |
|
return symbolImage(for: "trash.fill") |
|
} |
|
|
|
// Modern Slider |
|
func goBackwardsImage() -> NSImage? { |
|
return symbolImage(for: "gobackward.15") |
|
} |
|
|
|
func goForwardsImage() -> NSImage? { |
|
return symbolImage(for: "goforward.15") |
|
} |
|
|
|
func resetModernSliderImage() -> NSImage? { |
|
if let xmarkImage = symbolImage(for: "xmark.circle.fill") { |
|
return xmarkImage |
|
} |
|
|
|
return removeImage() |
|
} |
|
|
|
// MARK: Debug Description |
|
|
|
override var debugDescription: String { |
|
if themeIndex == .system { |
|
return "System Theme is \(retrieveCurrentSystem())" |
|
} |
|
return "Current Theme is \(themeIndex)" |
|
} |
|
|
|
override var description: String { |
|
return debugDescription |
|
} |
|
|
|
// MARK: Private |
|
|
|
private func symbolImage(for name: String) -> NSImage? { |
|
assert(name.isEmpty == false) |
|
|
|
if #available(OSX 11.0, *) { |
|
return NSImage(systemSymbolName: name, |
|
accessibilityDescription: name) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
private func retrieveCurrentSystem() -> Theme { |
|
if #available(OSX 10.14, *) { |
|
if let appleInterfaceStyle = UserDefaults.standard.object(forKey: UserDefaultKeys.appleInterfaceStyleKey) as? String { |
|
if appleInterfaceStyle.lowercased().contains("dark") { |
|
return .dark |
|
} |
|
} |
|
} |
|
return .light |
|
} |
|
|
|
private func setAppAppearance() { |
|
var appAppearance = NSAppearance(named: .aqua) |
|
|
|
if themeIndex == .dark || themeIndex == .solarizedDark { |
|
appAppearance = NSAppearance(named: .darkAqua) |
|
} else if themeIndex == .system { |
|
appAppearance = retrieveCurrentSystem() == .dark ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua) |
|
} |
|
if NSApp.appearance != appAppearance { |
|
NSApp.appearance = appAppearance |
|
} |
|
|
|
} |
|
|
|
private func fallbackImageProvider(_ lightImage: NSImage, |
|
_ darkImage: NSImage, |
|
_ systemImage: NSImage, |
|
_ solarizedLightImage: NSImage, |
|
_ solarizedDarkImage: NSImage) -> NSImage |
|
{ |
|
switch themeIndex { |
|
case .light: |
|
return lightImage |
|
case .dark: |
|
return darkImage |
|
case .system: |
|
return systemImage |
|
case .solarizedLight: |
|
return solarizedLightImage |
|
case .solarizedDark: |
|
return solarizedDarkImage |
|
} |
|
} |
|
}
|
|
|