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