Browse Source

Remove 10.14 checks.

master
Abhishek Banthia 9 months ago
parent
commit
511dd23c4a
  1. 8
      Clocker/Overall App/AppKit + Additions.swift
  2. 7
      Clocker/Overall App/Foundation + Additions.swift
  3. 297
      Clocker/Overall App/Themer.swift
  4. 222
      Clocker/Panel/PanelController.swift
  5. 40
      Clocker/Panel/ParentPanelController+ModernSlider.swift
  6. 500
      Clocker/Panel/ParentPanelController.swift
  7. 40
      Clocker/Panel/Rate Controller/ReviewController.swift
  8. 114
      Clocker/Panel/UI/TimezoneDataSource.swift
  9. 4
      Clocker/Preferences/Appearance/AppearanceViewController.swift
  10. 110
      Clocker/Preferences/Calendar/CalendarViewController.swift
  11. 504
      Clocker/Preferences/General/PreferencesViewController.swift
  12. 178
      Clocker/Preferences/Menu Bar/StatusItemHandler.swift
  13. 51
      Clocker/Preferences/Menu Bar/StatusItemView.swift

8
Clocker/Overall App/AppKit + Additions.swift

@ -6,11 +6,9 @@ extension NSTextField {
isEditable = false isEditable = false
isBordered = false isBordered = false
allowsDefaultTighteningForTruncation = true allowsDefaultTighteningForTruncation = true
if #available(OSX 10.12.2, *) { isAutomaticTextCompletionEnabled = false
isAutomaticTextCompletionEnabled = false allowsCharacterPickerTouchBarItem = false
allowsCharacterPickerTouchBarItem = false
}
} }
func disableWrapping() { func disableWrapping() {

7
Clocker/Overall App/Foundation + Additions.swift

@ -59,11 +59,6 @@ public extension Data {
extension NSKeyedArchiver { extension NSKeyedArchiver {
static func clocker_archive(with object: Any) -> Data? { static func clocker_archive(with object: Any) -> Data? {
return try? NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true)
if #available(macOS 10.14, *) {
return try? NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true)
}
return nil
} }
} }

297
Clocker/Overall App/Themer.swift

@ -15,7 +15,7 @@ class Themer: NSObject {
case solarizedLight case solarizedLight
case solarizedDark case solarizedDark
} }
private static var sharedInstance = Themer(index: UserDefaults.standard.integer(forKey: UserDefaultKeys.themeKey)) private static var sharedInstance = Themer(index: UserDefaults.standard.integer(forKey: UserDefaultKeys.themeKey))
private var effectiveApperanceObserver: NSKeyValueObservation? private var effectiveApperanceObserver: NSKeyValueObservation?
private var themeIndex: Theme { private var themeIndex: Theme {
@ -23,7 +23,7 @@ class Themer: NSObject {
NotificationCenter.default.post(name: .themeDidChangeNotification, object: nil) NotificationCenter.default.post(name: .themeDidChangeNotification, object: nil)
} }
} }
init(index: Int) { init(index: Int) {
switch index { switch index {
case 0: case 0:
@ -39,20 +39,18 @@ class Themer: NSObject {
default: default:
themeIndex = Theme.light themeIndex = Theme.light
} }
super.init() super.init()
setAppAppearance() setAppAppearance()
DistributedNotificationCenter.default.addObserver(self, DistributedNotificationCenter.default.addObserver(self,
selector: #selector(respondToInterfaceStyle), selector: #selector(respondToInterfaceStyle),
name: .interfaceStyleDidChange, name: .interfaceStyleDidChange,
object: nil) object: nil)
if #available(macOS 10.14, *) { effectiveApperanceObserver = NSApp.observe(\.effectiveAppearance) { _, _ in
effectiveApperanceObserver = NSApp.observe(\.effectiveAppearance) { _, _ in NotificationCenter.default.post(name: .themeDidChangeNotification, object: nil)
NotificationCenter.default.post(name: .themeDidChangeNotification, object: nil)
}
} }
} }
} }
@ -61,12 +59,12 @@ extension Themer {
class func shared() -> Themer { class func shared() -> Themer {
return sharedInstance return sharedInstance
} }
func set(theme: Int) { func set(theme: Int) {
if themeIndex.rawValue == theme { if themeIndex.rawValue == theme {
return return
} }
switch theme { switch theme {
case 0: case 0:
themeIndex = Theme.light themeIndex = Theme.light
@ -81,18 +79,18 @@ extension Themer {
default: default:
themeIndex = Theme.light themeIndex = Theme.light
} }
setAppAppearance() setAppAppearance()
} }
@objc func respondToInterfaceStyle() { @objc func respondToInterfaceStyle() {
OperationQueue.main.addOperation { OperationQueue.main.addOperation {
self.setAppAppearance() self.setAppAppearance()
} }
} }
// MARK: Color // MARK: Color
func sliderKnobColor() -> NSColor { func sliderKnobColor() -> NSColor {
switch themeIndex { switch themeIndex {
case .light, .solarizedLight: case .light, .solarizedLight:
@ -103,7 +101,7 @@ extension Themer {
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) 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 { func sliderRightColor() -> NSColor {
switch themeIndex { switch themeIndex {
case .dark: case .dark:
@ -114,104 +112,90 @@ extension Themer {
return NSColor.gray return NSColor.gray
} }
} }
func mainBackgroundColor() -> NSColor { func mainBackgroundColor() -> NSColor {
if #available(macOS 10.14, *) { switch themeIndex {
switch themeIndex { case .light:
case .light: return NSColor.white
return NSColor.white case .dark:
case .dark: return NSColor(deviceRed: 42.0 / 255.0, green: 42.0 / 255.0, blue: 42.0 / 255.0, alpha: 1.0)
return NSColor(deviceRed: 42.0 / 255.0, green: 42.0 / 255.0, blue: 42.0 / 255.0, alpha: 1.0) case .system:
case .system: return retrieveCurrentSystem() == .light ? NSColor.white : NSColor.windowBackgroundColor
return retrieveCurrentSystem() == .light ? NSColor.white : NSColor.windowBackgroundColor case .solarizedLight:
case .solarizedLight: return NSColor(deviceRed: 253.0 / 255.0, green: 246.0 / 255.0, blue: 227.0 / 255.0, alpha: 1.0)
return NSColor(deviceRed: 253.0 / 255.0, green: 246.0 / 255.0, blue: 227.0 / 255.0, alpha: 1.0) case .solarizedDark:
case .solarizedDark: return NSColor(deviceRed: 7.0 / 255.0, green: 54.0 / 255.0, blue: 66.0 / 255.0, alpha: 1.0)
return NSColor(deviceRed: 7.0 / 255.0, green: 54.0 / 255.0, blue: 66.0 / 255.0, alpha: 1.0)
}
} }
return themeIndex == .light ? NSColor.white : NSColor(deviceRed: 55.0 / 255.0, green: 71.0 / 255.0, blue: 79.0 / 255.0, alpha: 1.0)
} }
func textBackgroundColor() -> NSColor { func textBackgroundColor() -> NSColor {
if #available(macOS 10.14, *) { switch themeIndex {
switch themeIndex { case .light:
case .light: return NSColor(deviceRed: 241.0 / 255.0, green: 241.0 / 255.0, blue: 241.0 / 255.0, alpha: 1.0)
return NSColor(deviceRed: 241.0 / 255.0, green: 241.0 / 255.0, blue: 241.0 / 255.0, alpha: 1.0) case .dark:
case .dark: return NSColor(deviceRed: 42.0 / 255.0, green: 55.0 / 255.0, blue: 62.0 / 255.0, alpha: 1.0)
return NSColor(deviceRed: 42.0 / 255.0, green: 55.0 / 255.0, blue: 62.0 / 255.0, alpha: 1.0) case .system:
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
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:
case .solarizedLight: return NSColor(deviceRed: 238.0 / 255.0, green: 232.0 / 255.0, blue: 213.0 / 255.0, alpha: 1.0)
return NSColor(deviceRed: 238.0 / 255.0, green: 232.0 / 255.0, blue: 213.0 / 255.0, alpha: 1.0) case .solarizedDark:
case .solarizedDark: return NSColor(deviceRed: 0.0 / 255.0, green: 43.0 / 255.0, blue: 54.0 / 255.0, alpha: 1.0)
return NSColor(deviceRed: 0.0 / 255.0, green: 43.0 / 255.0, blue: 54.0 / 255.0, alpha: 1.0)
}
} }
return themeIndex == .light ?
NSColor(deviceRed: 241.0 / 255.0, green: 241.0 / 255.0, blue: 241.0 / 255.0, alpha: 1.0) :
NSColor(deviceRed: 42.0 / 255.0, green: 55.0 / 255.0, blue: 62.0 / 255.0, alpha: 1.0)
} }
func mainTextColor() -> NSColor { func mainTextColor() -> NSColor {
if #available(macOS 10.14, *) { switch themeIndex {
switch themeIndex { case .light:
case .light: return NSColor.black
return NSColor.black case .dark:
case .dark: return NSColor.white
return NSColor.white case .system:
case .system: return NSColor.textColor
return NSColor.textColor case .solarizedLight:
case .solarizedLight: return NSColor.black
return NSColor.black case .solarizedDark:
case .solarizedDark: return NSColor.white
return NSColor.white
}
} }
return themeIndex == .light ? NSColor.black : NSColor.white
} }
// MARK: Images // MARK: Images
func shutdownImage() -> NSImage { func shutdownImage() -> NSImage {
if let symbolImageForShutdown = symbolImage(for: "ellipsis.circle") { if let symbolImageForShutdown = symbolImage(for: "ellipsis.circle") {
return symbolImageForShutdown return symbolImageForShutdown
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("PowerIcon"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("PowerIcon"))!,
NSImage(named: NSImage.Name("PowerIcon-White"))!, NSImage(named: NSImage.Name("PowerIcon-White"))!,
NSImage(named: NSImage.Name("Power"))!, NSImage(named: NSImage.Name("Power"))!,
NSImage(named: NSImage.Name("PowerIcon"))!, NSImage(named: NSImage.Name("PowerIcon"))!,
NSImage(named: NSImage.Name("PowerIcon-White"))!) NSImage(named: NSImage.Name("PowerIcon-White"))!)
} }
func preferenceImage() -> NSImage { func preferenceImage() -> NSImage {
if let symbolImageForPreference = symbolImage(for: "plus") { if let symbolImageForPreference = symbolImage(for: "plus") {
return symbolImageForPreference return symbolImageForPreference
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Settings"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Settings"))!,
NSImage(named: NSImage.Name("Settings-White"))!, NSImage(named: NSImage.Name("Settings-White"))!,
NSImage(named: NSImage.actionTemplateName)!, NSImage(named: NSImage.actionTemplateName)!,
NSImage(named: NSImage.Name("Settings"))!, NSImage(named: NSImage.Name("Settings"))!,
NSImage(named: NSImage.Name("Settings-White"))!) NSImage(named: NSImage.Name("Settings-White"))!)
} }
func pinImage() -> NSImage { func pinImage() -> NSImage {
if let pinImage = symbolImage(for: "macwindow.on.rectangle") { if let pinImage = symbolImage(for: "macwindow.on.rectangle") {
return pinImage return pinImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Float"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Float"))!,
NSImage(named: NSImage.Name("Float-White"))!, NSImage(named: NSImage.Name("Float-White"))!,
NSImage(named: NSImage.Name("Pin"))!, NSImage(named: NSImage.Name("Pin"))!,
NSImage(named: NSImage.Name("Float"))!, NSImage(named: NSImage.Name("Float"))!,
NSImage(named: NSImage.Name("Float-White"))!) NSImage(named: NSImage.Name("Float-White"))!)
} }
func sunriseImage() -> NSImage { func sunriseImage() -> NSImage {
if let symbolImage = symbolImage(for: "sunrise.fill") { if let symbolImage = symbolImage(for: "sunrise.fill") {
return symbolImage return symbolImage
@ -222,31 +206,31 @@ extension Themer {
NSImage(named: NSImage.Name("Sunrise"))!, NSImage(named: NSImage.Name("Sunrise"))!,
NSImage(named: NSImage.Name("WhiteSunrise"))!) NSImage(named: NSImage.Name("WhiteSunrise"))!)
} }
func sunsetImage() -> NSImage { func sunsetImage() -> NSImage {
if let symbolImage = symbolImage(for: "sunset.fill") { if let symbolImage = symbolImage(for: "sunset.fill") {
return symbolImage return symbolImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Sunset"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Sunset"))!,
NSImage(named: NSImage.Name("WhiteSunset"))!, NSImage(named: NSImage.Name("WhiteSunset"))!,
NSImage(named: NSImage.Name("Sunset Dynamic"))!, NSImage(named: NSImage.Name("Sunset Dynamic"))!,
NSImage(named: NSImage.Name("Sunset"))!, NSImage(named: NSImage.Name("Sunset"))!,
NSImage(named: NSImage.Name("WhiteSunset"))!) NSImage(named: NSImage.Name("WhiteSunset"))!)
} }
func removeImage() -> NSImage { func removeImage() -> NSImage {
if let symbolImage = symbolImage(for: "xmark") { if let symbolImage = symbolImage(for: "xmark") {
return symbolImage return symbolImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Remove"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Remove"))!,
NSImage(named: NSImage.Name("WhiteRemove"))!, NSImage(named: NSImage.Name("WhiteRemove"))!,
NSImage(named: NSImage.Name("Remove Dynamic"))!, NSImage(named: NSImage.Name("Remove Dynamic"))!,
NSImage(named: NSImage.Name("Remove"))!, NSImage(named: NSImage.Name("Remove"))!,
NSImage(named: NSImage.Name("WhiteRemove"))!) NSImage(named: NSImage.Name("WhiteRemove"))!)
} }
func extraOptionsImage() -> NSImage { func extraOptionsImage() -> NSImage {
return fallbackImageProvider(NSImage(named: NSImage.Name("Extra"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Extra"))!,
NSImage(named: NSImage.Name("ExtraWhite"))!, NSImage(named: NSImage.Name("ExtraWhite"))!,
@ -254,22 +238,18 @@ extension Themer {
NSImage(named: NSImage.Name("Extra"))!, NSImage(named: NSImage.Name("Extra"))!,
NSImage(named: NSImage.Name("ExtraWhite"))!) NSImage(named: NSImage.Name("ExtraWhite"))!)
} }
func menubarOnboardingImage() -> NSImage { func menubarOnboardingImage() -> NSImage {
if #available(macOS 10.14, *) { switch themeIndex {
switch themeIndex { case .system:
case .system: return NSImage(named: NSImage.Name("Dynamic Menubar"))!
return NSImage(named: NSImage.Name("Dynamic Menubar"))! case .light, .solarizedLight:
case .light, .solarizedLight: return NSImage(named: NSImage.Name("Light Menubar"))!
return NSImage(named: NSImage.Name("Light Menubar"))! case .dark, .solarizedDark:
case .dark, .solarizedDark: return NSImage(named: NSImage.Name("Dark Menubar"))!
return NSImage(named: NSImage.Name("Dark Menubar"))!
}
} }
return retrieveCurrentSystem() == .dark ? NSImage(named: NSImage.Name("Dark Menubar"))! : NSImage(named: NSImage.Name("Light Menubar"))!
} }
func extraOptionsHighlightedImage() -> NSImage { func extraOptionsHighlightedImage() -> NSImage {
return fallbackImageProvider(NSImage(named: NSImage.Name("ExtraHighlighted"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("ExtraHighlighted"))!,
NSImage(named: NSImage.Name("ExtraWhiteHighlighted"))!, NSImage(named: NSImage.Name("ExtraWhiteHighlighted"))!,
@ -282,7 +262,7 @@ extension Themer {
if let copyImage = symbolImage(for: "doc.on.doc") { if let copyImage = symbolImage(for: "doc.on.doc") {
return copyImage return copyImage
} }
return NSImage() return NSImage()
} }
@ -290,105 +270,101 @@ extension Themer {
if let copyImage = symbolImage(for: "doc.on.doc.fill") { if let copyImage = symbolImage(for: "doc.on.doc.fill") {
return copyImage return copyImage
} }
return nil return nil
} }
func sharingImage() -> NSImage { func sharingImage() -> NSImage {
if let sharingImage = symbolImage(for: "square.and.arrow.up.on.square.fill") { if let sharingImage = symbolImage(for: "square.and.arrow.up.on.square.fill") {
return sharingImage return sharingImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Sharing"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Sharing"))!,
NSImage(named: NSImage.Name("SharingDarkIcon"))!, NSImage(named: NSImage.Name("SharingDarkIcon"))!,
NSImage(named: NSImage.Name("Sharing Dynamic"))!, NSImage(named: NSImage.Name("Sharing Dynamic"))!,
NSImage(named: NSImage.Name("Sharing"))!, NSImage(named: NSImage.Name("Sharing"))!,
NSImage(named: NSImage.Name("SharingDarkIcon"))!) NSImage(named: NSImage.Name("SharingDarkIcon"))!)
} }
func currentLocationImage() -> NSImage { func currentLocationImage() -> NSImage {
if let symbolImage = symbolImage(for: "location.fill") { if let symbolImage = symbolImage(for: "location.fill") {
return symbolImage return symbolImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("CurrentLocation"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("CurrentLocation"))!,
NSImage(named: NSImage.Name("CurrentLocationWhite"))!, NSImage(named: NSImage.Name("CurrentLocationWhite"))!,
NSImage(named: NSImage.Name("CurrentLocationDynamic"))!, NSImage(named: NSImage.Name("CurrentLocationDynamic"))!,
NSImage(named: NSImage.Name("CurrentLocation"))!, NSImage(named: NSImage.Name("CurrentLocation"))!,
NSImage(named: NSImage.Name("CurrentLocationWhite"))!) NSImage(named: NSImage.Name("CurrentLocationWhite"))!)
} }
func popoverAppearance() -> NSAppearance { func popoverAppearance() -> NSAppearance {
if #available(macOS 10.14, *) { switch themeIndex {
switch themeIndex { case .light, .solarizedLight:
case .light, .solarizedLight: return NSAppearance(named: NSAppearance.Name.vibrantLight)!
return NSAppearance(named: NSAppearance.Name.vibrantLight)! case .dark, .solarizedDark:
case .dark, .solarizedDark: return NSAppearance(named: NSAppearance.Name.vibrantDark)!
return NSAppearance(named: NSAppearance.Name.vibrantDark)! case .system:
case .system: return NSAppearance.current
return NSAppearance.current
}
} }
return themeIndex == .light ? NSAppearance(named: NSAppearance.Name.vibrantLight)! : NSAppearance(named: NSAppearance.Name.vibrantDark)!
} }
func addImage() -> NSImage { func addImage() -> NSImage {
if let symbolImageForPreference = symbolImage(for: "plus") { if let symbolImageForPreference = symbolImage(for: "plus") {
return symbolImageForPreference return symbolImageForPreference
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Add Icon"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Add Icon"))!,
NSImage(named: NSImage.Name("Add White"))!, NSImage(named: NSImage.Name("Add White"))!,
NSImage(named: .addDynamicIcon)!, NSImage(named: .addDynamicIcon)!,
NSImage(named: NSImage.Name("Add Icon"))!, NSImage(named: NSImage.Name("Add Icon"))!,
NSImage(named: NSImage.Name("Add White"))!) NSImage(named: NSImage.Name("Add White"))!)
} }
func privacyTabImage() -> NSImage { func privacyTabImage() -> NSImage {
if let privacyTabSFImage = symbolImage(for: "lock") { if let privacyTabSFImage = symbolImage(for: "lock") {
return privacyTabSFImage return privacyTabSFImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Privacy"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Privacy"))!,
NSImage(named: NSImage.Name("Privacy Dark"))!, NSImage(named: NSImage.Name("Privacy Dark"))!,
NSImage(named: .permissionTabIcon)!, NSImage(named: .permissionTabIcon)!,
NSImage(named: NSImage.Name("Privacy"))!, NSImage(named: NSImage.Name("Privacy"))!,
NSImage(named: NSImage.Name("Privacy Dark"))!) NSImage(named: NSImage.Name("Privacy Dark"))!)
} }
func appearanceTabImage() -> NSImage { func appearanceTabImage() -> NSImage {
if let appearanceTabImage = symbolImage(for: "eye") { if let appearanceTabImage = symbolImage(for: "eye") {
return appearanceTabImage return appearanceTabImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Appearance"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Appearance"))!,
NSImage(named: NSImage.Name("Appearance Dark"))!, NSImage(named: NSImage.Name("Appearance Dark"))!,
NSImage(named: .appearanceTabIcon)!, NSImage(named: .appearanceTabIcon)!,
NSImage(named: NSImage.Name("Appearance"))!, NSImage(named: NSImage.Name("Appearance"))!,
NSImage(named: NSImage.Name("Appearance Dark"))!) NSImage(named: NSImage.Name("Appearance Dark"))!)
} }
func calendarTabImage() -> NSImage { func calendarTabImage() -> NSImage {
if let calendarTabImage = symbolImage(for: "calendar") { if let calendarTabImage = symbolImage(for: "calendar") {
return calendarTabImage return calendarTabImage
} }
return fallbackImageProvider(NSImage(named: NSImage.Name("Calendar Tab Icon"))!, return fallbackImageProvider(NSImage(named: NSImage.Name("Calendar Tab Icon"))!,
NSImage(named: NSImage.Name("Calendar Tab Dark"))!, NSImage(named: NSImage.Name("Calendar Tab Dark"))!,
NSImage(named: .calendarTabIcon)!, NSImage(named: .calendarTabIcon)!,
NSImage(named: NSImage.Name("Calendar Tab Icon"))!, NSImage(named: NSImage.Name("Calendar Tab Icon"))!,
NSImage(named: NSImage.Name("Calendar Tab Dark"))!) NSImage(named: NSImage.Name("Calendar Tab Dark"))!)
} }
func generalTabImage() -> NSImage? { func generalTabImage() -> NSImage? {
return symbolImage(for: "gearshape") return symbolImage(for: "gearshape")
} }
func aboutTabImage() -> NSImage? { func aboutTabImage() -> NSImage? {
return symbolImage(for: "info.circle") return symbolImage(for: "info.circle")
} }
func videoCallImage() -> NSImage? { func videoCallImage() -> NSImage? {
if #available(macOS 11.0, *) { if #available(macOS 11.0, *) {
let symbolConfig = NSImage.SymbolConfiguration(pointSize: 20, weight: .regular) let symbolConfig = NSImage.SymbolConfiguration(pointSize: 20, weight: .regular)
@ -397,54 +373,54 @@ extension Themer {
return nil return nil
} }
} }
func filledTrashImage() -> NSImage? { func filledTrashImage() -> NSImage? {
return symbolImage(for: "trash.fill") return symbolImage(for: "trash.fill")
} }
// Modern Slider // Modern Slider
func goBackwardsImage() -> NSImage? { func goBackwardsImage() -> NSImage? {
return symbolImage(for: "gobackward.15") return symbolImage(for: "gobackward.15")
} }
func goForwardsImage() -> NSImage? { func goForwardsImage() -> NSImage? {
return symbolImage(for: "goforward.15") return symbolImage(for: "goforward.15")
} }
func resetModernSliderImage() -> NSImage? { func resetModernSliderImage() -> NSImage? {
if let xmarkImage = symbolImage(for: "xmark.circle.fill") { if let xmarkImage = symbolImage(for: "xmark.circle.fill") {
return xmarkImage return xmarkImage
} }
return removeImage() return removeImage()
} }
// MARK: Debug Description // MARK: Debug Description
override var debugDescription: String { override var debugDescription: String {
if themeIndex == .system { if themeIndex == .system {
return "System Theme is \(retrieveCurrentSystem())" return "System Theme is \(retrieveCurrentSystem())"
} }
return "Current Theme is \(themeIndex)" return "Current Theme is \(themeIndex)"
} }
override var description: String { override var description: String {
return debugDescription return debugDescription
} }
// MARK: Private // MARK: Private
private func symbolImage(for name: String) -> NSImage? { private func symbolImage(for name: String) -> NSImage? {
assert(name.isEmpty == false) assert(name.isEmpty == false)
if #available(OSX 11.0, *) { if #available(OSX 11.0, *) {
return NSImage(systemSymbolName: name, return NSImage(systemSymbolName: name,
accessibilityDescription: name) accessibilityDescription: name)
} }
return nil return nil
} }
private func retrieveCurrentSystem() -> Theme { private func retrieveCurrentSystem() -> Theme {
if #available(OSX 10.14, *) { if #available(OSX 10.14, *) {
if let appleInterfaceStyle = UserDefaults.standard.object(forKey: UserDefaultKeys.appleInterfaceStyleKey) as? String { if let appleInterfaceStyle = UserDefaults.standard.object(forKey: UserDefaultKeys.appleInterfaceStyleKey) as? String {
@ -455,43 +431,38 @@ extension Themer {
} }
return .light return .light
} }
private func setAppAppearance() { private func setAppAppearance() {
if #available(OSX 10.14, *) { var appAppearance = NSAppearance(named: .aqua)
var appAppearance = NSAppearance(named: .aqua)
if themeIndex == .dark || themeIndex == .solarizedDark {
if themeIndex == .dark || themeIndex == .solarizedDark { appAppearance = NSAppearance(named: .darkAqua)
appAppearance = NSAppearance(named: .darkAqua) } else if themeIndex == .system {
} else if themeIndex == .system { appAppearance = retrieveCurrentSystem() == .dark ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
appAppearance = retrieveCurrentSystem() == .dark ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua) }
} if NSApp.appearance != appAppearance {
if NSApp.appearance != appAppearance { NSApp.appearance = appAppearance
NSApp.appearance = appAppearance
}
} }
} }
private func fallbackImageProvider(_ lightImage: NSImage, private func fallbackImageProvider(_ lightImage: NSImage,
_ darkImage: NSImage, _ darkImage: NSImage,
_ systemImage: NSImage, _ systemImage: NSImage,
_ solarizedLightImage: NSImage, _ solarizedLightImage: NSImage,
_ solarizedDarkImage: NSImage) -> NSImage _ solarizedDarkImage: NSImage) -> NSImage
{ {
if #available(macOS 10.14, *) { switch themeIndex {
switch themeIndex { case .light:
case .light: return lightImage
return lightImage case .dark:
case .dark: return darkImage
return darkImage case .system:
case .system: return systemImage
return systemImage case .solarizedLight:
case .solarizedLight: return solarizedLightImage
return solarizedLightImage case .solarizedDark:
case .solarizedDark: return solarizedDarkImage
return solarizedDarkImage
}
} }
return themeIndex == .light ? lightImage : darkImage
} }
} }

222
Clocker/Panel/PanelController.swift

