// Copyright © 2015 Abhishek Banthia import Cocoa import CoreLoggerKit class PanelController: ParentPanelController { @objc dynamic var hasActivePanel: Bool = false static var sharedWindow = PanelController(windowNibName: .panel) @IBOutlet var backgroundView: BackgroundPanelView! override func windowDidLoad() { super.windowDidLoad() } class func shared() -> PanelController { return sharedWindow } 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() } } } func setFrameTheNewWay(_ rect: NSRect, _ maxX: CGFloat) { // Calculate window's top left point. // First, center window under status item. let width = (window?.frame)!.width 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") } guard isWindowLoaded == true else { return } super.dismissRowActions() updateDefaultPreferences() if DataStore.shared().timezones().isEmpty || DataStore.shared().shouldDisplay(.futureSlider) == false { futureSliderView.isHidden = true modernContainerView.isHidden = true } else if let value = DataStore.shared().retrieve(key: CLDisplayFutureSliderKey) as? NSNumber, modernContainerView != nil { if value.intValue == 1 { futureSliderView.isHidden = false modernContainerView.isHidden = true } else if value.intValue == 0 { futureSliderView.isHidden = true 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 = Set([IndexPath(item: modernSlider.numberOfItems(inSection: 0) / 2, section: 0)]) modernSlider.scrollToItems(at: indexPaths, scrollPosition: .centeredHorizontally) } 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") } } // 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") } guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } var statusBackgroundWindow = appDelegate.statusItemForPanel().statusItem.view?.window var statusView = appDelegate.statusItemForPanel().statusItem.view // 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 { var statusItemFrame = statusWindow.convertToScreen(statusButton.frame) 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 = minY setFrameTheNewWay(statusItemFrame, screenMaxX) if #available(OSX 10.14, *) { PerfLogger.endMarker("Set Panel Frame") } } } private func log() { if #available(OSX 10.14, *) { PerfLogger.startMarker("Logging") } let preferences = DataStore.shared().timezones() guard let theme = DataStore.shared().retrieve(key: CLThemeKey) as? NSNumber, let displayFutureSliderKey = DataStore.shared().retrieve(key: CLThemeKey) as? NSNumber, let showAppInForeground = DataStore.shared().retrieve(key: CLShowAppInForeground) as? NSNumber, let relativeDateKey = DataStore.shared().retrieve(key: CLRelativeDateKey) as? NSNumber, let fontSize = DataStore.shared().retrieve(key: CLUserFontSizePreference) as? NSNumber, let sunriseTime = DataStore.shared().retrieve(key: CLSunriseSunsetTime) as? NSNumber, let showDayInMenu = DataStore.shared().retrieve(key: CLShowDayInMenu) as? NSNumber, let showDateInMenu = DataStore.shared().retrieve(key: CLShowDateInMenu) as? NSNumber, let showPlaceInMenu = DataStore.shared().retrieve(key: CLShowPlaceInMenu) as? NSNumber, let showUpcomingEventView = DataStore.shared().retrieve(key: CLShowUpcomingEventView) as? String, let country = Locale.autoupdatingCurrent.regionCode 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", "Clocker mode": showAppInForeground.isEqual(to: NSNumber(value: 0)) ? "Menubar" : "Floating", "Relative Date": relativeDate, "Font Size": fontSize, "Sunrise Sunset": sunriseTime.isEqual(to: NSNumber(value: 0)) ? "Yes" : "No", "Show Day in Menu": showDayInMenu.isEqual(to: NSNumber(value: 0)) ? "Yes" : "No", "Show Date in Menu": showDateInMenu.isEqual(to: NSNumber(value: 0)) ? "Yes" : "No", "Show Place in Menu": showPlaceInMenu.isEqual(to: NSNumber(value: 0)) ? "Yes" : "No", "Show Upcoming Event View": showUpcomingEventView == "YES" ? "Yes" : "No", "Country": country, "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") } } private func startWindowTimer() { if #available(OSX 10.14, *) { 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") } return } startTimer() if #available(OSX 10.14, *) { 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() } } 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("\nWe 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 if count >= 1 || DataStore.shared().shouldDisplay(.showMeetingInMenubar) == true { if let handler = delegate?.statusItemForPanel(), let timer = handler.menubarTimer, !timer.isValid { 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 } 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.. if futureSliderView.isHidden == false, modernSlider.isHidden { futureSlider.doubleValue += Double(event.scrollingDeltaX) sliderMoved(futureSlider!) } } } extension PanelController: NSWindowDelegate { func windowWillClose(_: Notification) { parentTimer = nil setActivePanel(newValue: false) } func windowDidResignKey(_: Notification) { if let isVisible = window?.isVisible, isVisible == true { setActivePanel(newValue: false) } if let appDelegate = NSApplication.shared.delegate as? AppDelegate { appDelegate.statusItemForPanel().hasActiveIcon = false } } }