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
isBordered = false
allowsDefaultTighteningForTruncation = true
if #available(OSX 10.12.2, *) {
isAutomaticTextCompletionEnabled = false
allowsCharacterPickerTouchBarItem = false
}
isAutomaticTextCompletionEnabled = false
allowsCharacterPickerTouchBarItem = false
}
func disableWrapping() {

7
Clocker/Overall App/Foundation + Additions.swift

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

297
Clocker/Overall App/Themer.swift

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

222
Clocker/Panel/PanelController.swift

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

40
Clocker/Panel/ParentPanelController+ModernSlider.swift

@ -10,7 +10,7 @@ extension ParentPanelController: NSCollectionViewDataSource {
let futureSliderDayRange = (futureSliderDayPreference.intValue + 1)
return (PanelConstants.modernSliderPointsInADay * futureSliderDayRange * 2) + 1
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
guard let item = collectionView.makeItem(withIdentifier: TimeMarkerViewItem.reuseIdentifier, for: indexPath) as? TimeMarkerViewItem else {
return NSCollectionViewItem()
@ -36,16 +36,16 @@ extension ParentPanelController {
if let scrollView = modernSlider.superview?.superview as? NSScrollView {
scrollView.scrollerStyle = NSScroller.Style.overlay
}
goBackwardsButton.image = Themer.shared().goBackwardsImage()
goForwardButton.image = Themer.shared().goForwardsImage()
goForwardButton.isContinuous = true
goBackwardsButton.isContinuous = true
goBackwardsButton.toolTip = "Navigate 15 mins back"
goForwardButton.toolTip = "Navigate 15 mins forward"
modernSlider.wantsLayer = true // Required for animating reset to center
modernSlider.enclosingScrollView?.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
modernSlider.enclosingScrollView?.backgroundColor = NSColor.clear
@ -55,27 +55,27 @@ extension ParentPanelController {
selector: #selector(collectionViewDidScroll(_:)),
name: NSView.boundsDidChangeNotification,
object: modernSlider.superview)
// Set the modern slider label!
closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation()
if let unwrappedClosetQuarterTime = closestQuarterTimeRepresentation {
modernSliderLabel.stringValue = timezoneFormattedStringRepresentation(unwrappedClosetQuarterTime)
}
// Make sure modern slider is centered horizontally!
let indexPaths: Set<IndexPath> = Set([IndexPath(item: modernSlider.numberOfItems(inSection: 0) / 2, section: 0)])
modernSlider.scrollToItems(at: indexPaths, scrollPosition: .centeredHorizontally)
}
}
@IBAction func goForward(_: NSButton) {
navigateModernSliderToSpecificIndex(1)
}
@IBAction func goBackward(_: NSButton) {
navigateModernSliderToSpecificIndex(-1)
}
private func animateButton(_ hidden: Bool) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
@ -95,7 +95,7 @@ extension ParentPanelController {
goBackwardsButton.animator().alphaValue = hide ? 0.0 : 1.0
}, completionHandler: nil)
}
@IBAction func resetModernSlider(_: NSButton) {
closestQuarterTimeRepresentation = findClosestQuarterTimeApproximation()
modernSliderLabel.stringValue = "Time Scroller"
@ -112,7 +112,7 @@ extension ParentPanelController {
})
}
}
private func navigateModernSliderToSpecificIndex(_ index: Int) {
guard let contentView = modernSlider.superview as? NSClipView else {
return
@ -124,12 +124,12 @@ extension ParentPanelController {
modernSlider.scrollToItems(at: Set([previousIndexPath]), scrollPosition: .centeredHorizontally)
}
}
@objc func collectionViewDidScroll(_ notification: NSNotification) {
guard let contentView = notification.object as? NSClipView else {
return
}
let changedOrigin = contentView.documentVisibleRect.origin
let newPoint = NSPoint(x: changedOrigin.x + contentView.frame.width / 2, y: changedOrigin.y)
let indexPath = modernSlider.indexPathForItem(at: newPoint)
@ -141,7 +141,7 @@ extension ParentPanelController {
mainTableView.reloadData()
}
}
public func findClosestQuarterTimeApproximation() -> Date {
let defaultParameters = minuteFromCalendar()
let hourQuarterDate = Calendar.current.nextDate(after: defaultParameters.0,
@ -159,7 +159,7 @@ extension ParentPanelController {
}
return (minutes / 60, minutesRemaining)
}
public func setDefaultDateLabel(_ index: Int) -> Int {
let futureSliderDayPreference = DataStore.shared().retrieve(key: UserDefaultKeys.futureSliderRange) as? NSNumber ?? 5
let futureSliderDayRange = (futureSliderDayPreference.intValue + 1)
@ -173,7 +173,7 @@ extension ParentPanelController {
if resetModernSliderButton.isHidden {
animateButton(false)
}
return nextDate.minutes(from: Date()) + 1
} else if index < centerPoint {
let remainder = centerPoint - index + 1
@ -193,7 +193,7 @@ extension ParentPanelController {
return 0
}
}
private func minuteFromCalendar() -> (Date, Int) {
let currentDate = Date()
var minute = Calendar.current.component(.minute, from: currentDate)
@ -206,10 +206,10 @@ extension ParentPanelController {
} else {
minute = 0
}
return (currentDate, minute)
}
private func timezoneFormattedStringRepresentation(_ date: Date) -> String {
let dateFormatter = DateFormatterManager.dateFormatterWithFormat(with: .none,
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 {
private static var storage = UserDefaults.standard
private static var debugging = false
private enum Keys {
static let lastPrompt = "last-prompt"
static let lastVersion = "last-version"
static let install = "install"
}
class func applicationDidLaunch(_ defaults: UserDefaults) {
if ProcessInfo.processInfo.arguments.contains(UserDefaultKeys.testingLaunchArgument) {
debugging = true
}
storage = defaults
if defaults.object(forKey: Keys.install) == nil {
defaults.set(Date(), forKey: Keys.install)
}
}
class func setPreviewMode(_ value: Bool) {
debugging = value
}
class func prompted() {
storage.set(Date(), forKey: Keys.lastPrompt)
storage.set(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String, forKey: Keys.lastVersion)
}
class func canPrompt() -> Bool {
if debugging == true {
return true
}
let day: TimeInterval = -1 * 60 * 60 * 24
let minInstall: TimeInterval = day * 7
// Check if the app has been installed for atleast 7 days
guard let install = storage.object(forKey: Keys.install) as? Date,
install.timeIntervalSinceNow < minInstall
else { return false }
// If we have never been prompted before, go ahead and prompt
guard let lastPrompt = storage.object(forKey: Keys.lastPrompt) as? Date,
let lastVersion = storage.object(forKey: Keys.lastVersion) as? String
else { return true }
// Minimum interval between two versions should be 3 months
let minInterval: TimeInterval = day * 90
// never prompt w/in the same version
return lastVersion != (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String)
// limit all types of prompts to at least 1mo intervals
&& lastPrompt.timeIntervalSinceNow < minInterval
// limit all types of prompts to at least 1mo intervals
&& lastPrompt.timeIntervalSinceNow < minInterval
}
class func prompt() {
if #available(OSX 10.14, *) {
SKStoreReviewController.requestReview()
} else {
guard let ratingsURL = URL(string: AboutUsConstants.AppStoreLink) else {
return
}
NSWorkspace.shared.open(ratingsURL)
}
SKStoreReviewController.requestReview()
prompted()
}
}

114
Clocker/Panel/UI/TimezoneDataSource.swift

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

4
Clocker/Preferences/Appearance/AppearanceViewController.swift

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

110
Clocker/Preferences/Calendar/CalendarViewController.swift

@ -9,18 +9,18 @@ class ClockerTextBackgroundView: NSView {
wantsLayer = true
layer?.cornerRadius = 8.0
layer?.masksToBounds = false
NotificationCenter.default.addObserver(self,
selector: #selector(updateBackgroundColor),
name: .themeDidChangeNotification,
object: nil)
}
override func updateLayer() {
super.updateLayer()
layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor
}
@objc func updateBackgroundColor() {
layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor
}
@ -34,61 +34,59 @@ class CalendarViewController: ParentViewController {
@IBOutlet var informationField: NSTextField!
@IBOutlet var grantAccessButton: NSButton!
@IBOutlet var calendarsTableView: NSTableView!
@IBOutlet var showNextMeetingInMenubarControl: NSSegmentedControl!
@IBOutlet var backgroundView: NSView!
@IBOutlet var nextMeetingBackgroundView: NSView!
private var themeDidChangeNotification: NSObjectProtocol?
private lazy var calendars: [Any] = EventCenter.sharedCenter().fetchSourcesAndCalendars()
override func viewDidLoad() {
super.viewDidLoad()
setup()
NotificationCenter.default.addObserver(self,
selector: #selector(calendarAccessStatusChanged),
name: .calendarAccessGranted,
object: nil)
themeDidChangeNotification = NotificationCenter.default.addObserver(forName: .themeDidChangeNotification, object: nil, queue: OperationQueue.main) { _ in
self.setup()
}
if #available(macOS 10.14, *) {
noAccessView.material = .contentBackground
}
noAccessView.material = .contentBackground
upcomingEventView.setAccessibility("UpcomingEventView")
}
deinit {
if let themeDidChangeNotif = themeDidChangeNotification {
NotificationCenter.default.removeObserver(themeDidChangeNotif)
}
}
@objc func calendarAccessStatusChanged() {
verifyCalendarAccess()
view.window?.windowController?.showWindow(nil)
view.window?.makeKeyAndOrderFront(nil)
}
override func viewWillAppear() {
super.viewWillAppear()
verifyCalendarAccess()
showSegmentedControl.selectedSegment = DataStore.shared().shouldDisplay(ViewType.upcomingEventView) ? 0 : 1
}
private func verifyCalendarAccess() {
let hasCalendarAccess = EventCenter.sharedCenter().calendarAccessGranted()
let hasNotDeterminedCalendarAccess = EventCenter.sharedCenter().calendarAccessNotDetermined()
let hasDeniedCalendarAccess = EventCenter.sharedCenter().calendarAccessDenied()
noAccessView.isHidden = hasCalendarAccess
if hasNotDeterminedCalendarAccess {
informationField.stringValue = "Clocker is more useful when it can display events from your calendars.".localized()
setGrantAccess(title: "Grant Access".localized())
@ -99,13 +97,13 @@ class CalendarViewController: ParentViewController {
calendarsTableView.reloadData()
}
}
private func setGrantAccess(title: String) {
let style = NSMutableParagraphStyle()
style.alignment = .center
guard let boldFont = NSFont(name: "Avenir-Medium", size: 14.0) else { return }
let attributesDictionary: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.paragraphStyle: style,
NSAttributedString.Key.font: boldFont,
@ -115,18 +113,18 @@ class CalendarViewController: ParentViewController {
attributes: attributesDictionary)
grantAccessButton.attributedTitle = attributedString
}
private func onCalendarAccessDenial() {
informationField.stringValue = """
Clocker is more useful when it can display events from your calendars.
You can change this setting in System Preferences Security & Privacy Privacy.
""".localized()
setGrantAccess(title: "Launch Preferences".localized())
// Remove upcoming event view if possible
UserDefaults.standard.set("NO", forKey: UserDefaultKeys.showUpcomingEventView)
}
@IBAction func grantAccess(_: Any) {
if grantAccessButton.title == "Grant Access".localized() {
(parent as? CenteredTabViewController)?.selectedTabViewItemIndex = 3 // 3 is the Permissions View
@ -134,43 +132,43 @@ class CalendarViewController: ParentViewController {
NSWorkspace.shared.launchApplication("System Preferences")
}
}
@IBAction func showNextMeetingAction(_ sender: NSSegmentedControl) {
// We need to start the menubar timer if it hasn't been started already
guard let delegate = NSApplication.shared.delegate as? AppDelegate else {
assertionFailure()
return
}
let statusItemHandler = delegate.statusItemForPanel()
if sender.selectedSegment == 0 {
if let isValid = statusItemHandler.menubarTimer?.isValid, isValid == true {
Logger.info("Timer is already in progress")
updateStatusItem()
return
}
} else {
statusItemHandler.invalidateTimer(showIcon: true, isSyncing: false)
}
}
@IBAction func showUpcomingEventView(_ sender: NSSegmentedControl) {
var showUpcomingEventView = "YES"
if sender.selectedSegment == 1 {
showUpcomingEventView = "NO"
}
UserDefaults.standard.set(showUpcomingEventView, forKey: UserDefaultKeys.showUpcomingEventView)
if DataStore.shared().shouldDisplay(ViewType.showAppInForeground) {
let floatingWindow = FloatingWindowController.shared()
floatingWindow.determineUpcomingViewVisibility()
return
}
guard let panel = PanelController.panel() else { return }
if sender.selectedSegment == 1 {
panel.removeUpcomingEventView()
@ -180,15 +178,15 @@ class CalendarViewController: ParentViewController {
Logger.log(object: ["Show": "YES"], for: "Upcoming Event View")
}
}
private func updateStatusItem() {
guard let statusItem = (NSApplication.shared.delegate as? AppDelegate)?.statusItemForPanel() else {
return
}
statusItem.refresh()
}
@IBOutlet var headerLabel: NSTextField!
@IBOutlet var upcomingEventView: NSTextField!
@IBOutlet var allDayMeetingsLabel: NSTextField!
@ -198,7 +196,7 @@ class CalendarViewController: ParentViewController {
@IBOutlet var showEventsFromLabel: NSTextField!
@IBOutlet var charactersField: NSTextField!
@IBOutlet var truncateAccessoryLabel: NSTextField!
private func setup() {
// Grant access button's text color is taken care above.
headerLabel.stringValue = "Upcoming Event View Options".localized()
@ -209,11 +207,11 @@ class CalendarViewController: ParentViewController {
charactersField.stringValue = "characters".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()
[headerLabel, upcomingEventView, allDayMeetingsLabel,
showNextMeetingLabel, nextMeetingAccessoryLabel, truncateTextLabel,
showEventsFromLabel, charactersField, truncateAccessoryLabel].forEach { $0?.textColor = Themer.shared().mainTextColor() }
calendarsTableView.backgroundColor = Themer.shared().mainBackgroundColor()
truncateTextField.backgroundColor = Themer.shared().mainBackgroundColor()
}
@ -230,15 +228,15 @@ extension CalendarViewController: NSTableViewDelegate {
func tableView(_: NSTableView, shouldSelectRow _: Int) -> Bool {
return false
}
func tableView(_: NSTableView, heightOfRow row: Int) -> CGFloat {
guard let currentSource = calendars[row] as? String, !currentSource.isEmpty else {
return 30.0
}
return 24.0
}
func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? {
if let currentSource = calendars[row] as? String,
let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "sourceCellView"), owner: self) as? SourceTableViewCell
@ -246,7 +244,7 @@ extension CalendarViewController: NSTableViewDelegate {
message.sourceName.stringValue = currentSource
return message
}
if let currentSource = calendars[row] as? CalendarInfo,
let calendarCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "calendarCellView"), owner: self) as? CalendarTableViewCell
{
@ -258,36 +256,36 @@ extension CalendarViewController: NSTableViewDelegate {
calendarCell.calendarSelected.action = #selector(calendarSelected(_:))
return calendarCell
}
return nil
}
@objc func calendarSelected(_ checkbox: NSButton) {
let currentSelection = checkbox.tag
var sourcesAndCalendars = calendars
if var calInfo = sourcesAndCalendars[currentSelection] as? CalendarInfo {
calInfo.selected = (checkbox.state == .on)
sourcesAndCalendars[currentSelection] = calInfo
}
updateSelectedCalendars(sourcesAndCalendars)
}
private func updateSelectedCalendars(_ selection: [Any]) {
var selectedCalendars: [String] = []
for obj in selection {
if let calInfo = obj as? CalendarInfo, calInfo.selected {
selectedCalendars.append(calInfo.calendar.calendarIdentifier)
}
}
UserDefaults.standard.set(selectedCalendars, forKey: UserDefaultKeys.selectedCalendars)
calendars = EventCenter.sharedCenter().fetchSourcesAndCalendars()
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 {
var hasActiveIcon: Bool = false
var menubarTimer: Timer?
var statusItem: NSStatusItem = {
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.toolTip = "Clocker"
(statusItem.button?.cell as? NSButtonCell)?.highlightsBy = NSCell.StyleMask(rawValue: 0)
return statusItem
}()
private lazy var menubarTitleHandler = MenubarTitleProvider(with: self.store, eventStore: EventCenter.sharedCenter())
private var statusContainerView: StatusContainerView?
private var nsCalendar = Calendar.autoupdatingCurrent
private lazy var units: Set<Calendar.Component> = Set([.era, .year, .month, .day, .hour, .minute])
private var userNotificationsDidChangeNotif: NSObjectProtocol?
private let store: DataStore
// Current State might be set twice when the user first launches an app.
// First, when StatusItemHandler() is instantiated in AppDelegate
// Second, when AppDelegate.fetchLocalTimezone() is called triggering a customLabel didSet.
@ -50,7 +50,7 @@ class StatusItemHandler: NSObject {
case .icon:
statusItem.button?.image = nil
}
// Now setup for the new menubar state
switch currentState {
case .compactText:
@ -60,25 +60,25 @@ class StatusItemHandler: NSObject {
case .icon:
setClockerIcon()
}
Logger.info("Status Bar Current State changed: \(currentState)\n")
}
}
init(with dataStore: DataStore) {
store = dataStore
super.init()
setupStatusItem()
setupNotificationObservers()
}
func setupStatusItem() {
// Let's figure out the initial menubar state
var menubarState = MenubarState.icon
let shouldTextBeDisplayed = store.menubarTimezones()?.isEmpty ?? true
if !shouldTextBeDisplayed || store.shouldDisplay(.showMeetingInMenubar) {
if store.shouldDisplay(.menubarCompactMode) {
menubarState = .compactText
@ -86,68 +86,64 @@ class StatusItemHandler: NSObject {
menubarState = .standardText
}
}
// Initial state has been figured out. Time to set it!
currentState = menubarState
func setSelector() {
if #available(macOS 10.14, *) {
statusItem.button?.action = #selector(menubarIconClicked(_:))
} else {
statusItem.action = #selector(menubarIconClicked(_:))
}
statusItem.button?.action = #selector(menubarIconClicked(_:))
}
statusItem.button?.target = self
statusItem.autosaveName = NSStatusItem.AutosaveName("ClockerStatusItem")
setSelector()
}
private func setupNotificationObservers() {
let center = NotificationCenter.default
let mainQueue = OperationQueue.main
center.addObserver(self,
selector: #selector(updateMenubar),
name: NSWorkspace.didWakeNotification,
object: nil)
DistributedNotificationCenter.default.addObserver(self, selector: #selector(respondToInterfaceStyleChange),
name: .interfaceStyleDidChange,
object: nil)
userNotificationsDidChangeNotif = center.addObserver(forName: UserDefaults.didChangeNotification,
object: self,
queue: mainQueue)
{ _ in
self.setupStatusItem()
}
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: OperationQueue.main) { _ in
self.menubarTimer?.invalidate()
}
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: OperationQueue.main) { _ in
self.setupStatusItem()
}
}
deinit {
if let userNotifsDidChange = userNotificationsDidChangeNotif {
NotificationCenter.default.removeObserver(userNotifsDidChange)
}
}
private func constructCompactView(with upcomingEventView: Bool = false) {
statusItem.button?.subviews = []
statusContainerView = nil
let menubarTimezones = store.menubarTimezones() ?? []
if menubarTimezones.isEmpty {
currentState = .icon
return
}
statusContainerView = StatusContainerView(with: menubarTimezones,
store: store,
showUpcomingEventView: upcomingEventView,
@ -155,7 +151,7 @@ class StatusItemHandler: NSObject {
statusContainerView?.wantsLayer = true
statusItem.button?.addSubview(statusContainerView!)
statusItem.button?.frame = statusContainerView!.bounds
// 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.
if let window = statusItem.button?.window {
@ -168,7 +164,7 @@ class StatusItemHandler: NSObject {
}
statusItem.button?.subviews.first?.window?.backgroundColor = NSColor.clear
}
// 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
// Our icon is template, so it changes automatically; so is our standard status bar text
@ -178,68 +174,68 @@ class StatusItemHandler: NSObject {
updateCompactMenubar()
}
}
@objc func setHasActiveIcon(_ value: Bool) {
hasActiveIcon = value
}
@objc func menubarIconClicked(_ sender: NSStatusBarButton) {
guard let mainDelegate = NSApplication.shared.delegate as? AppDelegate else {
return
}
mainDelegate.togglePanel(sender)
}
@objc func updateMenubar() {
guard let fireDate = calculateFireDate() else { return }
let shouldDisplaySeconds = shouldDisplaySecondsInMenubar()
menubarTimer = Timer(fire: fireDate,
interval: 0,
repeats: false,
block: { [weak self] _ in
if let strongSelf = self {
strongSelf.refresh()
}
})
if let strongSelf = self {
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
menubarTimer?.tolerance = shouldDisplaySeconds ? 0.5 : 20
guard let runLoopTimer = menubarTimer else {
Logger.info("Timer is unexpectedly nil")
return
}
RunLoop.main.add(runLoopTimer, forMode: .common)
}
private func shouldDisplaySecondsInMenubar() -> Bool {
let syncedTimezones = store.menubarTimezones() ?? []
let timezonesSupportingSeconds = syncedTimezones.filter { data in
if let timezoneObj = TimezoneData.customObject(from: data) {
return timezoneObj.shouldShowSeconds(store.timezoneFormat())
}
return false
}
return timezonesSupportingSeconds.isEmpty == false
}
private func calculateFireDate() -> Date? {
let shouldDisplaySeconds = shouldDisplaySecondsInMenubar()
let menubarFavourites = store.menubarTimezones()
if !units.contains(.second), shouldDisplaySeconds {
units.insert(.second)
}
var components = nsCalendar.dateComponents(units, from: Date())
// We want to update every second only when there's a timezone present!
if shouldDisplaySeconds, let seconds = components.second, let favourites = menubarFavourites, !favourites.isEmpty {
components.second = seconds + 1
@ -249,15 +245,15 @@ class StatusItemHandler: NSObject {
Logger.info("Unable to create date components for the menubar timewr")
return nil
}
guard let fireDate = nsCalendar.date(from: components) else {
Logger.info("Unable to form Fire Date")
return nil
}
return fireDate
}
func updateCompactMenubar() {
let filteredEvents = EventCenter.sharedCenter().filteredEvents
let calendar = EventCenter.sharedCenter().autoupdatingCalendar
@ -270,7 +266,7 @@ class StatusItemHandler: NSObject {
constructCompactView(with: true)
}
}
if let upcomingEventView = retrieveUpcomingEventStatusView(), upcomingEvent == nil {
upcomingEventView.removeFromSuperview()
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.
statusContainerView?.updateTime()
}
private func removeUpcomingStatusItemView() {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.2
@ -290,7 +286,7 @@ class StatusItemHandler: NSObject {
}
}
}
func refresh() {
if currentState == .compactText {
updateCompactMenubar()
@ -308,69 +304,69 @@ class StatusItemHandler: NSObject {
menubarTimer?.invalidate()
}
}
private func setupForStandardTextMode() {
Logger.info("Initializing menubar timer")
// Let's invalidate the previous timer
menubarTimer?.invalidate()
menubarTimer = nil
setupForStandardText()
updateMenubar()
}
func invalidateTimer(showIcon show: Bool, isSyncing sync: Bool) {
// Check if user is not showing
// 1. Timezones
// 2. Upcoming Event
let menubarFavourites = store.menubarTimezones() ?? []
if menubarFavourites.isEmpty, store.shouldDisplay(.showMeetingInMenubar) == false {
Logger.info("Invalidating menubar timer!")
invalidation()
if show {
currentState = .icon
}
} else if sync {
Logger.info("Invalidating menubar timer for sync purposes!")
invalidation()
if show {
setClockerIcon()
}
} else {
Logger.info("Not stopping menubar timer!")
}
}
private func invalidation() {
menubarTimer?.invalidate()
}
private func setClockerIcon() {
if statusItem.button?.subviews.isEmpty == false {
statusItem.button?.subviews = []
}
if statusItem.button?.image?.name() == NSImage.Name.menubarIcon {
return
}
statusItem.button?.title = UserDefaultKeys.emptyString
statusItem.button?.image = NSImage(named: .menubarIcon)
statusItem.button?.imagePosition = .imageOnly
statusItem.button?.toolTip = "Clocker"
}
private func setupForStandardText() {
var menubarText = UserDefaultKeys.emptyString
if let menubarTitle = menubarTitleHandler.titleForMenubar() {
menubarText = menubarTitle
} else if store.shouldDisplay(.showMeetingInMenubar) {
@ -379,31 +375,31 @@ class StatusItemHandler: NSObject {
// We have no favourites to display and no meetings to show.
// That means we should display our icon!
}
guard !menubarText.isEmpty else {
setClockerIcon()
return
}
let attributes = [NSAttributedString.Key.font: NSFont.monospacedDigitSystemFont(ofSize: 13.0, weight: NSFont.Weight.regular),
NSAttributedString.Key.baselineOffset: 0.1] as [NSAttributedString.Key: Any]
statusItem.button?.attributedTitle = NSAttributedString(string: menubarText, attributes: attributes)
statusItem.button?.image = nil
statusItem.button?.imagePosition = .imageLeft
}
private func setupForCompactTextMode() {
// Let's invalidate the previous timer
menubarTimer?.invalidate()
menubarTimer = nil
let filteredEvents = EventCenter.sharedCenter().filteredEvents
let calendar = EventCenter.sharedCenter().autoupdatingCalendar
let checkForUpcomingEvents = menubarTitleHandler.checkForUpcomingEvents(filteredEvents, calendar: calendar)
constructCompactView(with: checkForUpcomingEvents != nil)
updateMenubar()
}
private func retrieveUpcomingEventStatusView() -> NSView? {
let upcomingEventView = statusContainerView?.subviews.first(where: { statusItemView in
if let upcomingEventView = statusItemView as? StatusItemViewConforming {
@ -413,26 +409,26 @@ class StatusItemHandler: NSObject {
})
return upcomingEventView
}
private func bufferCalculatedWidth() -> Int {
var totalWidth = 55
if store.shouldShowDayInMenubar() {
totalWidth += 12
}
if store.isBufferRequiredForTwelveHourFormats() {
totalWidth += 20
}
if store.shouldShowDateInMenubar() {
totalWidth += 20
}
if store.shouldDisplay(.showMeetingInMenubar) {
totalWidth += 100
}
return totalWidth
}
}

51
Clocker/Preferences/Menu Bar/StatusItemView.swift

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

Loading…
Cancel
Save