@ -5,47 +5,45 @@ import CoreLoggerKit
class PanelController: ParentPanelController { class PanelController: ParentPanelController {
@objc dynamic var hasActivePanel: Bool = false @objc dynamic var hasActivePanel: Bool = false
@IBOutlet var backgroundView: BackgroundPanelView! @IBOutlet var backgroundView: BackgroundPanelView!
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad() super.windowDidLoad()
} }
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
enablePerformanceLoggingIfNeccessary() enablePerformanceLoggingIfNeccessary()
window?.title = "Clocker Panel" window?.title = "Clocker Panel"
window?.setAccessibilityIdentifier("Clocker Panel") window?.setAccessibilityIdentifier("Clocker Panel")
// Otherwise, the panel can be dragged around while we try to scroll through the modern slider // Otherwise, the panel can be dragged around while we try to scroll through the modern slider
window?.isMovableByWindowBackground = false window?.isMovableByWindowBackground = false
futureSlider.isContinuous = true futureSlider.isContinuous = true
if let panel = window { if let panel = window {
panel.acceptsMouseMovedEvents = true panel.acceptsMouseMovedEvents = true
panel.level = .popUpMenu panel.level = .popUpMenu
panel.isOpaque = false panel.isOpaque = false
panel.backgroundColor = NSColor.clear panel.backgroundColor = NSColor.clear
} }
mainTableView.registerForDraggedTypes([.dragSession]) mainTableView.registerForDraggedTypes([.dragSession])
super.updatePanelColor() super.updatePanelColor()
super.updateDefaultPreferences() super.updateDefaultPreferences()
} }
private func enablePerformanceLoggingIfNeccessary() { private func enablePerformanceLoggingIfNeccessary() {
if !ProcessInfo.processInfo.environment.keys.contains("ENABLE_PERF_LOGGING") { if !ProcessInfo.processInfo.environment.keys.contains("ENABLE_PERF_LOGGING") {
if #available(OSX 10.14, *) { PerfLogger.disable()
PerfLogger.disable()
}
} }
} }
func setFrameTheNewWay(_ rect: NSRect, _ maxX: CGFloat) { func setFrameTheNewWay(_ rect: NSRect, _ maxX: CGFloat) {
// Calculate window's top left point. // Calculate window's top left point.
// First, center window under status item. // First, center window under status item.
@ -53,33 +51,31 @@ class PanelController: ParentPanelController {
var xPoint = CGFloat(roundf(Float(rect.midX - width / 2))) var xPoint = CGFloat(roundf(Float(rect.midX - width / 2)))
let yPoint = CGFloat(rect.minY - 2) let yPoint = CGFloat(rect.minY - 2)
let kMinimumSpaceBetweenWindowAndScreenEdge: CGFloat = 10 let kMinimumSpaceBetweenWindowAndScreenEdge: CGFloat = 10
if xPoint + width + kMinimumSpaceBetweenWindowAndScreenEdge > maxX { if xPoint + width + kMinimumSpaceBetweenWindowAndScreenEdge > maxX {
xPoint = maxX - width - kMinimumSpaceBetweenWindowAndScreenEdge xPoint = maxX - width - kMinimumSpaceBetweenWindowAndScreenEdge
} }
window?.setFrameTopLeftPoint(NSPoint(x: xPoint, y: yPoint)) window?.setFrameTopLeftPoint(NSPoint(x: xPoint, y: yPoint))
window?.invalidateShadow() window?.invalidateShadow()
} }
func open() { func open() {
if #available(OSX 10.14, *) { PerfLogger.startMarker("Open")
PerfLogger.startMarker("Open")
}
guard isWindowLoaded == true else { guard isWindowLoaded == true else {
return return
} }
super.dismissRowActions() super.dismissRowActions()
updateDefaultPreferences() updateDefaultPreferences()
setupUpcomingEventViewCollectionViewIfNeccesary() setupUpcomingEventViewCollectionViewIfNeccesary()
//TODO: Always hide the legacy slider. Delete this once v24.01 stabilizes. //TODO: Always hide the legacy slider. Delete this once v24.01 stabilizes.
futureSliderView.isHidden = true futureSliderView.isHidden = true
if DataStore.shared().timezones().isEmpty || DataStore.shared().shouldDisplay(.futureSlider) == false { if DataStore.shared().timezones().isEmpty || DataStore.shared().shouldDisplay(.futureSlider) == false {
modernContainerView.isHidden = true modernContainerView.isHidden = true
} else if let value = DataStore.shared().retrieve(key: UserDefaultKeys.displayFutureSliderKey) as? NSNumber, modernContainerView != nil { } else if let value = DataStore.shared().retrieve(key: UserDefaultKeys.displayFutureSliderKey) as? NSNumber, modernContainerView != nil {
@ -89,14 +85,14 @@ class PanelController: ParentPanelController {
modernContainerView.isHidden = false modernContainerView.isHidden = false
} }
} }
// Reset future slider value to zero // Reset future slider value to zero
futureSlider.integerValue = 0 futureSlider.integerValue = 0
sliderDatePicker.dateValue = Date() sliderDatePicker.dateValue = Date()
closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation() closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation()
modernSliderLabel.stringValue = "Time Scroller" modernSliderLabel.stringValue = "Time Scroller"
resetModernSliderButton.isHidden = true resetModernSliderButton.isHidden = true
if modernSlider != nil { if modernSlider != nil {
let indexPaths: Set<IndexPath> = Set([IndexPath(item: modernSlider.numberOfItems(inSection: 0) / 2, section: 0)]) let indexPaths: Set<IndexPath> = Set([IndexPath(item: modernSlider.numberOfItems(inSection: 0) / 2, section: 0)])
modernSlider.scrollToItems(at: indexPaths, scrollPosition: .centeredHorizontally) modernSlider.scrollToItems(at: indexPaths, scrollPosition: .centeredHorizontally)
@ -104,54 +100,50 @@ class PanelController: ParentPanelController {
goForwardButton.alphaValue = 0 goForwardButton.alphaValue = 0
goBackwardsButton.alphaValue = 0 goBackwardsButton.alphaValue = 0
setTimezoneDatasourceSlider(sliderValue: 0) setTimezoneDatasourceSlider(sliderValue: 0)
reviewView.isHidden = !ReviewController.canPrompt() reviewView.isHidden = !ReviewController.canPrompt()
reviewView.layer?.backgroundColor = NSColor.clear.cgColor reviewView.layer?.backgroundColor = NSColor.clear.cgColor
setPanelFrame() setPanelFrame()
startWindowTimer() startWindowTimer()
if DataStore.shared().shouldDisplay(ViewType.upcomingEventView) { if DataStore.shared().shouldDisplay(ViewType.upcomingEventView) {
retrieveCalendarEvents() retrieveCalendarEvents()
} else { } else {
removeUpcomingEventView() removeUpcomingEventView()
super.setScrollViewConstraint() super.setScrollViewConstraint()
} }
// This is done to make the UI look updated. // This is done to make the UI look updated.
mainTableView.reloadData() mainTableView.reloadData()
log() log()
if #available(OSX 10.14, *) { PerfLogger.endMarker("Open")
PerfLogger.endMarker("Open")
}
} }
// New way to set the panel's frame. // New way to set the panel's frame.
// This takes into account the screen's dimensions. // This takes into account the screen's dimensions.
private func setPanelFrame() { private func setPanelFrame() {
if #available(OSX 10.14, *) { PerfLogger.startMarker("Set Panel Frame")
PerfLogger.startMarker("Set Panel Frame")
}
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else {
return return
} }
var statusBackgroundWindow = appDelegate.statusItemForPanel().statusItem.button?.window var statusBackgroundWindow = appDelegate.statusItemForPanel().statusItem.button?.window
var statusView = appDelegate.statusItemForPanel().statusItem.button var statusView = appDelegate.statusItemForPanel().statusItem.button
// This below is a better way than actually checking if the menubar compact mode is set. // This below is a better way than actually checking if the menubar compact mode is set.
if statusBackgroundWindow == nil || statusView == nil { if statusBackgroundWindow == nil || statusView == nil {
statusBackgroundWindow = appDelegate.statusItemForPanel().statusItem.button?.window statusBackgroundWindow = appDelegate.statusItemForPanel().statusItem.button?.window
statusView = appDelegate.statusItemForPanel().statusItem.button statusView = appDelegate.statusItemForPanel().statusItem.button
} }
if let statusWindow = statusBackgroundWindow, if let statusWindow = statusBackgroundWindow,
let statusButton = statusView let statusButton = statusView
{ {
@ -159,32 +151,28 @@ class PanelController: ParentPanelController {
var statusItemScreen = NSScreen.main var statusItemScreen = NSScreen.main
var testPoint = statusItemFrame.origin var testPoint = statusItemFrame.origin
testPoint.y -= 100 testPoint.y -= 100
for screen in NSScreen.screens where screen.frame.contains(testPoint) { for screen in NSScreen.screens where screen.frame.contains(testPoint) {
statusItemScreen = screen statusItemScreen = screen
break break
} }
let screenMaxX = (statusItemScreen?.frame)!.maxX let screenMaxX = (statusItemScreen?.frame)!.maxX
let minY = statusItemFrame.origin.y < (statusItemScreen?.frame)!.maxY ? let minY = statusItemFrame.origin.y < (statusItemScreen?.frame)!.maxY ?
statusItemFrame.origin.y : statusItemFrame.origin.y :
(statusItemScreen?.frame)!.maxY (statusItemScreen?.frame)!.maxY
statusItemFrame.origin.y = minY statusItemFrame.origin.y = minY
setFrameTheNewWay(statusItemFrame, screenMaxX) setFrameTheNewWay(statusItemFrame, screenMaxX)
if #available(OSX 10.14, *) { PerfLogger.endMarker("Set Panel Frame")
PerfLogger.endMarker("Set Panel Frame")
}
} }
} }
private func log() { private func log() {
if #available(OSX 10.14, *) { PerfLogger.startMarker("Logging")
PerfLogger.startMarker("Logging")
}
let preferences = DataStore.shared().timezones() let preferences = DataStore.shared().timezones()
guard let theme = DataStore.shared().retrieve(key: UserDefaultKeys.themeKey) as? NSNumber, guard let theme = DataStore.shared().retrieve(key: UserDefaultKeys.themeKey) as? NSNumber,
let displayFutureSliderKey = DataStore.shared().retrieve(key: UserDefaultKeys.themeKey) as? NSNumber, let displayFutureSliderKey = DataStore.shared().retrieve(key: UserDefaultKeys.themeKey) as? NSNumber,
let showAppInForeground = DataStore.shared().retrieve(key: UserDefaultKeys.showAppInForeground) as? NSNumber, let showAppInForeground = DataStore.shared().retrieve(key: UserDefaultKeys.showAppInForeground) as? NSNumber,
@ -199,15 +187,15 @@ class PanelController: ParentPanelController {
else { else {
return return
} }
var relativeDate = "Relative" var relativeDate = "Relative"
if relativeDateKey.isEqual(to: NSNumber(value: 1)) { if relativeDateKey.isEqual(to: NSNumber(value: 1)) {
relativeDate = "Actual Day" relativeDate = "Actual Day"
} else if relativeDateKey.isEqual(to: NSNumber(value: 2)) { } else if relativeDateKey.isEqual(to: NSNumber(value: 2)) {
relativeDate = "Date" relativeDate = "Date"
} }
let panelEvent: [String: Any] = [ let panelEvent: [String: Any] = [
"Theme": theme.isEqual(to: NSNumber(value: 0)) ? "Default" : "Black", "Theme": theme.isEqual(to: NSNumber(value: 0)) ? "Default" : "Black",
"Display Future Slider": displayFutureSliderKey.isEqual(to: NSNumber(value: 0)) ? "Yes" : "No", "Display Future Slider": displayFutureSliderKey.isEqual(to: NSNumber(value: 0)) ? "Yes" : "No",
@ -223,41 +211,33 @@ class PanelController: ParentPanelController {
"Calendar Access Provided": EventCenter.sharedCenter().calendarAccessGranted() ? "Yes" : "No", "Calendar Access Provided": EventCenter.sharedCenter().calendarAccessGranted() ? "Yes" : "No",
"Number of Timezones": preferences.count, "Number of Timezones": preferences.count,
] ]
Logger.log(object: panelEvent, for: "openedPanel") Logger.log(object: panelEvent, for: "openedPanel")
if #available(OSX 10.14, *) { PerfLogger.endMarker("Logging")
PerfLogger.endMarker("Logging")
}
} }
private func startWindowTimer() { private func startWindowTimer() {
if #available(OSX 10.14, *) { PerfLogger.startMarker("Start Window Timer")
PerfLogger.startMarker("Start Window Timer")
}
stopMenubarTimerIfNeccesary() stopMenubarTimerIfNeccesary()
if let timer = parentTimer, timer.state == .paused { if let timer = parentTimer, timer.state == .paused {
parentTimer?.start() parentTimer?.start()
if #available(OSX 10.14, *) { PerfLogger.endMarker("Start Window Timer")
PerfLogger.endMarker("Start Window Timer")
}
return return
} }
startTimer() startTimer()
if #available(OSX 10.14, *) { PerfLogger.endMarker("Start Window Timer")
PerfLogger.endMarker("Start Window Timer")
}
} }
private func startTimer() { private func startTimer() {
Logger.info("Start timer called") Logger.info("Start timer called")
parentTimer = Repeater(interval: .seconds(1), mode: .infinite) { _ in parentTimer = Repeater(interval: .seconds(1), mode: .infinite) { _ in
OperationQueue.main.addOperation { OperationQueue.main.addOperation {
self.updateTime() self.updateTime()
@ -265,27 +245,27 @@ class PanelController: ParentPanelController {
} }
parentTimer!.start() parentTimer!.start()
} }
private func stopMenubarTimerIfNeccesary() { private func stopMenubarTimerIfNeccesary() {
let count = DataStore.shared().menubarTimezones()?.count ?? 0 let count = DataStore.shared().menubarTimezones()?.count ?? 0
if count >= 1 || DataStore.shared().shouldDisplay(.showMeetingInMenubar) { if count >= 1 || DataStore.shared().shouldDisplay(.showMeetingInMenubar) {
if let delegate = NSApplication.shared.delegate as? AppDelegate { if let delegate = NSApplication.shared.delegate as? AppDelegate {
Logger.info("We will be invalidating the menubar timer as we want the parent timer to take care of both panel and menubar ") Logger.info("We will be invalidating the menubar timer as we want the parent timer to take care of both panel and menubar ")
delegate.invalidateMenubarTimer(false) delegate.invalidateMenubarTimer(false)
} }
} }
} }
func cancelOperation() { func cancelOperation() {
setActivePanel(newValue: false) setActivePanel(newValue: false)
} }
func hasActivePanelGetter() -> Bool { func hasActivePanelGetter() -> Bool {
return hasActivePanel return hasActivePanel
} }
func minimize() { func minimize() {
let delegate = NSApplication.shared.delegate as? AppDelegate let delegate = NSApplication.shared.delegate as? AppDelegate
let count = DataStore.shared().menubarTimezones()?.count ?? 0 let count = DataStore.shared().menubarTimezones()?.count ?? 0
@ -294,96 +274,96 @@ class PanelController: ParentPanelController {
delegate?.setupMenubarTimer() delegate?.setupMenubarTimer()
} }
} }
parentTimer?.pause() parentTimer?.pause()
updatePopoverDisplayState() updatePopoverDisplayState()
NSAnimationContext.beginGrouping() NSAnimationContext.beginGrouping()
NSAnimationContext.current.duration = 0.1 NSAnimationContext.current.duration = 0.1
window?.animator().alphaValue = 0 window?.animator().alphaValue = 0
additionalOptionsPopover?.close() additionalOptionsPopover?.close()
NSAnimationContext.endGrouping() NSAnimationContext.endGrouping()
window?.orderOut(nil) window?.orderOut(nil)
datasource = nil datasource = nil
parentTimer?.pause() parentTimer?.pause()
parentTimer = nil parentTimer = nil
} }
func setActivePanel(newValue: Bool) { func setActivePanel(newValue: Bool) {
hasActivePanel = newValue hasActivePanel = newValue
hasActivePanel ? open() : minimize() hasActivePanel ? open() : minimize()
} }
class func panel() -> PanelController? { class func panel() -> PanelController? {
let panel = NSApplication.shared.windows.compactMap { window -> PanelController? in let panel = NSApplication.shared.windows.compactMap { window -> PanelController? in
guard let parent = window.windowController as? PanelController else { guard let parent = window.windowController as? PanelController else {
return nil return nil
} }
return parent return parent
} }
return panel.first return panel.first
} }
override func showNotesPopover(forRow row: Int, relativeTo positioningRect: NSRect, andButton target: NSButton!) -> Bool { override func showNotesPopover(forRow row: Int, relativeTo positioningRect: NSRect, andButton target: NSButton!) -> Bool {
if additionalOptionsPopover == nil { if additionalOptionsPopover == nil {
additionalOptionsPopover = NSPopover() additionalOptionsPopover = NSPopover()
} }
guard let popover = additionalOptionsPopover else { guard let popover = additionalOptionsPopover else {
return false return false
} }
target.image = Themer.shared().extraOptionsHighlightedImage() target.image = Themer.shared().extraOptionsHighlightedImage()
if popover.isShown, row == previousPopoverRow { if popover.isShown, row == previousPopoverRow {
popover.close() popover.close()
target.image = Themer.shared().extraOptionsImage() target.image = Themer.shared().extraOptionsImage()
previousPopoverRow = -1 previousPopoverRow = -1
return false return false
} }
previousPopoverRow = row previousPopoverRow = row
super.showNotesPopover(forRow: row, relativeTo: positioningRect, andButton: target) super.showNotesPopover(forRow: row, relativeTo: positioningRect, andButton: target)
popover.show(relativeTo: positioningRect, popover.show(relativeTo: positioningRect,
of: target, of: target,
preferredEdge: .minX) preferredEdge: .minX)
if let timer = parentTimer, timer.state == .paused { if let timer = parentTimer, timer.state == .paused {
timer.start() timer.start()
} }
return true return true
} }
func setupMenubarTimer() { func setupMenubarTimer() {
if let appDelegate = NSApplication.shared.delegate as? AppDelegate { if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
appDelegate.setupMenubarTimer() appDelegate.setupMenubarTimer()
} }
} }
func pauseTimer() { func pauseTimer() {
if let timer = parentTimer { if let timer = parentTimer {
timer.pause() timer.pause()
} }
} }
func refreshBackgroundView() { func refreshBackgroundView() {
backgroundView.setNeedsDisplay(backgroundView.bounds) backgroundView.setNeedsDisplay(backgroundView.bounds)
} }
override func scrollWheel(with event: NSEvent) { override func scrollWheel(with event: NSEvent) {
if event.phase == NSEvent.Phase.ended { if event.phase == NSEvent.Phase.ended {
Logger.log(object: nil, for: "Scroll Event Ended") Logger.log(object: nil, for: "Scroll Event Ended")
} }
// We only want to move the slider if the slider is visible. // We only want to move the slider if the slider is visible.
// If the parent view is hidden, then that doesn't automatically mean that all the childViews are also hidden // If the parent view is hidden, then that doesn't automatically mean that all the childViews are also hidden
// Hence, check if the parent view is totally hidden or not.. // Hence, check if the parent view is totally hidden or not..
@ -399,7 +379,7 @@ extension PanelController: NSWindowDelegate {
parentTimer = nil parentTimer = nil
setActivePanel(newValue: false) setActivePanel(newValue: false)
} }
func windowDidResignKey(_: Notification) { func windowDidResignKey(_: Notification) {
parentTimer = nil parentTimer = nil

40
Clocker/Panel/ParentPanelController+ModernSlider.swift

@ -10,7 +10,7 @@ extension ParentPanelController: NSCollectionViewDataSource {
let futureSliderDayRange = (futureSliderDayPreference.intValue + 1) let futureSliderDayRange = (futureSliderDayPreference.intValue + 1)
return (PanelConstants.modernSliderPointsInADay * futureSliderDayRange * 2) + 1 return (PanelConstants.modernSliderPointsInADay * futureSliderDayRange * 2) + 1
} }
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
guard let item = collectionView.makeItem(withIdentifier: TimeMarkerViewItem.reuseIdentifier, for: indexPath) as? TimeMarkerViewItem else { guard let item = collectionView.makeItem(withIdentifier: TimeMarkerViewItem.reuseIdentifier, for: indexPath) as? TimeMarkerViewItem else {
return NSCollectionViewItem() return NSCollectionViewItem()
@ -36,16 +36,16 @@ extension ParentPanelController {
if let scrollView = modernSlider.superview?.superview as? NSScrollView { if let scrollView = modernSlider.superview?.superview as? NSScrollView {
scrollView.scrollerStyle = NSScroller.Style.overlay scrollView.scrollerStyle = NSScroller.Style.overlay
} }
goBackwardsButton.image = Themer.shared().goBackwardsImage() goBackwardsButton.image = Themer.shared().goBackwardsImage()
goForwardButton.image = Themer.shared().goForwardsImage() goForwardButton.image = Themer.shared().goForwardsImage()
goForwardButton.isContinuous = true goForwardButton.isContinuous = true
goBackwardsButton.isContinuous = true goBackwardsButton.isContinuous = true
goBackwardsButton.toolTip = "Navigate 15 mins back" goBackwardsButton.toolTip = "Navigate 15 mins back"
goForwardButton.toolTip = "Navigate 15 mins forward" goForwardButton.toolTip = "Navigate 15 mins forward"
modernSlider.wantsLayer = true // Required for animating reset to center modernSlider.wantsLayer = true // Required for animating reset to center
modernSlider.enclosingScrollView?.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) modernSlider.enclosingScrollView?.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
modernSlider.enclosingScrollView?.backgroundColor = NSColor.clear modernSlider.enclosingScrollView?.backgroundColor = NSColor.clear
@ -55,27 +55,27 @@ extension ParentPanelController {
selector: #selector(collectionViewDidScroll(_:)), selector: #selector(collectionViewDidScroll(_:)),
name: NSView.boundsDidChangeNotification, name: NSView.boundsDidChangeNotification,
object: modernSlider.superview) object: modernSlider.superview)
// Set the modern slider label! // Set the modern slider label!
closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation() closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation()
if let unwrappedClosetQuarterTime = closestQuarterTimeRepresentation { if let unwrappedClosetQuarterTime = closestQuarterTimeRepresentation {
modernSliderLabel.stringValue = timezoneFormattedStringRepresentation(unwrappedClosetQuarterTime) modernSliderLabel.stringValue = timezoneFormattedStringRepresentation(unwrappedClosetQuarterTime)
} }
// Make sure modern slider is centered horizontally! // Make sure modern slider is centered horizontally!
let indexPaths: Set<IndexPath> = Set([IndexPath(item: modernSlider.numberOfItems(inSection: 0) / 2, section: 0)]) let indexPaths: Set<IndexPath> = Set([IndexPath(item: modernSlider.numberOfItems(inSection: 0) / 2, section: 0)])
modernSlider.scrollToItems(at: indexPaths, scrollPosition: .centeredHorizontally) modernSlider.scrollToItems(at: indexPaths, scrollPosition: .centeredHorizontally)
} }
} }
@IBAction func goForward(_: NSButton) { @IBAction func goForward(_: NSButton) {
navigateModernSliderToSpecificIndex(1) navigateModernSliderToSpecificIndex(1)
} }
@IBAction func goBackward(_: NSButton) { @IBAction func goBackward(_: NSButton) {
navigateModernSliderToSpecificIndex(-1) navigateModernSliderToSpecificIndex(-1)
} }
private func animateButton(_ hidden: Bool) { private func animateButton(_ hidden: Bool) {
NSAnimationContext.runAnimationGroup({ context in NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5 context.duration = 0.5
@ -95,7 +95,7 @@ extension ParentPanelController {
goBackwardsButton.animator().alphaValue = hide ? 0.0 : 1.0 goBackwardsButton.animator().alphaValue = hide ? 0.0 : 1.0
}, completionHandler: nil) }, completionHandler: nil)
} }
@IBAction func resetModernSlider(_: NSButton) { @IBAction func resetModernSlider(_: NSButton) {
closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation() closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation()
modernSliderLabel.stringValue = "Time Scroller" modernSliderLabel.stringValue = "Time Scroller"
@ -112,7 +112,7 @@ extension ParentPanelController {
}) })
} }
} }
private func navigateModernSliderToSpecificIndex(_ index: Int) { private func navigateModernSliderToSpecificIndex(_ index: Int) {
guard let contentView = modernSlider.superview as? NSClipView else { guard let contentView = modernSlider.superview as? NSClipView else {
return return
@ -124,12 +124,12 @@ extension ParentPanelController {
modernSlider.scrollToItems(at: Set([previousIndexPath]), scrollPosition: .centeredHorizontally) modernSlider.scrollToItems(at: Set([previousIndexPath]), scrollPosition: .centeredHorizontally)
} }
} }
@objc func collectionViewDidScroll(_ notification: NSNotification) { @objc func collectionViewDidScroll(_ notification: NSNotification) {
guard let contentView = notification.object as? NSClipView else { guard let contentView = notification.object as? NSClipView else {
return return
} }
let changedOrigin = contentView.documentVisibleRect.origin let changedOrigin = contentView.documentVisibleRect.origin
let newPoint = NSPoint(x: changedOrigin.x + contentView.frame.width / 2, y: changedOrigin.y) let newPoint = NSPoint(x: changedOrigin.x + contentView.frame.width / 2, y: changedOrigin.y)
let indexPath = modernSlider.indexPathForItem(at: newPoint) let indexPath = modernSlider.indexPathForItem(at: newPoint)
@ -141,7 +141,7 @@ extension ParentPanelController {
mainTableView.reloadData() mainTableView.reloadData()
} }
} }
public func findClosestQuarterTimeApproximation() -> Date { public func findClosestQuarterTimeApproximation() -> Date {
let defaultParameters = minuteFromCalendar() let defaultParameters = minuteFromCalendar()
let hourQuarterDate = Calendar.current.nextDate(after: defaultParameters.0, let hourQuarterDate = Calendar.current.nextDate(after: defaultParameters.0,
@ -159,7 +159,7 @@ extension ParentPanelController {
} }
return (minutes / 60, minutesRemaining) return (minutes / 60, minutesRemaining)
} }
public func setDefaultDateLabel(_ index: Int) -> Int { public func setDefaultDateLabel(_ index: Int) -> Int {
let futureSliderDayPreference = DataStore.shared().retrieve(key: UserDefaultKeys.futureSliderRange) as? NSNumber ?? 5 let futureSliderDayPreference = DataStore.shared().retrieve(key: UserDefaultKeys.futureSliderRange) as? NSNumber ?? 5
let futureSliderDayRange = (futureSliderDayPreference.intValue + 1) let futureSliderDayRange = (futureSliderDayPreference.intValue + 1)
@ -173,7 +173,7 @@ extension ParentPanelController {
if resetModernSliderButton.isHidden { if resetModernSliderButton.isHidden {
animateButton(false) animateButton(false)
} }
return nextDate.minutes(from: Date()) + 1 return nextDate.minutes(from: Date()) + 1
} else if index < centerPoint { } else if index < centerPoint {
let remainder = centerPoint - index + 1 let remainder = centerPoint - index + 1
@ -193,7 +193,7 @@ extension ParentPanelController {
return 0 return 0
} }
} }
private func minuteFromCalendar() -> (Date, Int) { private func minuteFromCalendar() -> (Date, Int) {
let currentDate = Date() let currentDate = Date()
var minute = Calendar.current.component(.minute, from: currentDate) var minute = Calendar.current.component(.minute, from: currentDate)
@ -206,10 +206,10 @@ extension ParentPanelController {
} else { } else {
minute = 0 minute = 0
} }
return (currentDate, minute) return (currentDate, minute)
} }
private func timezoneFormattedStringRepresentation(_ date: Date) -> String { private func timezoneFormattedStringRepresentation(_ date: Date) -> String {
let dateFormatter = DateFormatterManager.dateFormatterWithFormat(with: .none, let dateFormatter = DateFormatterManager.dateFormatterWithFormat(with: .none,
format: "MMM d HH:mm", format: "MMM d HH:mm",

500
Clocker/Panel/ParentPanelController.swift

File diff suppressed because it is too large Load Diff

40
Clocker/Panel/Rate Controller/ReviewController.swift

@ -6,71 +6,63 @@ import StoreKit
final class ReviewController { final class ReviewController {
private static var storage = UserDefaults.standard private static var storage = UserDefaults.standard
private static var debugging = false private static var debugging = false
private enum Keys { private enum Keys {
static let lastPrompt = "last-prompt" static let lastPrompt = "last-prompt"
static let lastVersion = "last-version" static let lastVersion = "last-version"
static let install = "install" static let install = "install"
} }
class func applicationDidLaunch(_ defaults: UserDefaults) { class func applicationDidLaunch(_ defaults: UserDefaults) {
if ProcessInfo.processInfo.arguments.contains(UserDefaultKeys.testingLaunchArgument) { if ProcessInfo.processInfo.arguments.contains(UserDefaultKeys.testingLaunchArgument) {
debugging = true debugging = true
} }
storage = defaults storage = defaults
if defaults.object(forKey: Keys.install) == nil { if defaults.object(forKey: Keys.install) == nil {
defaults.set(Date(), forKey: Keys.install) defaults.set(Date(), forKey: Keys.install)
} }
} }
class func setPreviewMode(_ value: Bool) { class func setPreviewMode(_ value: Bool) {
debugging = value debugging = value
} }
class func prompted() { class func prompted() {
storage.set(Date(), forKey: Keys.lastPrompt) storage.set(Date(), forKey: Keys.lastPrompt)
storage.set(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String, forKey: Keys.lastVersion) storage.set(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String, forKey: Keys.lastVersion)
} }
class func canPrompt() -> Bool { class func canPrompt() -> Bool {
if debugging == true { if debugging == true {
return true return true
} }
let day: TimeInterval = -1 * 60 * 60 * 24 let day: TimeInterval = -1 * 60 * 60 * 24
let minInstall: TimeInterval = day * 7 let minInstall: TimeInterval = day * 7
// Check if the app has been installed for atleast 7 days // Check if the app has been installed for atleast 7 days
guard let install = storage.object(forKey: Keys.install) as? Date, guard let install = storage.object(forKey: Keys.install) as? Date,
install.timeIntervalSinceNow < minInstall install.timeIntervalSinceNow < minInstall
else { return false } else { return false }
// If we have never been prompted before, go ahead and prompt // If we have never been prompted before, go ahead and prompt
guard let lastPrompt = storage.object(forKey: Keys.lastPrompt) as? Date, guard let lastPrompt = storage.object(forKey: Keys.lastPrompt) as? Date,
let lastVersion = storage.object(forKey: Keys.lastVersion) as? String let lastVersion = storage.object(forKey: Keys.lastVersion) as? String
else { return true } else { return true }
// Minimum interval between two versions should be 3 months // Minimum interval between two versions should be 3 months
let minInterval: TimeInterval = day * 90 let minInterval: TimeInterval = day * 90
// never prompt w/in the same version // never prompt w/in the same version
return lastVersion != (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) return lastVersion != (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String)
// limit all types of prompts to at least 1mo intervals // limit all types of prompts to at least 1mo intervals
&& lastPrompt.timeIntervalSinceNow < minInterval && lastPrompt.timeIntervalSinceNow < minInterval
} }
class func prompt() { class func prompt() {
if #available(OSX 10.14, *) { SKStoreReviewController.requestReview()
SKStoreReviewController.requestReview()
} else {
guard let ratingsURL = URL(string: AboutUsConstants.AppStoreLink) else {
return
}
NSWorkspace.shared.open(ratingsURL)
}
prompted() prompted()
} }
} }

114
Clocker/Panel/UI/TimezoneDataSource.swift

@ -7,7 +7,7 @@ class TimezoneDataSource: NSObject {
var timezones: [TimezoneData] = [] var timezones: [TimezoneData] = []
var sliderValue: Int = 0 var sliderValue: Int = 0
var dataStore: DataStore var dataStore: DataStore
init(items: [TimezoneData], store: DataStore) { init(items: [TimezoneData], store: DataStore) {
sliderValue = 0 sliderValue = 0
timezones = Array(items) timezones = Array(items)
@ -20,7 +20,7 @@ extension TimezoneDataSource {
func setSlider(value: Int) { func setSlider(value: Int) {
sliderValue = value sliderValue = value
} }
func setItems(items: [TimezoneData]) { func setItems(items: [TimezoneData]) {
timezones = items timezones = items
} }
@ -29,38 +29,36 @@ extension TimezoneDataSource {
extension TimezoneDataSource: NSTableViewDataSource, NSTableViewDelegate { extension TimezoneDataSource: NSTableViewDataSource, NSTableViewDelegate {
func numberOfRows(in _: NSTableView) -> Int { func numberOfRows(in _: NSTableView) -> Int {
var totalTimezones = timezones.count var totalTimezones = timezones.count
// If totalTimezone is 0, then we can show an option to add timezones // If totalTimezone is 0, then we can show an option to add timezones
if totalTimezones == 0 { if totalTimezones == 0 {
totalTimezones += 1 totalTimezones += 1
} }
return totalTimezones return totalTimezones
} }
func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? { func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? {
guard !timezones.isEmpty else { guard !timezones.isEmpty else {
if let addCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "addCell"), owner: self) as? AddTableViewCell { if let addCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "addCell"), owner: self) as? AddTableViewCell {
return addCellView return addCellView
} }
assertionFailure("Unable to create AddTableViewCell") assertionFailure("Unable to create AddTableViewCell")
return nil return nil
} }
guard let cellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timeZoneCell"), owner: self) as? TimezoneCellView else { guard let cellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "timeZoneCell"), owner: self) as? TimezoneCellView else {
assertionFailure("Unable to create tableviewcell") assertionFailure("Unable to create tableviewcell")
return NSView() return NSView()
} }
let currentModel = timezones[row] let currentModel = timezones[row]
let operation = TimezoneDataOperations(with: currentModel, store: dataStore) let operation = TimezoneDataOperations(with: currentModel, store: dataStore)
cellView.sunriseSetTime.stringValue = operation.formattedSunriseTime(with: sliderValue) cellView.sunriseSetTime.stringValue = operation.formattedSunriseTime(with: sliderValue)
cellView.sunriseImage.image = currentModel.isSunriseOrSunset ? Themer.shared().sunriseImage() : Themer.shared().sunsetImage() cellView.sunriseImage.image = currentModel.isSunriseOrSunset ? Themer.shared().sunriseImage() : Themer.shared().sunsetImage()
if #available(macOS 10.14, *) { cellView.sunriseImage.contentTintColor = currentModel.isSunriseOrSunset ? NSColor.systemYellow : NSColor.systemOrange
cellView.sunriseImage.contentTintColor = currentModel.isSunriseOrSunset ? NSColor.systemYellow : NSColor.systemOrange
}
cellView.relativeDate.stringValue = operation.date(with: sliderValue, displayType: .panel) cellView.relativeDate.stringValue = operation.date(with: sliderValue, displayType: .panel)
cellView.rowNumber = row cellView.rowNumber = row
cellView.customName.stringValue = currentModel.formattedTimezoneLabel() cellView.customName.stringValue = currentModel.formattedTimezoneLabel()
@ -82,110 +80,110 @@ extension TimezoneDataSource: NSTableViewDataSource, NSTableViewDelegate {
cellView.layout(with: currentModel) cellView.layout(with: currentModel)
cellView.setAccessibilityIdentifier(currentModel.formattedTimezoneLabel()) cellView.setAccessibilityIdentifier(currentModel.formattedTimezoneLabel())
cellView.setAccessibilityLabel(currentModel.formattedTimezoneLabel()) cellView.setAccessibilityLabel(currentModel.formattedTimezoneLabel())
return cellView return cellView
} }
func tableView(_: NSTableView, heightOfRow row: Int) -> CGFloat { func tableView(_: NSTableView, heightOfRow row: Int) -> CGFloat {
guard !timezones.isEmpty else { guard !timezones.isEmpty else {
return 100 return 100
} }
if let userFontSize = dataStore.retrieve(key: UserDefaultKeys.userFontSizePreference) as? NSNumber, if let userFontSize = dataStore.retrieve(key: UserDefaultKeys.userFontSizePreference) as? NSNumber,
timezones.count > row, timezones.count > row,
let relativeDisplay = dataStore.retrieve(key: UserDefaultKeys.relativeDateKey) as? NSNumber let relativeDisplay = dataStore.retrieve(key: UserDefaultKeys.relativeDateKey) as? NSNumber
{ {
let model = timezones[row] let model = timezones[row]
let shouldShowSunrise = dataStore.shouldDisplay(.sunrise) let shouldShowSunrise = dataStore.shouldDisplay(.sunrise)
var rowHeight: Int = userFontSize == 4 ? 60 : 65 var rowHeight: Int = userFontSize == 4 ? 60 : 65
if relativeDisplay.intValue == 3 { if relativeDisplay.intValue == 3 {
rowHeight -= 5 rowHeight -= 5
} }
if shouldShowSunrise, model.selectionType == .city { if shouldShowSunrise, model.selectionType == .city {
rowHeight += 8 rowHeight += 8
} }
if let note = model.note, !note.isEmpty { if let note = model.note, !note.isEmpty {
rowHeight += userFontSize.intValue + 15 rowHeight += userFontSize.intValue + 15
} else if TimezoneDataOperations(with: model, store: dataStore).nextDaylightSavingsTransitionIfAvailable(with: sliderValue) != nil { } else if TimezoneDataOperations(with: model, store: dataStore).nextDaylightSavingsTransitionIfAvailable(with: sliderValue) != nil {
rowHeight += userFontSize.intValue + 15 rowHeight += userFontSize.intValue + 15
} }
if model.isSystemTimezone { if model.isSystemTimezone {
rowHeight += 2 rowHeight += 2
} }
rowHeight += (userFontSize.intValue * 2) rowHeight += (userFontSize.intValue * 2)
return CGFloat(rowHeight) return CGFloat(rowHeight)
} }
return 1 return 1
} }
func tableView(_ tableView: NSTableView, rowActionsForRow row: Int, edge: NSTableView.RowActionEdge) -> [NSTableViewRowAction] { func tableView(_ tableView: NSTableView, rowActionsForRow row: Int, edge: NSTableView.RowActionEdge) -> [NSTableViewRowAction] {
guard !timezones.isEmpty else { guard !timezones.isEmpty else {
return [] return []
} }
let windowController = FloatingWindowController.shared() let windowController = FloatingWindowController.shared()
if edge == .trailing { if edge == .trailing {
let swipeToDelete = NSTableViewRowAction(style: .destructive, let swipeToDelete = NSTableViewRowAction(style: .destructive,
title: "Delete", title: "Delete",
handler: { _, row in handler: { _, row in
if self.timezones[row].isSystemTimezone { if self.timezones[row].isSystemTimezone {
self.showAlertForDeletingAHomeRow(row, tableView) self.showAlertForDeletingAHomeRow(row, tableView)
return return
} }
let indexSet = IndexSet(integer: row) let indexSet = IndexSet(integer: row)
tableView.removeRows(at: indexSet, withAnimation: NSTableView.AnimationOptions()) tableView.removeRows(at: indexSet, withAnimation: NSTableView.AnimationOptions())
if self.dataStore.shouldDisplay(ViewType.showAppInForeground) { if self.dataStore.shouldDisplay(ViewType.showAppInForeground) {
windowController.deleteTimezone(at: row) windowController.deleteTimezone(at: row)
} else { } else {
guard let panelController = PanelController.panel() else { return } guard let panelController = PanelController.panel() else { return }
panelController.deleteTimezone(at: row) panelController.deleteTimezone(at: row)
} }
}) })
if #available(OSX 11.0, *) { if #available(OSX 11.0, *) {
swipeToDelete.image = Themer.shared().filledTrashImage() swipeToDelete.image = Themer.shared().filledTrashImage()
} else { } else {
swipeToDelete.image = NSImage(named: NSImage.Name("Trash")) swipeToDelete.image = NSImage(named: NSImage.Name("Trash"))
} }
return [swipeToDelete] return [swipeToDelete]
} }
return [] return []
} }
private func showAlertForDeletingAHomeRow(_ row: Int, _ tableView: NSTableView) { private func showAlertForDeletingAHomeRow(_ row: Int, _ tableView: NSTableView) {
NSApplication.shared.activate(ignoringOtherApps: true) NSApplication.shared.activate(ignoringOtherApps: true)
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Confirm deleting the home row? 😅" alert.messageText = "Confirm deleting the home row? 😅"
alert.informativeText = "This row is automatically updated when Clocker detects a system timezone change. Are you sure you want to delete this?" alert.informativeText = "This row is automatically updated when Clocker detects a system timezone change. Are you sure you want to delete this?"
alert.addButton(withTitle: "Yes") alert.addButton(withTitle: "Yes")
alert.addButton(withTitle: "No") alert.addButton(withTitle: "No")
let response = alert.runModal() let response = alert.runModal()
if response.rawValue == 1000 { if response.rawValue == 1000 {
OperationQueue.main.addOperation { [weak self] in OperationQueue.main.addOperation { [weak self] in
guard let sSelf = self else { return } guard let sSelf = self else { return }
let indexSet = IndexSet(integer: row) let indexSet = IndexSet(integer: row)
tableView.removeRows(at: indexSet, withAnimation: NSTableView.AnimationOptions.slideUp) tableView.removeRows(at: indexSet, withAnimation: NSTableView.AnimationOptions.slideUp)
if sSelf.dataStore.shouldDisplay(ViewType.showAppInForeground) { if sSelf.dataStore.shouldDisplay(ViewType.showAppInForeground) {
let windowController = FloatingWindowController.shared() let windowController = FloatingWindowController.shared()
windowController.deleteTimezone(at: row) windowController.deleteTimezone(at: row)
@ -206,7 +204,7 @@ extension TimezoneDataSource: PanelTableViewDelegate {
rowCellView.extraOptions.alphaValue = 0.5 rowCellView.extraOptions.alphaValue = 0.5
continue continue
} }
rowCellView.extraOptions.alphaValue = (rowIndex == row) ? 1 : 0.5 rowCellView.extraOptions.alphaValue = (rowIndex == row) ? 1 : 0.5
if rowIndex == row, let hoverString = hoverStringForSelectedRow(row: row), sliderValue == 0 { if rowIndex == row, let hoverString = hoverStringForSelectedRow(row: row), sliderValue == 0 {
rowCellView.relativeDate.stringValue = hoverString rowCellView.relativeDate.stringValue = hoverString
@ -214,7 +212,7 @@ extension TimezoneDataSource: PanelTableViewDelegate {
} }
} }
} }
private func hoverStringForSelectedRow(row: Int) -> String? { private func hoverStringForSelectedRow(row: Int) -> String? {
let currentModel = timezones[row] let currentModel = timezones[row]
if let timezone = TimeZone(identifier: currentModel.timezone()) { if let timezone = TimeZone(identifier: currentModel.timezone()) {
@ -235,16 +233,16 @@ extension TimezoneDataSource: PanelTableViewDelegate {
extension TimezoneCellView { extension TimezoneCellView {
func layout(with model: TimezoneData) { func layout(with model: TimezoneData) {
let shouldDisplay = DataStore.shared().shouldDisplay(.sunrise) && !sunriseSetTime.stringValue.isEmpty let shouldDisplay = DataStore.shared().shouldDisplay(.sunrise) && !sunriseSetTime.stringValue.isEmpty
sunriseSetTime.isHidden = !shouldDisplay sunriseSetTime.isHidden = !shouldDisplay
sunriseImage.isHidden = !shouldDisplay sunriseImage.isHidden = !shouldDisplay
// If it's a timezone and not a place, we can't determine the sunrise/sunset time; hide the sunrise image // If it's a timezone and not a place, we can't determine the sunrise/sunset time; hide the sunrise image
if model.selectionType == .timezone, model.latitude == nil, model.longitude == nil { if model.selectionType == .timezone, model.latitude == nil, model.longitude == nil {
sunriseImage.isHidden = true sunriseImage.isHidden = true
} }
setupLayout() setupLayout()
} }
} }

4
Clocker/Preferences/Appearance/AppearanceViewController.swift

@ -126,10 +126,6 @@ class AppearanceViewController: ParentViewController {
sliderDayRangePopup.selectItem(at: selectedIndex.intValue) sliderDayRangePopup.selectItem(at: selectedIndex.intValue)
} }
if #available(macOS 10.14, *) {} else {
theme.removeItem(at: 2)
}
let shouldDisplayCompact = DataStore.shared().shouldDisplay(.menubarCompactMode) let shouldDisplayCompact = DataStore.shared().shouldDisplay(.menubarCompactMode)
menubarMode.setSelected(true, forSegment: shouldDisplayCompact ? 0 : 1) menubarMode.setSelected(true, forSegment: shouldDisplayCompact ? 0 : 1)

110
Clocker/Preferences/Calendar/CalendarViewController.swift

@ -9,18 +9,18 @@ class ClockerTextBackgroundView: NSView {
wantsLayer = true wantsLayer = true
layer?.cornerRadius = 8.0 layer?.cornerRadius = 8.0
layer?.masksToBounds = false layer?.masksToBounds = false
NotificationCenter.default.addObserver(self, NotificationCenter.default.addObserver(self,
selector: #selector(updateBackgroundColor), selector: #selector(updateBackgroundColor),
name: .themeDidChangeNotification, name: .themeDidChangeNotification,
object: nil) object: nil)
} }
override func updateLayer() { override func updateLayer() {
super.updateLayer() super.updateLayer()
layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor
} }
@objc func updateBackgroundColor() { @objc func updateBackgroundColor() {
layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor
} }
@ -34,61 +34,59 @@ class CalendarViewController: ParentViewController {
@IBOutlet var informationField: NSTextField! @IBOutlet var informationField: NSTextField!
@IBOutlet var grantAccessButton: NSButton! @IBOutlet var grantAccessButton: NSButton!
@IBOutlet var calendarsTableView: NSTableView! @IBOutlet var calendarsTableView: NSTableView!
@IBOutlet var showNextMeetingInMenubarControl: NSSegmentedControl! @IBOutlet var showNextMeetingInMenubarControl: NSSegmentedControl!
@IBOutlet var backgroundView: NSView! @IBOutlet var backgroundView: NSView!
@IBOutlet var nextMeetingBackgroundView: NSView! @IBOutlet var nextMeetingBackgroundView: NSView!
private var themeDidChangeNotification: NSObjectProtocol? private var themeDidChangeNotification: NSObjectProtocol?
private lazy var calendars: [Any] = EventCenter.sharedCenter().fetchSourcesAndCalendars() private lazy var calendars: [Any] = EventCenter.sharedCenter().fetchSourcesAndCalendars()
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setup() setup()
NotificationCenter.default.addObserver(self, NotificationCenter.default.addObserver(self,
selector: #selector(calendarAccessStatusChanged), selector: #selector(calendarAccessStatusChanged),
name: .calendarAccessGranted, name: .calendarAccessGranted,
object: nil) object: nil)
themeDidChangeNotification = NotificationCenter.default.addObserver(forName: .themeDidChangeNotification, object: nil, queue: OperationQueue.main) { _ in themeDidChangeNotification = NotificationCenter.default.addObserver(forName: .themeDidChangeNotification, object: nil, queue: OperationQueue.main) { _ in
self.setup() self.setup()
} }
if #available(macOS 10.14, *) { noAccessView.material = .contentBackground
noAccessView.material = .contentBackground
}
upcomingEventView.setAccessibility("UpcomingEventView") upcomingEventView.setAccessibility("UpcomingEventView")
} }
deinit { deinit {
if let themeDidChangeNotif = themeDidChangeNotification { if let themeDidChangeNotif = themeDidChangeNotification {
NotificationCenter.default.removeObserver(themeDidChangeNotif) NotificationCenter.default.removeObserver(themeDidChangeNotif)
} }
} }
@objc func calendarAccessStatusChanged() { @objc func calendarAccessStatusChanged() {
verifyCalendarAccess() verifyCalendarAccess()
view.window?.windowController?.showWindow(nil) view.window?.windowController?.showWindow(nil)
view.window?.makeKeyAndOrderFront(nil) view.window?.makeKeyAndOrderFront(nil)
} }
override func viewWillAppear() { override func viewWillAppear() {
super.viewWillAppear() super.viewWillAppear()
verifyCalendarAccess() verifyCalendarAccess()
showSegmentedControl.selectedSegment = DataStore.shared().shouldDisplay(ViewType.upcomingEventView) ? 0 : 1 showSegmentedControl.selectedSegment = DataStore.shared().shouldDisplay(ViewType.upcomingEventView) ? 0 : 1
} }
private func verifyCalendarAccess() { private func verifyCalendarAccess() {
let hasCalendarAccess = EventCenter.sharedCenter().calendarAccessGranted() let hasCalendarAccess = EventCenter.sharedCenter().calendarAccessGranted()
let hasNotDeterminedCalendarAccess = EventCenter.sharedCenter().calendarAccessNotDetermined() let hasNotDeterminedCalendarAccess = EventCenter.sharedCenter().calendarAccessNotDetermined()
let hasDeniedCalendarAccess = EventCenter.sharedCenter().calendarAccessDenied() let hasDeniedCalendarAccess = EventCenter.sharedCenter().calendarAccessDenied()
noAccessView.isHidden = hasCalendarAccess noAccessView.isHidden = hasCalendarAccess
if hasNotDeterminedCalendarAccess { if hasNotDeterminedCalendarAccess {
informationField.stringValue = "Clocker is more useful when it can display events from your calendars.".localized() informationField.stringValue = "Clocker is more useful when it can display events from your calendars.".localized()
setGrantAccess(title: "Grant Access".localized()) setGrantAccess(title: "Grant Access".localized())
@ -99,13 +97,13 @@ class CalendarViewController: ParentViewController {
calendarsTableView.reloadData() calendarsTableView.reloadData()
} }
} }
private func setGrantAccess(title: String) { private func setGrantAccess(title: String) {
let style = NSMutableParagraphStyle() let style = NSMutableParagraphStyle()
style.alignment = .center style.alignment = .center
guard let boldFont = NSFont(name: "Avenir-Medium", size: 14.0) else { return } guard let boldFont = NSFont(name: "Avenir-Medium", size: 14.0) else { return }
let attributesDictionary: [NSAttributedString.Key: Any] = [ let attributesDictionary: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.paragraphStyle: style, NSAttributedString.Key.paragraphStyle: style,
NSAttributedString.Key.font: boldFont, NSAttributedString.Key.font: boldFont,
@ -115,18 +113,18 @@ class CalendarViewController: ParentViewController {
attributes: attributesDictionary) attributes: attributesDictionary)
grantAccessButton.attributedTitle = attributedString grantAccessButton.attributedTitle = attributedString
} }
private func onCalendarAccessDenial() { private func onCalendarAccessDenial() {
informationField.stringValue = """ informationField.stringValue = """
Clocker is more useful when it can display events from your calendars. Clocker is more useful when it can display events from your calendars.
You can change this setting in System Preferences Security & Privacy Privacy. You can change this setting in System Preferences Security & Privacy Privacy.
""".localized() """.localized()
setGrantAccess(title: "Launch Preferences".localized()) setGrantAccess(title: "Launch Preferences".localized())
// Remove upcoming event view if possible // Remove upcoming event view if possible
UserDefaults.standard.set("NO", forKey: UserDefaultKeys.showUpcomingEventView) UserDefaults.standard.set("NO", forKey: UserDefaultKeys.showUpcomingEventView)
} }
@IBAction func grantAccess(_: Any) { @IBAction func grantAccess(_: Any) {
if grantAccessButton.title == "Grant Access".localized() { if grantAccessButton.title == "Grant Access".localized() {
(parent as? CenteredTabViewController)?.selectedTabViewItemIndex = 3 // 3 is the Permissions View (parent as? CenteredTabViewController)?.selectedTabViewItemIndex = 3 // 3 is the Permissions View
@ -134,43 +132,43 @@ class CalendarViewController: ParentViewController {
NSWorkspace.shared.launchApplication("System Preferences") NSWorkspace.shared.launchApplication("System Preferences")
} }
} }
@IBAction func showNextMeetingAction(_ sender: NSSegmentedControl) { @IBAction func showNextMeetingAction(_ sender: NSSegmentedControl) {
// We need to start the menubar timer if it hasn't been started already // We need to start the menubar timer if it hasn't been started already
guard let delegate = NSApplication.shared.delegate as? AppDelegate else { guard let delegate = NSApplication.shared.delegate as? AppDelegate else {
assertionFailure() assertionFailure()
return return
} }
let statusItemHandler = delegate.statusItemForPanel() let statusItemHandler = delegate.statusItemForPanel()
if sender.selectedSegment == 0 { if sender.selectedSegment == 0 {
if let isValid = statusItemHandler.menubarTimer?.isValid, isValid == true { if let isValid = statusItemHandler.menubarTimer?.isValid, isValid == true {
Logger.info("Timer is already in progress") Logger.info("Timer is already in progress")
updateStatusItem() updateStatusItem()
return return
} }
} else { } else {
statusItemHandler.invalidateTimer(showIcon: true, isSyncing: false) statusItemHandler.invalidateTimer(showIcon: true, isSyncing: false)
} }
} }
@IBAction func showUpcomingEventView(_ sender: NSSegmentedControl) { @IBAction func showUpcomingEventView(_ sender: NSSegmentedControl) {
var showUpcomingEventView = "YES" var showUpcomingEventView = "YES"
if sender.selectedSegment == 1 { if sender.selectedSegment == 1 {
showUpcomingEventView = "NO" showUpcomingEventView = "NO"
} }
UserDefaults.standard.set(showUpcomingEventView, forKey: UserDefaultKeys.showUpcomingEventView) UserDefaults.standard.set(showUpcomingEventView, forKey: UserDefaultKeys.showUpcomingEventView)
if DataStore.shared().shouldDisplay(ViewType.showAppInForeground) { if DataStore.shared().shouldDisplay(ViewType.showAppInForeground) {
let floatingWindow = FloatingWindowController.shared() let floatingWindow = FloatingWindowController.shared()
floatingWindow.determineUpcomingViewVisibility() floatingWindow.determineUpcomingViewVisibility()
return return
} }
guard let panel = PanelController.panel() else { return } guard let panel = PanelController.panel() else { return }
if sender.selectedSegment == 1 { if sender.selectedSegment == 1 {
panel.removeUpcomingEventView() panel.removeUpcomingEventView()
@ -180,15 +178,15 @@ class CalendarViewController: ParentViewController {
Logger.log(object: ["Show": "YES"], for: "Upcoming Event View") Logger.log(object: ["Show": "YES"], for: "Upcoming Event View")
} }
} }
private func updateStatusItem() { private func updateStatusItem() {
guard let statusItem = (NSApplication.shared.delegate as? AppDelegate)?.statusItemForPanel() else { guard let statusItem = (NSApplication.shared.delegate as? AppDelegate)?.statusItemForPanel() else {
return return
} }
statusItem.refresh() statusItem.refresh()
} }
@IBOutlet var headerLabel: NSTextField! @IBOutlet var headerLabel: NSTextField!
@IBOutlet var upcomingEventView: NSTextField! @IBOutlet var upcomingEventView: NSTextField!
@IBOutlet var allDayMeetingsLabel: NSTextField! @IBOutlet var allDayMeetingsLabel: NSTextField!
@ -198,7 +196,7 @@ class CalendarViewController: ParentViewController {
@IBOutlet var showEventsFromLabel: NSTextField! @IBOutlet var showEventsFromLabel: NSTextField!
@IBOutlet var charactersField: NSTextField! @IBOutlet var charactersField: NSTextField!
@IBOutlet var truncateAccessoryLabel: NSTextField! @IBOutlet var truncateAccessoryLabel: NSTextField!
private func setup() { private func setup() {
// Grant access button's text color is taken care above. // Grant access button's text color is taken care above.
headerLabel.stringValue = "Upcoming Event View Options".localized() headerLabel.stringValue = "Upcoming Event View Options".localized()
@ -209,11 +207,11 @@ class CalendarViewController: ParentViewController {
charactersField.stringValue = "characters".localized() charactersField.stringValue = "characters".localized()
showEventsFromLabel.stringValue = "Show events from".localized() showEventsFromLabel.stringValue = "Show events from".localized()
truncateAccessoryLabel.stringValue = "If meeting title is \"Meeting with Neel\" and truncate length is set to 5, text in menubar will appear as \"Meeti...\"".localized() truncateAccessoryLabel.stringValue = "If meeting title is \"Meeting with Neel\" and truncate length is set to 5, text in menubar will appear as \"Meeti...\"".localized()
[headerLabel, upcomingEventView, allDayMeetingsLabel, [headerLabel, upcomingEventView, allDayMeetingsLabel,
showNextMeetingLabel, nextMeetingAccessoryLabel, truncateTextLabel, showNextMeetingLabel, nextMeetingAccessoryLabel, truncateTextLabel,
showEventsFromLabel, charactersField, truncateAccessoryLabel].forEach { $0?.textColor = Themer.shared().mainTextColor() } showEventsFromLabel, charactersField, truncateAccessoryLabel].forEach { $0?.textColor = Themer.shared().mainTextColor() }
calendarsTableView.backgroundColor = Themer.shared().mainBackgroundColor() calendarsTableView.backgroundColor = Themer.shared().mainBackgroundColor()
truncateTextField.backgroundColor = Themer.shared().mainBackgroundColor() truncateTextField.backgroundColor = Themer.shared().mainBackgroundColor()
} }
@ -230,15 +228,15 @@ extension CalendarViewController: NSTableViewDelegate {
func tableView(_: NSTableView, shouldSelectRow _: Int) -> Bool { func tableView(_: NSTableView, shouldSelectRow _: Int) -> Bool {
return false return false
} }
func tableView(_: NSTableView, heightOfRow row: Int) -> CGFloat { func tableView(_: NSTableView, heightOfRow row: Int) -> CGFloat {
guard let currentSource = calendars[row] as? String, !currentSource.isEmpty else { guard let currentSource = calendars[row] as? String, !currentSource.isEmpty else {
return 30.0 return 30.0
} }
return 24.0 return 24.0
} }
func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? { func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? {
if let currentSource = calendars[row] as? String, if let currentSource = calendars[row] as? String,
let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "sourceCellView"), owner: self) as? SourceTableViewCell let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "sourceCellView"), owner: self) as? SourceTableViewCell
@ -246,7 +244,7 @@ extension CalendarViewController: NSTableViewDelegate {
message.sourceName.stringValue = currentSource message.sourceName.stringValue = currentSource
return message return message
} }
if let currentSource = calendars[row] as? CalendarInfo, if let currentSource = calendars[row] as? CalendarInfo,
let calendarCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "calendarCellView"), owner: self) as? CalendarTableViewCell let calendarCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "calendarCellView"), owner: self) as? CalendarTableViewCell
{ {
@ -258,36 +256,36 @@ extension CalendarViewController: NSTableViewDelegate {
calendarCell.calendarSelected.action = #selector(calendarSelected(_:)) calendarCell.calendarSelected.action = #selector(calendarSelected(_:))
return calendarCell return calendarCell
} }
return nil return nil
} }
@objc func calendarSelected(_ checkbox: NSButton) { @objc func calendarSelected(_ checkbox: NSButton) {
let currentSelection = checkbox.tag let currentSelection = checkbox.tag
var sourcesAndCalendars = calendars var sourcesAndCalendars = calendars
if var calInfo = sourcesAndCalendars[currentSelection] as? CalendarInfo { if var calInfo = sourcesAndCalendars[currentSelection] as? CalendarInfo {
calInfo.selected = (checkbox.state == .on) calInfo.selected = (checkbox.state == .on)
sourcesAndCalendars[currentSelection] = calInfo sourcesAndCalendars[currentSelection] = calInfo
} }
updateSelectedCalendars(sourcesAndCalendars) updateSelectedCalendars(sourcesAndCalendars)
} }
private func updateSelectedCalendars(_ selection: [Any]) { private func updateSelectedCalendars(_ selection: [Any]) {
var selectedCalendars: [String] = [] var selectedCalendars: [String] = []
for obj in selection { for obj in selection {
if let calInfo = obj as? CalendarInfo, calInfo.selected { if let calInfo = obj as? CalendarInfo, calInfo.selected {
selectedCalendars.append(calInfo.calendar.calendarIdentifier) selectedCalendars.append(calInfo.calendar.calendarIdentifier)
} }
} }
UserDefaults.standard.set(selectedCalendars, forKey: UserDefaultKeys.selectedCalendars) UserDefaults.standard.set(selectedCalendars, forKey: UserDefaultKeys.selectedCalendars)
calendars = EventCenter.sharedCenter().fetchSourcesAndCalendars() calendars = EventCenter.sharedCenter().fetchSourcesAndCalendars()
EventCenter.sharedCenter().filterEvents() EventCenter.sharedCenter().filterEvents()
} }
} }

504
Clocker/Preferences/General/PreferencesViewController.swift

File diff suppressed because it is too large Load Diff

178
Clocker/Preferences/Menu Bar/StatusItemHandler.swift

@ -12,28 +12,28 @@ private enum MenubarState {
class StatusItemHandler: NSObject { class StatusItemHandler: NSObject {
var hasActiveIcon: Bool = false var hasActiveIcon: Bool = false
var menubarTimer: Timer? var menubarTimer: Timer?
var statusItem: NSStatusItem = { var statusItem: NSStatusItem = {
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.toolTip = "Clocker" statusItem.button?.toolTip = "Clocker"
(statusItem.button?.cell as? NSButtonCell)?.highlightsBy = NSCell.StyleMask(rawValue: 0) (statusItem.button?.cell as? NSButtonCell)?.highlightsBy = NSCell.StyleMask(rawValue: 0)
return statusItem return statusItem
}() }()
private lazy var menubarTitleHandler = MenubarTitleProvider(with: self.store, eventStore: EventCenter.sharedCenter()) private lazy var menubarTitleHandler = MenubarTitleProvider(with: self.store, eventStore: EventCenter.sharedCenter())
private var statusContainerView: StatusContainerView? private var statusContainerView: StatusContainerView?
private var nsCalendar = Calendar.autoupdatingCurrent private var nsCalendar = Calendar.autoupdatingCurrent
private lazy var units: Set<Calendar.Component> = Set([.era, .year, .month, .day, .hour, .minute]) private lazy var units: Set<Calendar.Component> = Set([.era, .year, .month, .day, .hour, .minute])
private var userNotificationsDidChangeNotif: NSObjectProtocol? private var userNotificationsDidChangeNotif: NSObjectProtocol?
private let store: DataStore private let store: DataStore
// Current State might be set twice when the user first launches an app. // Current State might be set twice when the user first launches an app.
// First, when StatusItemHandler() is instantiated in AppDelegate // First, when StatusItemHandler() is instantiated in AppDelegate
// Second, when AppDelegate.fetchLocalTimezone() is called triggering a customLabel didSet. // Second, when AppDelegate.fetchLocalTimezone() is called triggering a customLabel didSet.
@ -50,7 +50,7 @@ class StatusItemHandler: NSObject {
case .icon: case .icon:
statusItem.button?.image = nil statusItem.button?.image = nil
} }
// Now setup for the new menubar state // Now setup for the new menubar state
switch currentState { switch currentState {
case .compactText: case .compactText:
@ -60,25 +60,25 @@ class StatusItemHandler: NSObject {
case .icon: case .icon:
setClockerIcon() setClockerIcon()
} }
Logger.info("Status Bar Current State changed: \(currentState)\n") Logger.info("Status Bar Current State changed: \(currentState)\n")
} }
} }
init(with dataStore: DataStore) { init(with dataStore: DataStore) {
store = dataStore store = dataStore
super.init() super.init()
setupStatusItem() setupStatusItem()
setupNotificationObservers() setupNotificationObservers()
} }
func setupStatusItem() { func setupStatusItem() {
// Let's figure out the initial menubar state // Let's figure out the initial menubar state
var menubarState = MenubarState.icon var menubarState = MenubarState.icon
let shouldTextBeDisplayed = store.menubarTimezones()?.isEmpty ?? true let shouldTextBeDisplayed = store.menubarTimezones()?.isEmpty ?? true
if !shouldTextBeDisplayed || store.shouldDisplay(.showMeetingInMenubar) { if !shouldTextBeDisplayed || store.shouldDisplay(.showMeetingInMenubar) {
if store.shouldDisplay(.menubarCompactMode) { if store.shouldDisplay(.menubarCompactMode) {
menubarState = .compactText menubarState = .compactText
@ -86,68 +86,64 @@ class StatusItemHandler: NSObject {
menubarState = .standardText menubarState = .standardText
} }
} }
// Initial state has been figured out. Time to set it! // Initial state has been figured out. Time to set it!
currentState = menubarState currentState = menubarState
func setSelector() { func setSelector() {
if #available(macOS 10.14, *) { statusItem.button?.action = #selector(menubarIconClicked(_:))
statusItem.button?.action = #selector(menubarIconClicked(_:))
} else {
statusItem.action = #selector(menubarIconClicked(_:))
}
} }
statusItem.button?.target = self statusItem.button?.target = self
statusItem.autosaveName = NSStatusItem.AutosaveName("ClockerStatusItem") statusItem.autosaveName = NSStatusItem.AutosaveName("ClockerStatusItem")
setSelector() setSelector()
} }
private func setupNotificationObservers() { private func setupNotificationObservers() {
let center = NotificationCenter.default let center = NotificationCenter.default
let mainQueue = OperationQueue.main let mainQueue = OperationQueue.main
center.addObserver(self, center.addObserver(self,
selector: #selector(updateMenubar), selector: #selector(updateMenubar),
name: NSWorkspace.didWakeNotification, name: NSWorkspace.didWakeNotification,
object: nil) object: nil)
DistributedNotificationCenter.default.addObserver(self, selector: #selector(respondToInterfaceStyleChange), DistributedNotificationCenter.default.addObserver(self, selector: #selector(respondToInterfaceStyleChange),
name: .interfaceStyleDidChange, name: .interfaceStyleDidChange,
object: nil) object: nil)
userNotificationsDidChangeNotif = center.addObserver(forName: UserDefaults.didChangeNotification, userNotificationsDidChangeNotif = center.addObserver(forName: UserDefaults.didChangeNotification,
object: self, object: self,
queue: mainQueue) queue: mainQueue)
{ _ in { _ in
self.setupStatusItem() self.setupStatusItem()
} }
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: OperationQueue.main) { _ in NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: OperationQueue.main) { _ in
self.menubarTimer?.invalidate() self.menubarTimer?.invalidate()
} }
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: OperationQueue.main) { _ in NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: OperationQueue.main) { _ in
self.setupStatusItem() self.setupStatusItem()
} }
} }
deinit { deinit {
if let userNotifsDidChange = userNotificationsDidChangeNotif { if let userNotifsDidChange = userNotificationsDidChangeNotif {
NotificationCenter.default.removeObserver(userNotifsDidChange) NotificationCenter.default.removeObserver(userNotifsDidChange)
} }
} }
private func constructCompactView(with upcomingEventView: Bool = false) { private func constructCompactView(with upcomingEventView: Bool = false) {
statusItem.button?.subviews = [] statusItem.button?.subviews = []
statusContainerView = nil statusContainerView = nil
let menubarTimezones = store.menubarTimezones() ?? [] let menubarTimezones = store.menubarTimezones() ?? []
if menubarTimezones.isEmpty { if menubarTimezones.isEmpty {
currentState = .icon currentState = .icon
return return
} }
statusContainerView = StatusContainerView(with: menubarTimezones, statusContainerView = StatusContainerView(with: menubarTimezones,
store: store, store: store,
showUpcomingEventView: upcomingEventView, showUpcomingEventView: upcomingEventView,
@ -155,7 +151,7 @@ class StatusItemHandler: NSObject {
statusContainerView?.wantsLayer = true statusContainerView?.wantsLayer = true
statusItem.button?.addSubview(statusContainerView!) statusItem.button?.addSubview(statusContainerView!)
statusItem.button?.frame = statusContainerView!.bounds statusItem.button?.frame = statusContainerView!.bounds
// For OS < 11, we need to fix the sizing (width) on the button's window // For OS < 11, we need to fix the sizing (width) on the button's window
// Otherwise, we won't be able to see the menu bar option at all. // Otherwise, we won't be able to see the menu bar option at all.
if let window = statusItem.button?.window { if let window = statusItem.button?.window {
@ -168,7 +164,7 @@ class StatusItemHandler: NSObject {
} }
statusItem.button?.subviews.first?.window?.backgroundColor = NSColor.clear statusItem.button?.subviews.first?.window?.backgroundColor = NSColor.clear
} }
// This is called when the Apple interface style pre-Mojave is changed. // This is called when the Apple interface style pre-Mojave is changed.
// In High Sierra and before, we could have a dark or light menubar and dock // In High Sierra and before, we could have a dark or light menubar and dock
// Our icon is template, so it changes automatically; so is our standard status bar text // Our icon is template, so it changes automatically; so is our standard status bar text
@ -178,68 +174,68 @@ class StatusItemHandler: NSObject {
updateCompactMenubar() updateCompactMenubar()
} }
} }
@objc func setHasActiveIcon(_ value: Bool) { @objc func setHasActiveIcon(_ value: Bool) {
hasActiveIcon = value hasActiveIcon = value
} }
@objc func menubarIconClicked(_ sender: NSStatusBarButton) { @objc func menubarIconClicked(_ sender: NSStatusBarButton) {
guard let mainDelegate = NSApplication.shared.delegate as? AppDelegate else { guard let mainDelegate = NSApplication.shared.delegate as? AppDelegate else {
return return
} }
mainDelegate.togglePanel(sender) mainDelegate.togglePanel(sender)
} }
@objc func updateMenubar() { @objc func updateMenubar() {
guard let fireDate = calculateFireDate() else { return } guard let fireDate = calculateFireDate() else { return }
let shouldDisplaySeconds = shouldDisplaySecondsInMenubar() let shouldDisplaySeconds = shouldDisplaySecondsInMenubar()
menubarTimer = Timer(fire: fireDate, menubarTimer = Timer(fire: fireDate,
interval: 0, interval: 0,
repeats: false, repeats: false,
block: { [weak self] _ in block: { [weak self] _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.refresh() strongSelf.refresh()
} }
}) })
// Tolerance, even a small amount, has a positive imapct on the power usage. As a rule, we set it to 10% of the interval // Tolerance, even a small amount, has a positive imapct on the power usage. As a rule, we set it to 10% of the interval
menubarTimer?.tolerance = shouldDisplaySeconds ? 0.5 : 20 menubarTimer?.tolerance = shouldDisplaySeconds ? 0.5 : 20
guard let runLoopTimer = menubarTimer else { guard let runLoopTimer = menubarTimer else {
Logger.info("Timer is unexpectedly nil") Logger.info("Timer is unexpectedly nil")
return return
} }
RunLoop.main.add(runLoopTimer, forMode: .common) RunLoop.main.add(runLoopTimer, forMode: .common)
} }
private func shouldDisplaySecondsInMenubar() -> Bool { private func shouldDisplaySecondsInMenubar() -> Bool {
let syncedTimezones = store.menubarTimezones() ?? [] let syncedTimezones = store.menubarTimezones() ?? []
let timezonesSupportingSeconds = syncedTimezones.filter { data in let timezonesSupportingSeconds = syncedTimezones.filter { data in
if let timezoneObj = TimezoneData.customObject(from: data) { if let timezoneObj = TimezoneData.customObject(from: data) {
return timezoneObj.shouldShowSeconds(store.timezoneFormat()) return timezoneObj.shouldShowSeconds(store.timezoneFormat())
} }
return false return false
} }
return timezonesSupportingSeconds.isEmpty == false return timezonesSupportingSeconds.isEmpty == false
} }
private func calculateFireDate() -> Date? { private func calculateFireDate() -> Date? {
let shouldDisplaySeconds = shouldDisplaySecondsInMenubar() let shouldDisplaySeconds = shouldDisplaySecondsInMenubar()
let menubarFavourites = store.menubarTimezones() let menubarFavourites = store.menubarTimezones()
if !units.contains(.second), shouldDisplaySeconds { if !units.contains(.second), shouldDisplaySeconds {
units.insert(.second) units.insert(.second)
} }
var components = nsCalendar.dateComponents(units, from: Date()) var components = nsCalendar.dateComponents(units, from: Date())
// We want to update every second only when there's a timezone present! // We want to update every second only when there's a timezone present!
if shouldDisplaySeconds, let seconds = components.second, let favourites = menubarFavourites, !favourites.isEmpty { if shouldDisplaySeconds, let seconds = components.second, let favourites = menubarFavourites, !favourites.isEmpty {
components.second = seconds + 1 components.second = seconds + 1
@ -249,15 +245,15 @@ class StatusItemHandler: NSObject {
Logger.info("Unable to create date components for the menubar timewr") Logger.info("Unable to create date components for the menubar timewr")
return nil return nil
} }
guard let fireDate = nsCalendar.date(from: components) else { guard let fireDate = nsCalendar.date(from: components) else {
Logger.info("Unable to form Fire Date") Logger.info("Unable to form Fire Date")
return nil return nil
} }
return fireDate return fireDate
} }
func updateCompactMenubar() { func updateCompactMenubar() {
let filteredEvents = EventCenter.sharedCenter().filteredEvents let filteredEvents = EventCenter.sharedCenter().filteredEvents
let calendar = EventCenter.sharedCenter().autoupdatingCalendar let calendar = EventCenter.sharedCenter().autoupdatingCalendar
@ -270,7 +266,7 @@ class StatusItemHandler: NSObject {
constructCompactView(with: true) constructCompactView(with: true)
} }
} }
if let upcomingEventView = retrieveUpcomingEventStatusView(), upcomingEvent == nil { if let upcomingEventView = retrieveUpcomingEventStatusView(), upcomingEvent == nil {
upcomingEventView.removeFromSuperview() upcomingEventView.removeFromSuperview()
constructCompactView() // So that Status Container View reclaims the space constructCompactView() // So that Status Container View reclaims the space
@ -278,7 +274,7 @@ class StatusItemHandler: NSObject {
// This will internally call `statusItemViewSetNeedsDisplay` on all subviews ensuring all text in the menubar is up-to-date. // This will internally call `statusItemViewSetNeedsDisplay` on all subviews ensuring all text in the menubar is up-to-date.
statusContainerView?.updateTime() statusContainerView?.updateTime()
} }
private func removeUpcomingStatusItemView() { private func removeUpcomingStatusItemView() {
NSAnimationContext.runAnimationGroup({ context in NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.2 context.duration = 0.2
@ -290,7 +286,7 @@ class StatusItemHandler: NSObject {
} }
} }
} }
func refresh() { func refresh() {
if currentState == .compactText { if currentState == .compactText {
updateCompactMenubar() updateCompactMenubar()
@ -308,69 +304,69 @@ class StatusItemHandler: NSObject {
menubarTimer?.invalidate() menubarTimer?.invalidate()
} }
} }
private func setupForStandardTextMode() { private func setupForStandardTextMode() {
Logger.info("Initializing menubar timer") Logger.info("Initializing menubar timer")
// Let's invalidate the previous timer // Let's invalidate the previous timer
menubarTimer?.invalidate() menubarTimer?.invalidate()
menubarTimer = nil menubarTimer = nil
setupForStandardText() setupForStandardText()
updateMenubar() updateMenubar()
} }
func invalidateTimer(showIcon show: Bool, isSyncing sync: Bool) { func invalidateTimer(showIcon show: Bool, isSyncing sync: Bool) {
// Check if user is not showing // Check if user is not showing
// 1. Timezones // 1. Timezones
// 2. Upcoming Event // 2. Upcoming Event
let menubarFavourites = store.menubarTimezones() ?? [] let menubarFavourites = store.menubarTimezones() ?? []
if menubarFavourites.isEmpty, store.shouldDisplay(.showMeetingInMenubar) == false { if menubarFavourites.isEmpty, store.shouldDisplay(.showMeetingInMenubar) == false {
Logger.info("Invalidating menubar timer!") Logger.info("Invalidating menubar timer!")
invalidation() invalidation()
if show { if show {
currentState = .icon currentState = .icon
} }
} else if sync { } else if sync {
Logger.info("Invalidating menubar timer for sync purposes!") Logger.info("Invalidating menubar timer for sync purposes!")
invalidation() invalidation()
if show { if show {
setClockerIcon() setClockerIcon()
} }
} else { } else {
Logger.info("Not stopping menubar timer!") Logger.info("Not stopping menubar timer!")
} }
} }
private func invalidation() { private func invalidation() {
menubarTimer?.invalidate() menubarTimer?.invalidate()
} }
private func setClockerIcon() { private func setClockerIcon() {
if statusItem.button?.subviews.isEmpty == false { if statusItem.button?.subviews.isEmpty == false {
statusItem.button?.subviews = [] statusItem.button?.subviews = []
} }
if statusItem.button?.image?.name() == NSImage.Name.menubarIcon { if statusItem.button?.image?.name() == NSImage.Name.menubarIcon {
return return
} }
statusItem.button?.title = UserDefaultKeys.emptyString statusItem.button?.title = UserDefaultKeys.emptyString
statusItem.button?.image = NSImage(named: .menubarIcon) statusItem.button?.image = NSImage(named: .menubarIcon)
statusItem.button?.imagePosition = .imageOnly statusItem.button?.imagePosition = .imageOnly
statusItem.button?.toolTip = "Clocker" statusItem.button?.toolTip = "Clocker"
} }
private func setupForStandardText() { private func setupForStandardText() {
var menubarText = UserDefaultKeys.emptyString var menubarText = UserDefaultKeys.emptyString
if let menubarTitle = menubarTitleHandler.titleForMenubar() { if let menubarTitle = menubarTitleHandler.titleForMenubar() {
menubarText = menubarTitle menubarText = menubarTitle
} else if store.shouldDisplay(.showMeetingInMenubar) { } else if store.shouldDisplay(.showMeetingInMenubar) {
@ -379,31 +375,31 @@ class StatusItemHandler: NSObject {
// We have no favourites to display and no meetings to show. // We have no favourites to display and no meetings to show.
// That means we should display our icon! // That means we should display our icon!
} }
guard !menubarText.isEmpty else { guard !menubarText.isEmpty else {
setClockerIcon() setClockerIcon()
return return
} }
let attributes = [NSAttributedString.Key.font: NSFont.monospacedDigitSystemFont(ofSize: 13.0, weight: NSFont.Weight.regular), let attributes = [NSAttributedString.Key.font: NSFont.monospacedDigitSystemFont(ofSize: 13.0, weight: NSFont.Weight.regular),
NSAttributedString.Key.baselineOffset: 0.1] as [NSAttributedString.Key: Any] NSAttributedString.Key.baselineOffset: 0.1] as [NSAttributedString.Key: Any]
statusItem.button?.attributedTitle = NSAttributedString(string: menubarText, attributes: attributes) statusItem.button?.attributedTitle = NSAttributedString(string: menubarText, attributes: attributes)
statusItem.button?.image = nil statusItem.button?.image = nil
statusItem.button?.imagePosition = .imageLeft statusItem.button?.imagePosition = .imageLeft
} }
private func setupForCompactTextMode() { private func setupForCompactTextMode() {
// Let's invalidate the previous timer // Let's invalidate the previous timer
menubarTimer?.invalidate() menubarTimer?.invalidate()
menubarTimer = nil menubarTimer = nil
let filteredEvents = EventCenter.sharedCenter().filteredEvents let filteredEvents = EventCenter.sharedCenter().filteredEvents
let calendar = EventCenter.sharedCenter().autoupdatingCalendar let calendar = EventCenter.sharedCenter().autoupdatingCalendar
let checkForUpcomingEvents = menubarTitleHandler.checkForUpcomingEvents(filteredEvents, calendar: calendar) let checkForUpcomingEvents = menubarTitleHandler.checkForUpcomingEvents(filteredEvents, calendar: calendar)
constructCompactView(with: checkForUpcomingEvents != nil) constructCompactView(with: checkForUpcomingEvents != nil)
updateMenubar() updateMenubar()
} }
private func retrieveUpcomingEventStatusView() -> NSView? { private func retrieveUpcomingEventStatusView() -> NSView? {
let upcomingEventView = statusContainerView?.subviews.first(where: { statusItemView in let upcomingEventView = statusContainerView?.subviews.first(where: { statusItemView in
if let upcomingEventView = statusItemView as? StatusItemViewConforming { if let upcomingEventView = statusItemView as? StatusItemViewConforming {
@ -413,26 +409,26 @@ class StatusItemHandler: NSObject {
}) })
return upcomingEventView return upcomingEventView
} }
private func bufferCalculatedWidth() -> Int { private func bufferCalculatedWidth() -> Int {
var totalWidth = 55 var totalWidth = 55
if store.shouldShowDayInMenubar() { if store.shouldShowDayInMenubar() {
totalWidth += 12 totalWidth += 12
} }
if store.isBufferRequiredForTwelveHourFormats() { if store.isBufferRequiredForTwelveHourFormats() {
totalWidth += 20 totalWidth += 20
} }
if store.shouldShowDateInMenubar() { if store.shouldShowDateInMenubar() {
totalWidth += 20 totalWidth += 20
} }
if store.shouldDisplay(.showMeetingInMenubar) { if store.shouldDisplay(.showMeetingInMenubar) {
totalWidth += 100 totalWidth += 100
} }
return totalWidth return totalWidth
} }
} }

51
Clocker/Preferences/Menu Bar/StatusItemView.swift

@ -27,27 +27,18 @@ var compactModeTimeFont: NSFont {
extension NSView { extension NSView {
var hasDarkAppearance: Bool { var hasDarkAppearance: Bool {
if #available(OSX 10.14, *) { switch effectiveAppearance.name {
switch effectiveAppearance.name { case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: return true
return true default:
default: return false
return false
}
} else {
switch effectiveAppearance.name {
case .vibrantDark:
return true
default:
return false
}
} }
} }
} }
class StatusItemView: NSView { class StatusItemView: NSView {
// MARK: Private variables // MARK: Private variables
private let locationView = NSTextField(labelWithString: "Hello") private let locationView = NSTextField(labelWithString: "Hello")
private let timeView = NSTextField(labelWithString: "Mon 19:14 PM") private let timeView = NSTextField(labelWithString: "Mon 19:14 PM")
private var operationsObject: TimezoneDataOperations { private var operationsObject: TimezoneDataOperations {
@ -63,10 +54,10 @@ class StatusItemView: NSView {
paragraphStyle.lineHeightMultiple = CGFloat(lineHeight) paragraphStyle.lineHeightMultiple = CGFloat(lineHeight)
return paragraphStyle return paragraphStyle
}() }()
private var timeAttributes: [NSAttributedString.Key: AnyObject] { private var timeAttributes: [NSAttributedString.Key: AnyObject] {
let textColor = hasDarkAppearance ? NSColor.white : NSColor.black let textColor = hasDarkAppearance ? NSColor.white : NSColor.black
let attributes = [ let attributes = [
NSAttributedString.Key.font: compactModeTimeFont, NSAttributedString.Key.font: compactModeTimeFont,
NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.foregroundColor: textColor,
@ -75,10 +66,10 @@ class StatusItemView: NSView {
] ]
return attributes return attributes
} }
private var textFontAttributes: [NSAttributedString.Key: Any] { private var textFontAttributes: [NSAttributedString.Key: Any] {
let textColor = hasDarkAppearance ? NSColor.white : NSColor.black let textColor = hasDarkAppearance ? NSColor.white : NSColor.black
let textFontAttributes = [ let textFontAttributes = [
NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 10),
NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.foregroundColor: textColor,
@ -87,39 +78,39 @@ class StatusItemView: NSView {
] ]
return textFontAttributes return textFontAttributes
} }
// MARK: Public // MARK: Public
var dataObject: TimezoneData! { var dataObject: TimezoneData! {
didSet { didSet {
initialSetup() initialSetup()
} }
} }
override init(frame frameRect: NSRect) { override init(frame frameRect: NSRect) {
super.init(frame: frameRect) super.init(frame: frameRect)
[timeView, locationView].forEach { [timeView, locationView].forEach {
$0.wantsLayer = true $0.wantsLayer = true
$0.applyDefaultStyle() $0.applyDefaultStyle()
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0) addSubview($0)
} }
timeView.disableWrapping() timeView.disableWrapping()
var topAnchorConstant: CGFloat = 7.0 var topAnchorConstant: CGFloat = 7.0
if #available(macOS 11.0, *) { if #available(macOS 11.0, *) {
topAnchorConstant = 0.0 topAnchorConstant = 0.0
} }
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
locationView.leadingAnchor.constraint(equalTo: leadingAnchor), locationView.leadingAnchor.constraint(equalTo: leadingAnchor),
locationView.trailingAnchor.constraint(equalTo: trailingAnchor), locationView.trailingAnchor.constraint(equalTo: trailingAnchor),
locationView.topAnchor.constraint(equalTo: topAnchor, constant: topAnchorConstant), locationView.topAnchor.constraint(equalTo: topAnchor, constant: topAnchorConstant),
locationView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.35), locationView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.35),
]) ])
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
timeView.leadingAnchor.constraint(equalTo: leadingAnchor), timeView.leadingAnchor.constraint(equalTo: leadingAnchor),
timeView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0), timeView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
@ -127,18 +118,18 @@ class StatusItemView: NSView {
timeView.bottomAnchor.constraint(equalTo: bottomAnchor), timeView.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
} }
@available(OSX 10.14, *) @available(OSX 10.14, *)
override func viewDidChangeEffectiveAppearance() { override func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance() super.viewDidChangeEffectiveAppearance()
statusItemViewSetNeedsDisplay() statusItemViewSetNeedsDisplay()
} }
private func initialSetup() { private func initialSetup() {
locationView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuTitle(), attributes: textFontAttributes) locationView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuTitle(), attributes: textFontAttributes)
timeView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuSubtitle(), attributes: timeAttributes) timeView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuSubtitle(), attributes: timeAttributes)
} }
@available(*, unavailable) @available(*, unavailable)
required init?(coder _: NSCoder) { required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
@ -150,7 +141,7 @@ extension StatusItemView: StatusItemViewConforming {
locationView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuTitle(), attributes: textFontAttributes) locationView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuTitle(), attributes: textFontAttributes)
timeView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuSubtitle(), attributes: timeAttributes) timeView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuSubtitle(), attributes: timeAttributes)
} }
func statusItemViewIdentifier() -> String { func statusItemViewIdentifier() -> String {
return "location_view" return "location_view"
} }

Loading…
Cancel
Save