diff --git a/Calendr/Calendar/CalendarCellView.swift b/Calendr/Calendar/CalendarCellView.swift index b9e6aea3..abf91b44 100644 --- a/Calendr/Calendar/CalendarCellView.swift +++ b/Calendr/Calendar/CalendarCellView.swift @@ -15,6 +15,7 @@ class CalendarCellView: NSView { private let viewModel: Observable private let hoverObserver: AnyObserver private let clickObserver: AnyObserver + private let doubleClickObserver: AnyObserver private let calendarScaling: Observable private let label = Label() @@ -25,12 +26,14 @@ class CalendarCellView: NSView { viewModel: Observable, hoverObserver: AnyObserver, clickObserver: AnyObserver, + doubleClickObserver: AnyObserver, calendarScaling: Observable ) { self.viewModel = viewModel self.hoverObserver = hoverObserver self.clickObserver = clickObserver + self.doubleClickObserver = doubleClickObserver self.calendarScaling = calendarScaling super.init(frame: .zero) @@ -143,6 +146,11 @@ class CalendarCellView: NSView { .bind(to: clickObserver) .disposed(by: disposeBag) + rx.doubleClick + .withLatestFrom(viewModel.map(\.date)) + .bind(to: doubleClickObserver) + .disposed(by: disposeBag) + rx.mouseEntered .withLatestFrom(viewModel.map(\.date)) .bind(to: hoverObserver) diff --git a/Calendr/Calendar/CalendarView.swift b/Calendr/Calendar/CalendarView.swift index 05cd88cb..a34ef267 100644 --- a/Calendr/Calendar/CalendarView.swift +++ b/Calendr/Calendar/CalendarView.swift @@ -15,18 +15,21 @@ class CalendarView: NSView { private let viewModel: CalendarViewModel private let hoverObserver: AnyObserver private let clickObserver: AnyObserver + private let doubleClickObserver: AnyObserver private let gridView = NSGridView(numberOfColumns: 8, rows: 7) init( viewModel: CalendarViewModel, hoverObserver: AnyObserver, - clickObserver: AnyObserver + clickObserver: AnyObserver, + doubleClickObserver: AnyObserver ) { self.viewModel = viewModel self.hoverObserver = hoverObserver self.clickObserver = clickObserver + self.doubleClickObserver = doubleClickObserver super.init(frame: .zero) @@ -145,6 +148,7 @@ class CalendarView: NSView { viewModel: cellViewModel, hoverObserver: hoverObserver, clickObserver: clickObserver, + doubleClickObserver: doubleClickObserver, calendarScaling: viewModel.calendarScaling ) gridView.cell(atColumnIndex: 1 + day % 7, rowIndex: 1 + day / 7).contentView = cellView diff --git a/Calendr/Config/Calendr.entitlements b/Calendr/Config/Calendr.entitlements index 59f1bdbd..d9120b5a 100644 --- a/Calendr/Config/Calendr.entitlements +++ b/Calendr/Config/Calendr.entitlements @@ -8,5 +8,9 @@ com.apple.security.personal-information.calendars + com.apple.security.temporary-exception.apple-events + + com.apple.ical + diff --git a/Calendr/Config/Info.plist b/Calendr/Config/Info.plist index c247eec4..b6e687d5 100644 --- a/Calendr/Config/Info.plist +++ b/Calendr/Config/Info.plist @@ -34,5 +34,7 @@ NSApplication NSRemindersUsageDescription Calendr needs access to display your reminders. + NSAppleEventsUsageDescription + Calendr needs access to open Calendar app at the selected date. diff --git a/Calendr/Extensions/NSGestureRecognizer+Rx.swift b/Calendr/Extensions/NSGestureRecognizer+Rx.swift index a16dca54..4b2d1b94 100644 --- a/Calendr/Extensions/NSGestureRecognizer+Rx.swift +++ b/Calendr/Extensions/NSGestureRecognizer+Rx.swift @@ -24,6 +24,7 @@ private class GestureProxy { extension Reactive where Base: NSView { var click: Observable { click { _ in } } + var doubleClick: Observable { click { $0.numberOfClicksRequired = 2 } } func click (_ configure: @escaping (T) -> Void) -> Observable { gesture(configure) diff --git a/Calendr/Main/MainViewController.swift b/Calendr/Main/MainViewController.swift index 13c86570..231340e2 100644 --- a/Calendr/Main/MainViewController.swift +++ b/Calendr/Main/MainViewController.swift @@ -42,6 +42,7 @@ class MainViewController: NSViewController, NSPopoverDelegate { private let disposeBag = DisposeBag() private var popoverDisposeBag = DisposeBag() private let dateClick = PublishSubject() + private let dateDoubleClick = PublishSubject() private let initialDate: BehaviorSubject private let selectedDate = PublishSubject() private let isShowingDetails = BehaviorSubject(value: false) @@ -127,7 +128,8 @@ class MainViewController: NSViewController, NSPopoverDelegate { calendarView = CalendarView( viewModel: calendarViewModel, hoverObserver: hoverObserver, - clickObserver: dateClick.asObserver() + clickObserver: dateClick.asObserver(), + doubleClickObserver: dateDoubleClick.asObserver() ) let eventListEventsObservable = calendarViewModel.focusedDateEventsObservable @@ -317,12 +319,20 @@ class MainViewController: NSViewController, NSPopoverDelegate { } .disposed(by: disposeBag) - calendarBtn.rx.tap.bind { [workspace] in - if let appUrl = workspace.urlForApplication(toOpen: URL(string: "webcal://")!) { - workspace.open(appUrl) + dateDoubleClick + .observe(on: ConcurrentDispatchQueueScheduler(qos: .userInteractive)) + .bind { [weak self] date in + self?.openCalendar(at: date, mode: .day) } - } - .disposed(by: disposeBag) + .disposed(by: disposeBag) + + calendarBtn.rx.tap + .withLatestFrom(selectedDate) + .observe(on: ConcurrentDispatchQueueScheduler(qos: .userInteractive)) + .bind { [weak self] date in + self?.openCalendar(at: date, mode: .month) + } + .disposed(by: disposeBag) searchInput.rx.text .skipNil() @@ -651,6 +661,30 @@ class MainViewController: NSViewController, NSPopoverDelegate { return dateSelector } + + // MARK: - Scripts + + private enum CalendarViewMode: String { + case day + case month + } + + private func openCalendar(at date: Date, mode: CalendarViewMode) { + do { + let dateString = DateFormatter().with(style: .full).string(from: date) + try runScript(""" + tell application "Calendar" + switch view to \(mode) view + view calendar at date ("\(dateString)") + end tell + """) + } catch { + debugPrint("Open Calendar script failed, fallback to workspace method") + if let appUrl = workspace.urlForApplication(toOpen: URL(string: "webcal://")!) { + workspace.open(appUrl) + } + } + } } private enum Constants { @@ -659,3 +693,23 @@ private enum Constants { static let margin: CGFloat = 8 } } + +// MARK: - Apple Script + +private enum ScriptError: Error { + case source + case compile + case execute +} + +private func runScript(_ source: String) throws { + guard let script = NSAppleScript(source: source) else { + throw ScriptError.source + } + guard script.compileAndReturnError(nil) else { + throw ScriptError.compile + } + if script.executeAndReturnError(nil).description.isEmpty { + throw ScriptError.execute + } +} diff --git a/Calendr/Previews/CalendarViewPreview.swift b/Calendr/Previews/CalendarViewPreview.swift index a750f00a..5f072a7d 100644 --- a/Calendr/Previews/CalendarViewPreview.swift +++ b/Calendr/Previews/CalendarViewPreview.swift @@ -37,7 +37,8 @@ struct CalendarViewPreview: PreviewProvider { notificationCenter: notificationCenter ), hoverObserver: hovered.asObserver(), - clickObserver: selected.asObserver() + clickObserver: selected.asObserver(), + doubleClickObserver: .dummy() ) .preview() .fixedSize()