diff --git a/Calendr.xcodeproj/project.pbxproj b/Calendr.xcodeproj/project.pbxproj index 3f09b10c..2ef50797 100644 --- a/Calendr.xcodeproj/project.pbxproj +++ b/Calendr.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ 3430675E25F6BC15000D4003 /* HistoricalSchedulerTimeConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430675D25F6BC15000D4003 /* HistoricalSchedulerTimeConverter.swift */; }; 3430676325F6C130000D4003 /* TrackedHistoricalScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430676225F6C130000D4003 /* TrackedHistoricalScheduler.swift */; }; 3430ED03259634E00045DA53 /* NSStackView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430ED02259634E00045DA53 /* NSStackView+Rx.swift */; }; - 3433E87A294AA3D50042533A /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3433E879294AA3D50042533A /* Notification.swift */; }; 34427692269B5760004CFE1C /* MockMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34427691269B5760004CFE1C /* MockMainViewController.swift */; }; 3442769A269B5CA4004CFE1C /* UITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34427699269B5CA4004CFE1C /* UITestCase.swift */; }; 3449402E25C348B20020E664 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3449402D25C348B20020E664 /* GeneralSettingsViewController.swift */; }; @@ -78,6 +77,7 @@ 347D0FF825954975002451EC /* CalendarCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D0FF725954975002451EC /* CalendarCellViewModel.swift */; }; 347D0FFE259566B5002451EC /* CalendarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D0FFD259566B5002451EC /* CalendarViewModel.swift */; }; 347E6D3E25AA2811009A6716 /* CalendarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347E6D3D25AA2811009A6716 /* CalendarViewModelTests.swift */; }; + 347FF78F2BCB6A5500E5B63B /* Popover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347FF78E2BCB6A5500E5B63B /* Popover.swift */; }; 3487A43825E706F800FCC7D7 /* NextEventViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3487A43725E706F800FCC7D7 /* NextEventViewModel.swift */; }; 3487A43C25E70F5B00FCC7D7 /* NextEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3487A43B25E70F5B00FCC7D7 /* NextEventView.swift */; }; 348B8D0425B2925100E518FE /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348B8D0325B2925100E518FE /* SettingsViewModel.swift */; }; @@ -201,7 +201,6 @@ 3430675D25F6BC15000D4003 /* HistoricalSchedulerTimeConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoricalSchedulerTimeConverter.swift; sourceTree = ""; }; 3430676225F6C130000D4003 /* TrackedHistoricalScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackedHistoricalScheduler.swift; sourceTree = ""; }; 3430ED02259634E00045DA53 /* NSStackView+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSStackView+Rx.swift"; sourceTree = ""; }; - 3433E879294AA3D50042533A /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 343B74C826D168A80081A440 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = UnitTests.xctestplan; path = CalendrTests/UnitTests.xctestplan; sourceTree = SOURCE_ROOT; }; 343B74C926D169E80081A440 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 34427691269B5760004CFE1C /* MockMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMainViewController.swift; sourceTree = ""; }; @@ -256,6 +255,7 @@ 347D0FF725954975002451EC /* CalendarCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarCellViewModel.swift; sourceTree = ""; }; 347D0FFD259566B5002451EC /* CalendarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewModel.swift; sourceTree = ""; }; 347E6D3D25AA2811009A6716 /* CalendarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewModelTests.swift; sourceTree = ""; }; + 347FF78E2BCB6A5500E5B63B /* Popover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Popover.swift; sourceTree = ""; }; 3487A43725E706F800FCC7D7 /* NextEventViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextEventViewModel.swift; sourceTree = ""; }; 3487A43B25E70F5B00FCC7D7 /* NextEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextEventView.swift; sourceTree = ""; }; 348B8D0325B2925100E518FE /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -555,6 +555,7 @@ 34D25E49292F9E2100557E70 /* ImageButton.swift */, 34F128D025968044007DF31C /* Label.swift */, 348E704A25C61B8D00B3B160 /* Radio.swift */, + 347FF78E2BCB6A5500E5B63B /* Popover.swift */, ); path = Components; sourceTree = ""; @@ -567,7 +568,6 @@ 34924CED259FD064009C3450 /* DateFormatter.swift */, 34651F5325E22A3900518C5A /* DateIntervalFormatter.swift */, 34EDE71A2AC46515007C5854 /* Error.swift */, - 3433E879294AA3D50042533A /* Notification.swift */, 34E60D6226A0EA32004DA082 /* NSAccessibilityProtocol.swift */, 34E60D6026A0D6D6004DA082 /* NSAccessibilityProtocol+Rx.swift */, 34B5A09625B0F8A500F7F7ED /* NSButton.swift */, @@ -1015,11 +1015,11 @@ 346C5A4A2BC2D6F60007106C /* NextEventPreview.swift in Sources */, 34B2C59E279FFF0600B52BF3 /* NSGestureRecognizer+Rx.swift in Sources */, 34E004A725B61D5200241419 /* StatusItemViewModel.swift in Sources */, - 3433E87A294AA3D50042533A /* Notification.swift in Sources */, 34D19FC0262299D300B2732C /* NSEdgeInsets.swift in Sources */, 34702168259E761100827AE7 /* Calendar.swift in Sources */, 34F201332693F562006CE2FF /* MockEventListSettings.swift in Sources */, 34C4599C25DEFFDA00561C29 /* BuildConfig.m in Sources */, + 347FF78F2BCB6A5500E5B63B /* Popover.swift in Sources */, 34651F1525E1BB8400518C5A /* Strings.generated.swift in Sources */, 34651F5425E22A3900518C5A /* DateIntervalFormatter.swift in Sources */, 3421DA162693EDEB00056837 /* MockCalendarSettings.swift in Sources */, @@ -1285,7 +1285,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.10.11; + MARKETING_VERSION = 1.11.0; PRODUCT_BUNDLE_IDENTIFIER = br.paker.Calendr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Calendr/Config/Calendr-Bridging-Header.h"; @@ -1311,7 +1311,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.10.11; + MARKETING_VERSION = 1.11.0; PRODUCT_BUNDLE_IDENTIFIER = br.paker.Calendr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Calendr/Config/Calendr-Bridging-Header.h"; diff --git a/Calendr/Calendar/CalendarCellView.swift b/Calendr/Calendar/CalendarCellView.swift index abf91b44..8d40a153 100644 --- a/Calendr/Calendar/CalendarCellView.swift +++ b/Calendr/Calendar/CalendarCellView.swift @@ -162,6 +162,10 @@ class CalendarCellView: NSView { borderLayer.frame = bounds } + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + return true + } + // Prevent propagating event to superview override func mouseExited(with event: NSEvent) { } @@ -172,7 +176,13 @@ class CalendarCellView: NSView { removeTrackingArea(trackingArea) } - addTrackingRect(bounds, owner: self, userData: nil, assumeInside: false) + let trackingArea = NSTrackingArea( + rect: bounds, + options: [.mouseEnteredAndExited, .activeInKeyWindow], + owner: self + ) + + addTrackingArea(trackingArea) } required init?(coder: NSCoder) { diff --git a/Calendr/Components/Popover.swift b/Calendr/Components/Popover.swift new file mode 100644 index 00000000..a895e36e --- /dev/null +++ b/Calendr/Components/Popover.swift @@ -0,0 +1,241 @@ +// +// Popover.swift +// Calendr +// +// Created by Paker on 14/04/24. +// + +import AppKit + +var popovers: [Popover] = [] + +@objc protocol PopoverDelegate { + @objc optional func popoverWillShow() + @objc optional func popoverDidShow() + @objc optional func popoverWillClose() + @objc optional func popoverDidClose() +} + +class Popover: NSObject, PopoverWindowDelegate { + + private var window: PopoverWindow? + private var isClosing = false + + var contentViewController: NSViewController? + var delegate: PopoverDelegate? + var behavior: NSPopover.Behavior = .transient + + func show(from view: NSView) { + present(from: view, edge: .maxY, spacing: 0, single: true) + } + + func push(from view: NSView) { + present(from: view, edge: .minX, spacing: 8, single: false) + } + + func present(from view: NSView, edge: NSRectEdge, spacing: CGFloat, single: Bool) { + + if let window { + return window.move(to: view, edge: edge, spacing: spacing) + } + + if single { + closeAll() + } + + guard let contentViewController else { return } + + delegate?.popoverWillShow?() + + let contentView = contentViewController.view.forAutoLayout() + let container = NSVisualEffectView() + container.maskImage = .mask(withCornerRadius: 12) + container.state = .active + container.addSubview(contentView) + container.edges(to: contentView) + + let window = PopoverWindow() + window.contentView = container + window.isOpaque = false + window.backgroundColor = .clear + window.styleMask = .borderless + window.level = .floating + window.isReleasedWhenClosed = false + window._delegate = self + window.move(to: view, edge: edge, spacing: spacing) + window.activate() + + delegate?.popoverDidShow?() + + self.window = window + + popovers.append(self) + } + + private var isMouseInside: Bool { + guard let window else { return false } + return NSMouseInRect(NSEvent.mouseLocation, window.frame, false) + } + + private func closeAll() { + for popover in popovers { + popover.window?.performClose(nil) + } + } + + func windowDidResignKey(_ notification: Notification) { + + guard !isClosing else { + return + } + + guard NSApp.isActive else { + return closeAll() + } + + guard !isMouseInside else { + return + } + + window?.performClose(nil) + } + + func windowShouldClose(_ sender: NSWindow) -> Bool { + behavior == .transient + } + + func windowWillClose(_ notification: Notification) { + isClosing = true + let wasTop = popovers.last == self + + delegate?.popoverWillClose?() + popovers.removeAll { $0 == self } + delegate?.popoverDidClose?() + + guard wasTop, let newTop = popovers.last?.window else { + return + } + + newTop.activate() + } +} + +@objc protocol PopoverWindowDelegate: NSWindowDelegate { + @objc optional func windowDidClose() +} + +private class PopoverWindow: NSWindow { + + override weak var delegate: NSWindowDelegate? { + set { + assert(newValue == nil) + _delegate = nil + } + get { _delegate } + } + + weak var _delegate: PopoverWindowDelegate? { + didSet { + super.delegate = _delegate + } + } + + override var canBecomeKey: Bool { + return true + } + + override var acceptsFirstResponder: Bool { + return true + } + + override func cancelOperation(_ sender: Any?) { + performClose(nil) + } + + override func performClose(_ sender: Any?) { + guard delegate?.windowShouldClose?(self) != false else { + return + } + close() + } + + override func close() { + super.close() + _delegate?.windowDidClose?() + } + + func activate() { + makeKeyAndOrderFront(nil) + NSRunningApplication.current.activate() + NSApp.activate(ignoringOtherApps: true) + makeFirstResponder(nil) + } + + func move(to anchor: NSView, edge: NSRectEdge, spacing: CGFloat) { + + if let origin = relativePosition(to: anchor, edge: edge, spacing: spacing) { + setFrameOrigin(origin) + } + } + + private func relativePosition(to view: NSView, edge: NSRectEdge, spacing: CGFloat) -> NSPoint? { + + guard let viewWindow = view.window, let screen = NSScreen.main else { + return nil + } + + struct Limits { + let minX: CGFloat + let maxX: CGFloat + let minY: CGFloat + let maxY: CGFloat + } + + let limit = Limits( + minX: screen.visibleFrame.minX, + maxX: screen.visibleFrame.maxX - frame.width, + minY: screen.visibleFrame.minY, + maxY: screen.visibleFrame.maxY - frame.height + ) + + // screen coordinates are inverted + let viewFrame = viewWindow.convertToScreen(view.convert(view.bounds, to: nil)) + + var position: NSPoint? + + let centerX = min(limit.maxX, max(limit.minX, viewFrame.midX - frame.width / 2)) + let centerY = min(limit.maxY, max(limit.minY, viewFrame.midY - frame.height / 2)) + + switch edge { + case .minX: + position = NSPoint(x: max(limit.minX, viewFrame.minX - frame.width - spacing), y: centerY) + case .maxX: + position = NSPoint(x: min(limit.maxX, viewFrame.maxX + spacing), y: centerY) + case .minY: + position = NSPoint(x: centerX, y: min(limit.maxY, viewFrame.maxY + spacing)) + case .maxY: + position = NSPoint(x: centerX, y: max(limit.minY, viewFrame.minY - frame.height - spacing)) + default: + break + } + + return position + } +} + +private extension NSImage { + + static func mask(withCornerRadius radius: CGFloat) -> NSImage { + + let image = NSImage(size: NSSize(width: radius * 2, height: radius * 2), flipped: false) { + NSBezierPath(roundedRect: $0, xRadius: radius, yRadius: radius).fill() + NSColor.black.set() + return true + } + + image.capInsets = NSEdgeInsets(top: radius, left: radius, bottom: radius, right: radius) + image.resizingMode = .stretch + + return image + } +} diff --git a/Calendr/Events/EventDetails/EventDetailsViewController.swift b/Calendr/Events/EventDetails/EventDetailsViewController.swift index 32729045..877414f6 100644 --- a/Calendr/Events/EventDetails/EventDetailsViewController.swift +++ b/Calendr/Events/EventDetails/EventDetailsViewController.swift @@ -8,7 +8,7 @@ import Cocoa import RxSwift -class EventDetailsViewController: NSViewController, NSPopoverDelegate { +class EventDetailsViewController: NSViewController, PopoverDelegate { private let disposeBag = DisposeBag() @@ -35,9 +35,6 @@ class EventDetailsViewController: NSViewController, NSPopoverDelegate { private let viewModel: EventDetailsViewModel - private var animatesClose = true - private var mouseMovedEventMonitor: Any? - private lazy var notesHeightConstraint = notesTextView.height(equalTo: 0) init(viewModel: EventDetailsViewModel) { @@ -420,7 +417,6 @@ class EventDetailsViewController: NSViewController, NSPopoverDelegate { .observe(on: MainScheduler.instance) .subscribe( onNext: { [weak self] in - self?.animatesClose = false self?.view.window?.performClose(nil) }, onError: { error in @@ -430,53 +426,16 @@ class EventDetailsViewController: NSViewController, NSPopoverDelegate { .disposed(by: disposeBag) } - private func setUpAutoClose() { - - mouseMovedEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { [weak self] event in - - if !NSMouseInRect(NSEvent.mouseLocation, NSScreen.main!.frame, false) { - let parentViewController = self?.view.window?.parent?.contentViewController - self?.animatesClose = !(parentViewController is MainViewController) - self?.view.window?.performClose(nil) - } - return event - } - } - - func popoverWillShow(_ notification: Notification) { - - notification.popover.animates = false - - setUpAutoClose() - } - - func popoverDidShow(_ notification: Notification) { - // 🔨 Allow dismiss with the escape key - view.window?.makeKey() - view.window?.makeFirstResponder(self) - - // 🔨 Fix cursor not changing when hovering text - NSApp.activate(ignoringOtherApps: true) + func popoverDidShow() { viewModel.isShowingObserver.onNext(true) } - func popoverWillClose(_ notification: Notification) { - // 🔨 Prevent retain cycle - view.window?.makeFirstResponder(nil) - - notification.popover.animates = animatesClose - - NSEvent.removeMonitor(mouseMovedEventMonitor!) + func popoverDidClose() { viewModel.isShowingObserver.onNext(false) } - func popoverDidClose(_ notification: Notification) { - // 🔨 Prevent retain cycle - notification.popover.contentViewController = nil - } - private func makeLine() -> NSView { let line = NSView.spacer(height: 1) diff --git a/Calendr/Events/EventList/EventView.swift b/Calendr/Events/EventList/EventView.swift index 03055b06..8ad42811 100644 --- a/Calendr/Events/EventList/EventView.swift +++ b/Calendr/Events/EventList/EventView.swift @@ -216,23 +216,17 @@ class EventView: NSView { // do not delay other click events $0.delaysPrimaryMouseButtonEvents = false } - .map { [viewModel] in viewModel.makeDetailsViewModel() } .withUnretained(self) - .flatMapFirst { view, viewModel -> Observable in - let vc = EventDetailsViewController(viewModel: viewModel) - let popover = NSPopover() + .flatMapFirst { [viewModel] view, _ -> Observable in + let vc = EventDetailsViewController(viewModel: viewModel.makeDetailsViewModel()) + let popover = Popover() popover.behavior = .transient popover.contentViewController = vc popover.delegate = vc - popover.show(relativeTo: .zero, of: view, preferredEdge: .minX) + popover.push(from: view) return popover.rx.deallocated } - .bind { [weak self] in - // 🔨 Allow clicking outside to dismiss the main view after dismissing the event details - if NSApp.keyWindow == nil { - self?.window?.makeKey() - } - } + .subscribe() .disposed(by: disposeBag) rx.observe(\.frame) @@ -240,6 +234,10 @@ class EventView: NSView { .disposed(by: disposeBag) } + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + return true + } + override func updateLayer() { super.updateLayer() hoverLayer.frame = bounds diff --git a/Calendr/Extensions/Notification.swift b/Calendr/Extensions/Notification.swift deleted file mode 100644 index 68eea242..00000000 --- a/Calendr/Extensions/Notification.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Notification.swift -// Calendr -// -// Created by Paker on 15/12/2022. -// - -import AppKit - -extension Notification { - - var popover: NSPopover { object as! NSPopover } -} diff --git a/Calendr/Main/Keyboard.swift b/Calendr/Main/Keyboard.swift index 3f5e4aab..30ad1e30 100644 --- a/Calendr/Main/Keyboard.swift +++ b/Calendr/Main/Keyboard.swift @@ -47,12 +47,13 @@ class Keyboard { deinit { removeMonitor() } - var handler: ((NSEvent) -> NSEvent?)? { - willSet { - removeMonitor() - } - didSet { - eventMonitor = handler.flatMap { NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: $0) } + func listen(in vc: NSViewController, handler: @escaping (NSEvent, Key) -> NSEvent?) { + removeMonitor() + eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak vc] event in + if vc?.view.window == event.window, let key = Key.from(event) { + return handler(event, key) + } + return event } } } diff --git a/Calendr/Main/MainViewController.swift b/Calendr/Main/MainViewController.swift index 97517318..5cd8d0d8 100644 --- a/Calendr/Main/MainViewController.swift +++ b/Calendr/Main/MainViewController.swift @@ -9,7 +9,7 @@ import Cocoa import RxSwift import KeyboardShortcuts -class MainViewController: NSViewController, NSPopoverDelegate { +class MainViewController: NSViewController { // ViewControllers private let settingsViewController: SettingsViewController @@ -60,7 +60,7 @@ class MainViewController: NSViewController, NSPopoverDelegate { private let dateProvider: DateProviding private let screenProvider: ScreenProviding private let notificationCenter: NotificationCenter - private var mouseMovedEventMonitor: Any? + private var heightConstraint: NSLayoutConstraint? // MARK: - Initalization @@ -250,13 +250,15 @@ class MainViewController: NSViewController, NSPopoverDelegate { mainStackView.leading(equalTo: view, constant: Constants.MainStackView.margin) mainStackView.trailing(equalTo: view, constant: Constants.MainStackView.margin) - let heightConstraint = mainStackView + heightConstraint = view.height(equalTo: 0).activate() + + let maxHeightConstraint = mainStackView .heightAnchor.constraint(lessThanOrEqualToConstant: 0) .activate() screenProvider.screenObservable .map { 0.9 * $0.visibleFrame.height } - .bind(to: heightConstraint.rx.constant) + .bind(to: maxHeightConstraint.rx.constant) .disposed(by: disposeBag) let popoverView = view.rx.observe(\.superview) @@ -379,7 +381,7 @@ class MainViewController: NSViewController, NSPopoverDelegate { let settingsMenu = NSMenu() - settingsMenu.addItem(withTitle: Strings.Settings.title, action: #selector(openSettings), keyEquivalent: ",") + settingsMenu.addItem(withTitle: Strings.Settings.title, action: #selector(openSettings), keyEquivalent: ",").target = self settingsMenu.addItem(.separator()) @@ -393,7 +395,7 @@ class MainViewController: NSViewController, NSPopoverDelegate { pickerSubmenuItem.view = pickerViewController.view.forAutoLayout() addChild(pickerViewController) - settingsMenu.addItem(withTitle: Strings.search, action: #selector(showSearchInput), keyEquivalent: "f") + settingsMenu.addItem(withTitle: Strings.search, action: #selector(showSearchInput), keyEquivalent: "f").target = self settingsMenu.addItem(.separator()) @@ -407,6 +409,7 @@ class MainViewController: NSViewController, NSPopoverDelegate { @objc private func openSettings() { + settingsViewController.viewWillAppear() presentAsModalWindow(settingsViewController) } @@ -422,11 +425,9 @@ class MainViewController: NSViewController, NSPopoverDelegate { searchInput.isHidden = true } - private func setUpAndShow(_ popover: NSPopover, from button: NSStatusBarButton) { + private func setUpAndShow(_ popover: Popover, from button: NSStatusBarButton) { popover.contentViewController = self - popover.delegate = self - popover.animates = false settingsViewController.rx.viewWillAppear .map(.applicationDefined) @@ -447,44 +448,13 @@ class MainViewController: NSViewController, NSPopoverDelegate { .bind(to: popover.rx.behavior) .disposed(by: popoverDisposeBag) - screenProvider.screenObservable - .withUnretained(popover) { p, _ in p } - .filter(\.animates.isFalse) - .bind { - $0.show(relativeTo: .zero, of: button, preferredEdge: .maxY) - } - .disposed(by: popoverDisposeBag) - } - - private func setUpAutoClose() { - - mouseMovedEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { [view] event in - - if !NSMouseInRect(NSEvent.mouseLocation, NSScreen.main!.frame, false) { - view.window?.performClose(nil) - } - return event - } - } - - func popoverWillShow(_ notification: Notification) { - - setUpAutoClose() - } - - func popoverWillClose(_ notification: Notification) { - - notification.popover.animates = true - - NSEvent.removeMonitor(mouseMovedEventMonitor!) + popover.show(from: button) } override func viewDidLayout() { super.viewDidLayout() - guard let window = view.window, window.isVisible else { return } - - window.setContentSize(contentSize) + heightConstraint?.constant = contentSize.height } private var contentSize: CGSize { @@ -493,16 +463,6 @@ class MainViewController: NSViewController, NSPopoverDelegate { return size } - // 🔨 Dirty hack to force a layout pass before showing the popover - private func forceLayout() { - - let wctrl = NSWindowController(window: NSWindow(contentViewController: self)) - wctrl.window?.orderFrontRegardless() - wctrl.close() - - view.window?.setContentSize(contentSize) - } - private let mainStatusItemClickHandler = StatusItemClickHandler() private func setUpMainStatusItem() { @@ -515,9 +475,7 @@ class MainViewController: NSViewController, NSPopoverDelegate { .flatMapFirst { [weak self] _ -> Observable in guard let self else { return .empty() } - forceLayout() - - let popover = NSPopover() + let popover = Popover() setUpAndShow(popover, from: statusBarButton) return popover.rx.deallocated @@ -540,13 +498,6 @@ class MainViewController: NSViewController, NSPopoverDelegate { statusBarButton.setUpClickHandler(clickHandler) - mainStackView.rx.observe(\.frame) - .bind { [weak self] _ in - guard let self, let window = view.window, window.isVisible else { return } - view.frame.size = contentSize - } - .disposed(by: disposeBag) - statusItemViewModel.image .bind(to: statusBarButton.rx.image) .disposed(by: disposeBag) @@ -597,16 +548,14 @@ class MainViewController: NSViewController, NSPopoverDelegate { .disposed(by: disposeBag) clickHandler.leftClick - .withUnretained(self) - .flatMapFirst { (self, _) in self.isShowingDetails.filter(!).take(1).void() } - .compactMap { viewModel.makeDetailsViewModel() } - .flatMapFirst { viewModel -> Observable in - let vc = EventDetailsViewController(viewModel: viewModel) - let popover = NSPopover() + .flatMapFirst { _ -> Observable in + guard let vm = viewModel.makeDetailsViewModel() else { return .just(()) } + let vc = EventDetailsViewController(viewModel: vm) + let popover = Popover() popover.behavior = .transient popover.contentViewController = vc popover.delegate = vc - popover.show(relativeTo: .zero, of: statusBarButton, preferredEdge: .maxY) + popover.show(from: statusBarButton) return popover.rx.deallocated } .subscribe() @@ -622,18 +571,8 @@ class MainViewController: NSViewController, NSPopoverDelegate { private func setUpKeyboard() { - keyboard.handler = { [weak self] event -> NSEvent? in - guard - let self, - (try? isShowingDetails.value()) == false, - let key = Keyboard.Key.from(event) - else { return event } - - if let vc = presentedViewControllers?.last { - guard key ~= .escape else { return event } - dismiss(vc) - return .none - } + keyboard.listen(in: self) { [weak self] event, key -> NSEvent? in + guard let self else { return event } switch key { case .command("q"): diff --git a/Calendr/Settings/SettingsViewController.swift b/Calendr/Settings/SettingsViewController.swift index 47887985..10bb28ba 100644 --- a/Calendr/Settings/SettingsViewController.swift +++ b/Calendr/Settings/SettingsViewController.swift @@ -12,6 +12,7 @@ class SettingsViewController: NSTabViewController { private let notificationCenter: NotificationCenter private let disposeBag = DisposeBag() + private let keyboard = Keyboard() init( settingsViewModel: SettingsViewModel, @@ -47,6 +48,8 @@ class SettingsViewController: NSTabViewController { setUpAccessibility() setUpBindings() + + setUpKeyboard() } deinit { @@ -135,6 +138,22 @@ class SettingsViewController: NSTabViewController { } } + private func setUpKeyboard() { + + keyboard.listen(in: self) { [weak self] event, key -> NSEvent? in + guard let self else { return event } + + switch key { + case .escape: + view.window?.performClose(nil) + default: + return event + } + + return .none + } + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }