Abhishek
4 years ago
5 changed files with 743 additions and 16 deletions
@ -0,0 +1,60 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Cocoa |
||||||
|
import EventKit |
||||||
|
|
||||||
|
class MenubarHandler: NSObject { |
||||||
|
func titleForMenubar() -> String? { |
||||||
|
if let nextEvent = checkForUpcomingEvents() { |
||||||
|
return nextEvent |
||||||
|
} |
||||||
|
|
||||||
|
guard let menubarTitles = DataStore.shared().menubarTimezones() else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// If the menubar is in compact mode, we don't need any of the below calculations; exit early |
||||||
|
if DataStore.shared().shouldDisplay(.menubarCompactMode) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if menubarTitles.isEmpty == false { |
||||||
|
let titles = menubarTitles.map { (data) -> String? in |
||||||
|
let timezone = TimezoneData.customObject(from: data) |
||||||
|
let operationsObject = TimezoneDataOperations(with: timezone!) |
||||||
|
return "\(operationsObject.menuTitle().trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines))" |
||||||
|
} |
||||||
|
|
||||||
|
let titlesStringified = titles.compactMap { $0 } |
||||||
|
return titlesStringified.joined(separator: " ") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
private func checkForUpcomingEvents() -> String? { |
||||||
|
if DataStore.shared().shouldDisplay(.showMeetingInMenubar) { |
||||||
|
let filteredDates = EventCenter.sharedCenter().eventsForDate |
||||||
|
let autoupdatingCal = EventCenter.sharedCenter().autoupdatingCalendar |
||||||
|
guard let events = filteredDates[autoupdatingCal.startOfDay(for: Date())] else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
for event in events { |
||||||
|
if event.event.startDate.timeIntervalSinceNow > 0, !event.isAllDay { |
||||||
|
let timeForEventToStart = event.event.startDate.timeIntervalSinceNow / 60 |
||||||
|
|
||||||
|
if timeForEventToStart > 30 { |
||||||
|
Logger.info("Our next event: \(event.event.title ?? "Error") starts in \(timeForEventToStart) mins") |
||||||
|
|
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
return EventCenter.sharedCenter().format(event: event.event) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,181 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Cocoa |
||||||
|
|
||||||
|
func bufferCalculatedWidth() -> Int { |
||||||
|
var totalWidth = 55 |
||||||
|
|
||||||
|
if DataStore.shared().shouldShowDayInMenubar() { |
||||||
|
totalWidth += 12 |
||||||
|
} |
||||||
|
|
||||||
|
if DataStore.shared().isBufferRequiredForTwelveHourFormats() { |
||||||
|
totalWidth += 20 |
||||||
|
} |
||||||
|
|
||||||
|
if DataStore.shared().shouldShowDateInMenubar() { |
||||||
|
totalWidth += 20 |
||||||
|
} |
||||||
|
|
||||||
|
return totalWidth |
||||||
|
} |
||||||
|
|
||||||
|
func compactWidth(for timezone: TimezoneData) -> Int { |
||||||
|
var totalWidth = 55 |
||||||
|
let timeFormat = timezone.timezoneFormat() |
||||||
|
|
||||||
|
if DataStore.shared().shouldShowDayInMenubar() { |
||||||
|
totalWidth += 12 |
||||||
|
} |
||||||
|
|
||||||
|
if timeFormat == DateFormat.twelveHour |
||||||
|
|| timeFormat == DateFormat.twelveHourWithSeconds |
||||||
|
|| timeFormat == DateFormat.twelveHourWithZero |
||||||
|
|| timeFormat == DateFormat.twelveHourWithSeconds { |
||||||
|
totalWidth += 20 |
||||||
|
} else if timeFormat == DateFormat.twentyFourHour |
||||||
|
|| timeFormat == DateFormat.twentyFourHourWithSeconds { |
||||||
|
totalWidth += 0 |
||||||
|
} |
||||||
|
|
||||||
|
if timezone.shouldShowSeconds() { |
||||||
|
// Slight buffer needed when the Menubar supplementary text was Mon 9:27:58 AM |
||||||
|
totalWidth += 15 |
||||||
|
} |
||||||
|
|
||||||
|
if DataStore.shared().shouldShowDateInMenubar() { |
||||||
|
totalWidth += 20 |
||||||
|
} |
||||||
|
|
||||||
|
print("-- Compact Width is \(totalWidth)") |
||||||
|
return totalWidth |
||||||
|
} |
||||||
|
|
||||||
|
// Test with Sat 12:46 AM |
||||||
|
let bufferWidth: CGFloat = 9.5 |
||||||
|
|
||||||
|
class StatusContainerView: NSView { |
||||||
|
private var previousX: Int = 0 |
||||||
|
|
||||||
|
override func awakeFromNib() { |
||||||
|
super.awakeFromNib() |
||||||
|
wantsLayer = true |
||||||
|
layer?.backgroundColor = NSColor.clear.cgColor |
||||||
|
} |
||||||
|
|
||||||
|
init(with timezones: [Data]) { |
||||||
|
func addSubviews() { |
||||||
|
timezones.forEach { |
||||||
|
if let timezoneObject = TimezoneData.customObject(from: $0) { |
||||||
|
addTimezone(timezoneObject) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let timeBasedAttributes = [ |
||||||
|
NSAttributedString.Key.font: compactModeTimeFont, |
||||||
|
NSAttributedString.Key.backgroundColor: NSColor.clear, |
||||||
|
NSAttributedString.Key.paragraphStyle: defaultParagraphStyle, |
||||||
|
] |
||||||
|
|
||||||
|
func containerWidth(for timezones: [Data]) -> CGFloat { |
||||||
|
let compressedWidth = timezones.reduce(0.0) { (result, timezone) -> CGFloat in |
||||||
|
|
||||||
|
if let timezoneObject = TimezoneData.customObject(from: timezone) { |
||||||
|
let precalculatedWidth = Double(compactWidth(for: timezoneObject)) |
||||||
|
let operationObject = TimezoneDataOperations(with: timezoneObject) |
||||||
|
let calculatedSubtitleSize = compactModeTimeFont.size(operationObject.compactMenuSubtitle(), precalculatedWidth, attributes: timeBasedAttributes) |
||||||
|
let calculatedTitleSize = compactModeTimeFont.size(operationObject.compactMenuTitle(), precalculatedWidth, attributes: timeBasedAttributes) |
||||||
|
let showSeconds = timezoneObject.shouldShowSeconds() |
||||||
|
let secondsBuffer: CGFloat = showSeconds ? 7 : 0 |
||||||
|
return result + max(calculatedTitleSize.width, calculatedSubtitleSize.width) + bufferWidth + secondsBuffer |
||||||
|
} |
||||||
|
|
||||||
|
return result + CGFloat(bufferCalculatedWidth()) |
||||||
|
} |
||||||
|
|
||||||
|
let calculatedWidth = min(compressedWidth, |
||||||
|
CGFloat(timezones.count * bufferCalculatedWidth())) |
||||||
|
return calculatedWidth |
||||||
|
} |
||||||
|
|
||||||
|
let statusItemWidth = containerWidth(for: timezones) |
||||||
|
let frame = NSRect(x: 0, y: 0, width: statusItemWidth, height: 30) |
||||||
|
super.init(frame: frame) |
||||||
|
|
||||||
|
addSubviews() |
||||||
|
} |
||||||
|
|
||||||
|
required init?(coder _: NSCoder) { |
||||||
|
fatalError("init(coder:) has not been implemented") |
||||||
|
} |
||||||
|
|
||||||
|
func addTimezone(_ timezone: TimezoneData) { |
||||||
|
let calculatedWidth = bestWidth(for: timezone) |
||||||
|
let frame = NSRect(x: previousX, y: 0, width: calculatedWidth, height: 30) |
||||||
|
|
||||||
|
let statusItemView = StatusItemView(frame: frame) |
||||||
|
statusItemView.dataObject = timezone |
||||||
|
|
||||||
|
addSubview(statusItemView) |
||||||
|
|
||||||
|
previousX += calculatedWidth |
||||||
|
} |
||||||
|
|
||||||
|
private func bestWidth(for timezone: TimezoneData) -> Int { |
||||||
|
var textColor = hasDarkAppearance ? NSColor.white : NSColor.black |
||||||
|
|
||||||
|
if #available(OSX 11.0, *) { |
||||||
|
textColor = NSColor.white |
||||||
|
} |
||||||
|
|
||||||
|
let timeBasedAttributes = [ |
||||||
|
NSAttributedString.Key.font: compactModeTimeFont, |
||||||
|
NSAttributedString.Key.foregroundColor: textColor, |
||||||
|
NSAttributedString.Key.backgroundColor: NSColor.clear, |
||||||
|
NSAttributedString.Key.paragraphStyle: defaultParagraphStyle, |
||||||
|
] |
||||||
|
|
||||||
|
let operation = TimezoneDataOperations(with: timezone) |
||||||
|
let bestSize = compactModeTimeFont.size(operation.compactMenuSubtitle(), |
||||||
|
Double(compactWidth(for: timezone)), attributes: timeBasedAttributes) |
||||||
|
let bestTitleSize = compactModeTimeFont.size(operation.compactMenuTitle(), Double(compactWidth(for: timezone)), attributes: timeBasedAttributes) |
||||||
|
|
||||||
|
return Int(max(bestSize.width, bestTitleSize.width) + bufferWidth) |
||||||
|
} |
||||||
|
|
||||||
|
func updateTime() { |
||||||
|
if subviews.isEmpty { |
||||||
|
assertionFailure("Subviews count should > 0") |
||||||
|
} |
||||||
|
|
||||||
|
// See if frame's width needs any adjustment |
||||||
|
adjustWidthIfNeccessary() |
||||||
|
} |
||||||
|
|
||||||
|
private func adjustWidthIfNeccessary() { |
||||||
|
var newWidth: CGFloat = 0 |
||||||
|
|
||||||
|
subviews.forEach { |
||||||
|
if let statusItem = $0 as? StatusItemView, statusItem.isHidden == false { |
||||||
|
// Determine what's the best width required to display the current string. |
||||||
|
let newBestWidth = CGFloat(bestWidth(for: statusItem.dataObject)) |
||||||
|
|
||||||
|
// Let's note if the current width is too small/correct |
||||||
|
newWidth += statusItem.frame.size.width != newBestWidth ? newBestWidth : statusItem.frame.size.width |
||||||
|
|
||||||
|
statusItem.frame = CGRect(x: statusItem.frame.origin.x, |
||||||
|
y: statusItem.frame.origin.y, |
||||||
|
width: newBestWidth, |
||||||
|
height: statusItem.frame.size.height) |
||||||
|
|
||||||
|
statusItem.updateTimeInMenubar() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if newWidth != frame.size.width, newWidth > frame.size.width + 2.0 { |
||||||
|
Logger.info("Correcting our width to \(newWidth) and the previous width was \(frame.size.width)") |
||||||
|
frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: newWidth, height: frame.size.height) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,354 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Cocoa |
||||||
|
|
||||||
|
private enum MenubarState { |
||||||
|
case compactText |
||||||
|
case standardText |
||||||
|
case icon |
||||||
|
} |
||||||
|
|
||||||
|
class StatusItemHandler: NSObject { |
||||||
|
var hasActiveIcon: Bool = false |
||||||
|
|
||||||
|
var menubarTimer: Timer? |
||||||
|
|
||||||
|
var statusItem: NSStatusItem = { |
||||||
|
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) |
||||||
|
statusItem.highlightMode = false |
||||||
|
return statusItem |
||||||
|
}() |
||||||
|
|
||||||
|
private var menubarTitleHandler = MenubarHandler() |
||||||
|
|
||||||
|
private var parentView: StatusContainerView? |
||||||
|
|
||||||
|
private var nsCalendar = Calendar.autoupdatingCurrent |
||||||
|
|
||||||
|
private lazy var units: Set<Calendar.Component> = Set([.era, .year, .month, .day, .hour, .minute]) |
||||||
|
|
||||||
|
private var userNotificationsDidChangeNotif: NSObjectProtocol? |
||||||
|
|
||||||
|
// Current State is 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. |
||||||
|
// TODO: Make sure it's set just once. |
||||||
|
private var currentState: MenubarState = .standardText { |
||||||
|
didSet { |
||||||
|
// Do some cleanup |
||||||
|
switch oldValue { |
||||||
|
case .compactText: |
||||||
|
statusItem.view = nil |
||||||
|
parentView = nil |
||||||
|
case .standardText: |
||||||
|
statusItem.button?.title = CLEmptyString |
||||||
|
case .icon: |
||||||
|
statusItem.button?.image = nil |
||||||
|
} |
||||||
|
|
||||||
|
// Now setup for the new menubar state |
||||||
|
switch currentState { |
||||||
|
case .compactText: |
||||||
|
setupForCompactTextMode() |
||||||
|
case .standardText: |
||||||
|
setupForStandardTextMode() |
||||||
|
case .icon: |
||||||
|
setClockerIcon() |
||||||
|
} |
||||||
|
|
||||||
|
Logger.info("\nStatus Bar Current State changed: \(currentState)\n") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override init() { |
||||||
|
super.init() |
||||||
|
|
||||||
|
setupStatusItem() |
||||||
|
setupNotificationObservers() |
||||||
|
} |
||||||
|
|
||||||
|
func setupStatusItem() { |
||||||
|
// Let's figure out the initial menubar state |
||||||
|
var menubarState = MenubarState.icon |
||||||
|
|
||||||
|
let shouldTextBeDisplayed = DataStore.shared().menubarTimezones()?.isEmpty ?? true |
||||||
|
|
||||||
|
if !shouldTextBeDisplayed || DataStore.shared().shouldDisplay(.showMeetingInMenubar) { |
||||||
|
if DataStore.shared().shouldDisplay(.menubarCompactMode) { |
||||||
|
menubarState = .compactText |
||||||
|
} else { |
||||||
|
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.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() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
deinit { |
||||||
|
if let userNotifsDidChange = userNotificationsDidChangeNotif { |
||||||
|
NotificationCenter.default.removeObserver(userNotifsDidChange) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private func constructCompactView() { |
||||||
|
parentView = nil |
||||||
|
|
||||||
|
let menubarTimezones = retrieveSyncedMenubarTimezones() |
||||||
|
|
||||||
|
if menubarTimezones.isEmpty { |
||||||
|
currentState = .icon |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
parentView = StatusContainerView(with: menubarTimezones) |
||||||
|
statusItem.view = parentView |
||||||
|
statusItem.view?.window?.backgroundColor = NSColor.clear |
||||||
|
} |
||||||
|
|
||||||
|
private func retrieveSyncedMenubarTimezones() -> [Data] { |
||||||
|
let defaultPreferences = DataStore.shared().retrieve(key: CLDefaultPreferenceKey) as? [Data] ?? [] |
||||||
|
|
||||||
|
let menubarTimezones = defaultPreferences.filter { (data) -> Bool in |
||||||
|
if let timezoneObj = TimezoneData.customObject(from: data) { |
||||||
|
return timezoneObj.isFavourite == 1 |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
return menubarTimezones |
||||||
|
} |
||||||
|
|
||||||
|
// 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 |
||||||
|
// Only need to handle the compact mode! |
||||||
|
@objc func respondToInterfaceStyleChange() { |
||||||
|
if DataStore.shared().shouldDisplay(.menubarCompactMode) { |
||||||
|
updateCompactMenubar() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@objc func setHasActiveIcon(_ value: Bool) { |
||||||
|
hasActiveIcon = value |
||||||
|
} |
||||||
|
|
||||||
|
@objc func menubarIconClicked(_ sender: Any) { |
||||||
|
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.performTimerWork() |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 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 = retrieveSyncedMenubarTimezones() |
||||||
|
|
||||||
|
for timezone in syncedTimezones { |
||||||
|
if let timezoneObj = TimezoneData.customObject(from: timezone) { |
||||||
|
let shouldShowSeconds = timezoneObj.shouldShowSeconds() |
||||||
|
if shouldShowSeconds { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
private func calculateFireDate() -> Date? { |
||||||
|
let shouldDisplaySeconds = shouldDisplaySecondsInMenubar() |
||||||
|
let menubarFavourites = DataStore.shared().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 |
||||||
|
} else if let minutes = components.minute { |
||||||
|
components.minute = minutes + 1 |
||||||
|
} else { |
||||||
|
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() { |
||||||
|
parentView?.updateTime() |
||||||
|
} |
||||||
|
|
||||||
|
func performTimerWork() { |
||||||
|
if currentState == .compactText { |
||||||
|
updateCompactMenubar() |
||||||
|
updateMenubar() |
||||||
|
} else if currentState == .standardText, let title = menubarTitleHandler.titleForMenubar() { |
||||||
|
// Need setting button's image to nil |
||||||
|
// Especially if we have showUpcomingEvents turned to true and menubar timezones are empty |
||||||
|
statusItem.button?.image = nil |
||||||
|
statusItem.button?.title = title |
||||||
|
updateMenubar() |
||||||
|
} else { |
||||||
|
setClockerIcon() |
||||||
|
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 = DataStore.shared().menubarTimezones() ?? [] |
||||||
|
|
||||||
|
if menubarFavourites.isEmpty, DataStore.shared().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.view != nil { |
||||||
|
statusItem.view = nil |
||||||
|
} |
||||||
|
|
||||||
|
if statusItem.button?.image?.name() == NSImage.Name.menubarIcon { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
statusItem.button?.title = CLEmptyString |
||||||
|
statusItem.button?.image = NSImage(named: .menubarIcon) |
||||||
|
statusItem.button?.imagePosition = .imageOnly |
||||||
|
} |
||||||
|
|
||||||
|
private func setupForStandardText() { |
||||||
|
var menubarText = CLEmptyString |
||||||
|
|
||||||
|
if let menubarTitle = menubarTitleHandler.titleForMenubar() { |
||||||
|
menubarText = menubarTitle |
||||||
|
} else if DataStore.shared().shouldDisplay(.showMeetingInMenubar) { |
||||||
|
// Don't have any meeting to show |
||||||
|
} else { |
||||||
|
// We have no favourites to display and no meetings to show. |
||||||
|
// That means we should display our icon! |
||||||
|
} |
||||||
|
|
||||||
|
guard !menubarText.isEmpty else { |
||||||
|
setClockerIcon() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
statusItem.button?.title = menubarText |
||||||
|
statusItem.button?.font = NSFont.monospacedDigitSystemFont(ofSize: 14.0, weight: NSFont.Weight.regular) |
||||||
|
statusItem.button?.image = nil |
||||||
|
statusItem.button?.imagePosition = .imageLeft |
||||||
|
} |
||||||
|
|
||||||
|
private func setupForCompactTextMode() { |
||||||
|
// Let's invalidate the previous timer |
||||||
|
menubarTimer?.invalidate() |
||||||
|
menubarTimer = nil |
||||||
|
|
||||||
|
constructCompactView() |
||||||
|
updateMenubar() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,132 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Cocoa |
||||||
|
|
||||||
|
var defaultParagraphStyle: NSMutableParagraphStyle { |
||||||
|
let paragraphStyle = NSMutableParagraphStyle() |
||||||
|
paragraphStyle.alignment = .center |
||||||
|
paragraphStyle.lineBreakMode = .byTruncatingTail |
||||||
|
return paragraphStyle |
||||||
|
} |
||||||
|
|
||||||
|
var compactModeTimeFont: NSFont { |
||||||
|
return NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .regular) |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class StatusItemView: NSView { |
||||||
|
// MARK: Private variables |
||||||
|
|
||||||
|
private let locationView: NSTextField = NSTextField(labelWithString: "Hello") |
||||||
|
private let timeView: NSTextField = NSTextField(labelWithString: "Mon 19:14 PM") |
||||||
|
private var operationsObject: TimezoneDataOperations { |
||||||
|
return TimezoneDataOperations(with: dataObject) |
||||||
|
} |
||||||
|
|
||||||
|
private var timeAttributes: [NSAttributedString.Key: AnyObject] { |
||||||
|
let textColor = hasDarkAppearance ? NSColor.white : NSColor.black |
||||||
|
|
||||||
|
let attributes = [ |
||||||
|
NSAttributedString.Key.font: compactModeTimeFont, |
||||||
|
NSAttributedString.Key.foregroundColor: textColor, |
||||||
|
NSAttributedString.Key.backgroundColor: NSColor.clear, |
||||||
|
NSAttributedString.Key.paragraphStyle: defaultParagraphStyle, |
||||||
|
] |
||||||
|
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, |
||||||
|
NSAttributedString.Key.backgroundColor: NSColor.clear, |
||||||
|
NSAttributedString.Key.paragraphStyle: defaultParagraphStyle, |
||||||
|
] |
||||||
|
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() |
||||||
|
|
||||||
|
NSLayoutConstraint.activate([ |
||||||
|
locationView.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||||
|
locationView.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||||
|
locationView.topAnchor.constraint(equalTo: topAnchor, constant: 7), |
||||||
|
locationView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.35), |
||||||
|
]) |
||||||
|
|
||||||
|
NSLayoutConstraint.activate([ |
||||||
|
timeView.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||||
|
timeView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0), |
||||||
|
timeView.topAnchor.constraint(equalTo: locationView.bottomAnchor), |
||||||
|
timeView.bottomAnchor.constraint(equalTo: bottomAnchor), |
||||||
|
]) |
||||||
|
} |
||||||
|
|
||||||
|
@available(OSX 10.14, *) |
||||||
|
override func viewDidChangeEffectiveAppearance() { |
||||||
|
super.viewDidChangeEffectiveAppearance() |
||||||
|
updateTimeInMenubar() |
||||||
|
} |
||||||
|
|
||||||
|
func updateTimeInMenubar() { |
||||||
|
locationView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuTitle(), attributes: textFontAttributes) |
||||||
|
timeView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuSubtitle(), attributes: timeAttributes) |
||||||
|
} |
||||||
|
|
||||||
|
private func initialSetup() { |
||||||
|
locationView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuTitle(), attributes: textFontAttributes) |
||||||
|
timeView.attributedStringValue = NSAttributedString(string: operationsObject.compactMenuSubtitle(), attributes: timeAttributes) |
||||||
|
} |
||||||
|
|
||||||
|
required init?(coder _: NSCoder) { |
||||||
|
fatalError("init(coder:) has not been implemented") |
||||||
|
} |
||||||
|
|
||||||
|
override func mouseDown(with event: NSEvent) { |
||||||
|
super.mouseDown(with: event) |
||||||
|
guard let mainDelegate = NSApplication.shared.delegate as? AppDelegate else { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
mainDelegate.togglePanel(event) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue