diff --git a/Clocker/Preferences/Calendar/CalendarViewController.swift b/Clocker/Preferences/Calendar/CalendarViewController.swift index 6e77ab4..9166746 100644 --- a/Clocker/Preferences/Calendar/CalendarViewController.swift +++ b/Clocker/Preferences/Calendar/CalendarViewController.swift @@ -84,7 +84,6 @@ class CalendarViewController: ParentViewController { verifyCalendarAccess() showSegmentedControl.selectedSegment = DataStore.shared().shouldDisplay(ViewType.upcomingEventView) ? 0 : 1 - showNextMeetingInMenubarControl.isEnabled = DataStore.shared().shouldDisplay(.menubarCompactMode) ? false : true } private func verifyCalendarAccess() { diff --git a/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift b/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift index 69ea823..84aee9f 100644 --- a/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift +++ b/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift @@ -34,7 +34,7 @@ class MenubarTitleProvider: NSObject { return nil } - private func checkForUpcomingEvents() -> String? { + func checkForUpcomingEvents() -> String? { if DataStore.shared().shouldDisplay(.showMeetingInMenubar) { let filteredDates = EventCenter.sharedCenter().eventsForDate let autoupdatingCal = EventCenter.sharedCenter().autoupdatingCalendar @@ -48,7 +48,6 @@ class MenubarTitleProvider: NSObject { if timeForEventToStart > 30 { Logger.info("Our next event: \(event.event.title ?? "Error") starts in \(timeForEventToStart) mins") - continue } diff --git a/Clocker/Preferences/Menu Bar/StatusContainerView.swift b/Clocker/Preferences/Menu Bar/StatusContainerView.swift index 2e95f92..94d2bf0 100644 --- a/Clocker/Preferences/Menu Bar/StatusContainerView.swift +++ b/Clocker/Preferences/Menu Bar/StatusContainerView.swift @@ -18,6 +18,10 @@ func bufferCalculatedWidth() -> Int { if DataStore.shared().shouldShowDateInMenubar() { totalWidth += 20 } + + if DataStore.shared().shouldDisplay(.showMeetingInMenubar) { + totalWidth += 100 + } return totalWidth } @@ -55,6 +59,14 @@ func compactWidth(for timezone: TimezoneData) -> Int { // Test with Sat 12:46 AM let bufferWidth: CGFloat = 9.5 +protocol StatusItemViewConforming { + /// Mark that we need to refresh the text we're showing in the menubar + func statusItemViewSetNeedsDisplay() + + /// Status Item Views can be used to represent different information (like time in location, or an upcoming meeting). Distinguish between different status items view through this identifier + func statusItemViewIdentifier() -> String +} + class StatusContainerView: NSView { private var previousX: Int = 0 @@ -64,8 +76,20 @@ class StatusContainerView: NSView { layer?.backgroundColor = NSColor.clear.cgColor } - init(with timezones: [Data]) { + init(with timezones: [Data], showUpcomingEventView: Bool) { func addSubviews() { + if showUpcomingEventView, + let events = EventCenter.sharedCenter().eventsForDate[NSCalendar.autoupdatingCurrent.startOfDay(for: Date())], + events.isEmpty == false, + let upcomingEvent = EventCenter.sharedCenter().nextOccuring(events) { + let calculatedWidth = bestWidth(for: upcomingEvent) + let frame = NSRect(x: previousX, y: 0, width: calculatedWidth, height: 30) + let calendarItemView = UpcomingEventStatusItemView(frame: frame) + calendarItemView.dataObject = upcomingEvent + addSubview(calendarItemView) + previousX += calculatedWidth + } + timezones.forEach { if let timezoneObject = TimezoneData.customObject(from: $0) { addTimezone(timezoneObject) @@ -80,7 +104,7 @@ class StatusContainerView: NSView { ] func containerWidth(for timezones: [Data]) -> CGFloat { - let compressedWidth = timezones.reduce(0.0) { result, timezone -> CGFloat in + var compressedWidth = timezones.reduce(0.0) { result, timezone -> CGFloat in if let timezoneObject = TimezoneData.customObject(from: timezone) { let precalculatedWidth = Double(compactWidth(for: timezoneObject)) @@ -95,6 +119,10 @@ class StatusContainerView: NSView { return result + CGFloat(bufferCalculatedWidth()) } + if showUpcomingEventView { + compressedWidth += 70 + } + let calculatedWidth = min(compressedWidth, CGFloat(timezones.count * bufferCalculatedWidth())) return calculatedWidth @@ -145,11 +173,41 @@ class StatusContainerView: NSView { return Int(max(bestSize.width, bestTitleSize.width) + bufferWidth) } + + private func bestWidth(for eventInfo: EventInfo) -> 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 bestSize = compactModeTimeFont.size(eventInfo.metadataForMeeting(), + 55, // Default for a location based status view + attributes: timeBasedAttributes) + let bestTitleSize = compactModeTimeFont.size(eventInfo.event.title, + 70, // Little more buffer since meeting titles tend to be longer + attributes: timeBasedAttributes) + + return Int(max(bestSize.width, bestTitleSize.width) + bufferWidth) + } func updateTime() { if subviews.isEmpty { assertionFailure("Subviews count should > 0") } + + for view in subviews { + if let conformingView = view as? StatusItemViewConforming { + conformingView.statusItemViewSetNeedsDisplay() + } + } // See if frame's width needs any adjustment adjustWidthIfNeccessary() @@ -170,8 +228,6 @@ class StatusContainerView: NSView { y: statusItem.frame.origin.y, width: newBestWidth, height: statusItem.frame.size.height) - - statusItem.updateTimeInMenubar() } } diff --git a/Clocker/Preferences/Menu Bar/StatusItemHandler.swift b/Clocker/Preferences/Menu Bar/StatusItemHandler.swift index 4b3c568..596898c 100644 --- a/Clocker/Preferences/Menu Bar/StatusItemHandler.swift +++ b/Clocker/Preferences/Menu Bar/StatusItemHandler.swift @@ -24,7 +24,7 @@ class StatusItemHandler: NSObject { private var menubarTitleHandler = MenubarTitleProvider() - private var parentView: StatusContainerView? + private var statusContainerView: StatusContainerView? private var nsCalendar = Calendar.autoupdatingCurrent @@ -42,7 +42,7 @@ class StatusItemHandler: NSObject { switch oldValue { case .compactText: statusItem.view = nil - parentView = nil + statusContainerView = nil case .standardText: statusItem.button?.title = CLEmptyString case .icon: @@ -134,8 +134,8 @@ class StatusItemHandler: NSObject { } } - private func constructCompactView() { - parentView = nil + private func constructCompactView(with upcomingEventView: Bool = false) { + statusContainerView = nil let menubarTimezones = DataStore.shared().menubarTimezones() ?? [] if menubarTimezones.isEmpty { @@ -143,8 +143,9 @@ class StatusItemHandler: NSObject { return } - parentView = StatusContainerView(with: menubarTimezones) - statusItem.view = parentView + statusContainerView = StatusContainerView(with: menubarTimezones, + showUpcomingEventView: upcomingEventView) + statusItem.view = statusContainerView statusItem.view?.window?.backgroundColor = NSColor.clear } @@ -241,7 +242,20 @@ class StatusItemHandler: NSObject { } func updateCompactMenubar() { - parentView?.updateTime() + if let upcomingEvent = menubarTitleHandler.checkForUpcomingEvents() { + print("Need to construct upcoming event view \(upcomingEvent)") + // Iterate and see if we're showing the calendar item view + let upcomingEventView = retrieveUpcomingEventStatusView() + // If not, reconstruct Status Container View with another view + if upcomingEventView == nil { + constructCompactView(with: true) + } + } else { + let upcomingEventView = retrieveUpcomingEventStatusView() + upcomingEventView?.removeFromSuperview() + } + // This will internally call `statusItemViewSetNeedsDisplay` on all subviews ensuring all text in the menubar is up-to-date. + statusContainerView?.updateTime() } func refresh() { @@ -347,7 +361,17 @@ class StatusItemHandler: NSObject { menubarTimer?.invalidate() menubarTimer = nil - constructCompactView() + constructCompactView(with: menubarTitleHandler.checkForUpcomingEvents() != nil) updateMenubar() } + + private func retrieveUpcomingEventStatusView() -> NSView? { + let upcomingEventView = statusContainerView?.subviews.first(where: { statusItemView in + if let upcomingEventView = statusItemView as? StatusItemViewConforming { + return upcomingEventView.statusItemViewIdentifier() == "upcoming_event_view" + } + return false + }) + return upcomingEventView + } }