Skip to content

Commit

Permalink
Pick calendars to show next event
Browse files Browse the repository at this point in the history
  • Loading branch information
pakerwreah committed Feb 20, 2023
1 parent b369575 commit eef3aa4
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 79 deletions.
4 changes: 4 additions & 0 deletions Calendr.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
34ABB9EC29A15A030021F3CF /* EventOptionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ABB9EB29A15A030021F3CF /* EventOptionsViewModelTests.swift */; };
34ABB9EE29A163890021F3CF /* ContextMenuFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ABB9ED29A163890021F3CF /* ContextMenuFactory.swift */; };
34ABB9F029A166220021F3CF /* ContextMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ABB9EF29A166220021F3CF /* ContextMenuFactoryTests.swift */; };
34ABB9F229A2AFA30021F3CF /* CursorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ABB9F129A2AFA30021F3CF /* CursorButton.swift */; };
34AC60C626925FA5005312B6 /* PreviewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34AC60C526925FA5005312B6 /* PreviewExtensions.swift */; };
34B1C7C525EDA01F00E5CA80 /* EventIntervalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B1C7C425EDA01F00E5CA80 /* EventIntervalView.swift */; };
34B23E9C25C0C94000DADCD6 /* NSButton+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B23E9B25C0C94000DADCD6 /* NSButton+Rx.swift */; };
Expand Down Expand Up @@ -269,6 +270,7 @@
34ABB9EB29A15A030021F3CF /* EventOptionsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventOptionsViewModelTests.swift; sourceTree = "<group>"; };
34ABB9ED29A163890021F3CF /* ContextMenuFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuFactory.swift; sourceTree = "<group>"; };
34ABB9EF29A166220021F3CF /* ContextMenuFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuFactoryTests.swift; sourceTree = "<group>"; };
34ABB9F129A2AFA30021F3CF /* CursorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CursorButton.swift; sourceTree = "<group>"; };
34AC60C526925FA5005312B6 /* PreviewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewExtensions.swift; sourceTree = "<group>"; };
34B1C7C425EDA01F00E5CA80 /* EventIntervalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventIntervalView.swift; sourceTree = "<group>"; };
34B23E9B25C0C94000DADCD6 /* NSButton+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSButton+Rx.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -509,6 +511,7 @@
isa = PBXGroup;
children = (
34CC382C25B636A6006CBD99 /* Checkbox.swift */,
34ABB9F129A2AFA30021F3CF /* CursorButton.swift */,
341B2B3D25D06A6F00336342 /* Dropdown.swift */,
34D25E49292F9E2100557E70 /* ImageButton.swift */,
34F128D025968044007DF31C /* Label.swift */,
Expand Down Expand Up @@ -873,6 +876,7 @@
34651F5A25E2F08500518C5A /* String.swift in Sources */,
34651F7425E3207200518C5A /* WorkspaceServiceProvider.swift in Sources */,
34286E4625D5CD9C0097EC5D /* WeekNumberCellView.swift in Sources */,
34ABB9F229A2AFA30021F3CF /* CursorButton.swift in Sources */,
341B2B3E25D06A6F00336342 /* Dropdown.swift in Sources */,
347D0F9825952F89002451EC /* AppDelegate.swift in Sources */,
3449403225C348C70020E664 /* CalendarPickerViewController.swift in Sources */,
Expand Down
5 changes: 5 additions & 0 deletions Calendr/Assets/Icons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ enum Icons {
static let unpinned = NSImage(systemName: "pin")
}

enum CalendarPicker {
static let nextEventSelected = NSImage(systemName: "alarm")
static let nextEventUnselected = NSImage(systemName: "moon.zzz")
}

enum Settings {
static let general = NSImage(systemName: "gear")
static let calendars = NSImage(systemName: "calendar.badge.plus")
Expand Down
18 changes: 13 additions & 5 deletions Calendr/Components/Checkbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@

import Cocoa

func Checkbox(title: String = "") -> NSButton {
let checkbox = NSButton(checkboxWithTitle: title, target: nil, action: nil)
checkbox.setContentHuggingPriority(.fittingSizeCompression, for: .horizontal)
checkbox.refusesFirstResponder = true
return checkbox
class Checkbox: CursorButton {

init(title: String = "", cursor: NSCursor? = .pointingHand) {
super.init(cursor: cursor)

self.title = title
setButtonType(.switch)
setContentHuggingPriority(.fittingSizeCompression, for: .horizontal)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
59 changes: 59 additions & 0 deletions Calendr/Components/CursorButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// CursorButton.swift
// Calendr
//
// Created by Paker on 19/02/23.
//

import Cocoa

class CursorButton: NSButton {

private var trackingArea: NSTrackingArea?
private let cursor: NSCursor?

init(cursor: NSCursor? = .pointingHand) {
self.cursor = cursor
super.init(frame: .zero)
refusesFirstResponder = true
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// 🔨 For some reason addCursorRect is not reliable,
// maybe because the container may clip the touch area and mess up
// the mouse enter detection, but to be honest I have no idea why
override func updateTrackingAreas() {

if let trackingArea = trackingArea {
removeTrackingArea(trackingArea)
}

trackingArea = NSTrackingArea(
rect: bounds,
options: [.cursorUpdate, .mouseMoved, .mouseEnteredAndExited, .activeAlways],
owner: self
)

addTrackingArea(trackingArea!)

super.updateTrackingAreas()
}

override func cursorUpdate(with event: NSEvent) {
super.cursorUpdate(with: event)
cursor?.set()
}

override func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)
cursorUpdate(with: event)
}

override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
super.cursorUpdate(with: event)
}
}
44 changes: 2 additions & 42 deletions Calendr/Components/ImageButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,19 @@

import Cocoa

class ImageButton: NSButton {

private var trackingArea: NSTrackingArea?
private let cursor: NSCursor?
class ImageButton: CursorButton {

init(image: NSImage? = nil, cursor: NSCursor? = .pointingHand) {
self.cursor = cursor
super.init(frame: .zero)
super.init(cursor: cursor)
self.image = image

isBordered = false
bezelStyle = .roundRect
refusesFirstResponder = true
showsBorderOnlyWhileMouseInside = true // this actually controls highlight ¯\_(ツ)_/¯
setContentCompressionResistancePriority(.required, for: .horizontal)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// 🔨 For some reason addCursorRect is not reliable,
// maybe because the container may clip the touch area and mess up
// the mouse enter detection, but to be honest I have no idea why
override func updateTrackingAreas() {

if let trackingArea = trackingArea {
removeTrackingArea(trackingArea)
}

trackingArea = NSTrackingArea(
rect: bounds,
options: [.cursorUpdate, .mouseMoved, .mouseEnteredAndExited, .activeAlways],
owner: self
)

addTrackingArea(trackingArea!)

super.updateTrackingAreas()
}

override func cursorUpdate(with event: NSEvent) {
super.cursorUpdate(with: event)
cursor?.set()
}

override func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)
cursorUpdate(with: event)
}

override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
super.cursorUpdate(with: event)
}
}
9 changes: 8 additions & 1 deletion Calendr/Main/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,16 @@ class MainViewController: NSViewController, NSPopoverDelegate {

eventListView = EventListView(viewModel: eventListViewModel)

let nextEventCalendars = Observable
.combineLatest(
calendarPickerViewModel.enabledCalendars,
calendarPickerViewModel.nextEventCalendars
)
.map { $0.filter($1.contains) }

nextEventViewModel = NextEventViewModel(
settings: settingsViewModel,
enabledCalendars: calendarPickerViewModel.enabledCalendars,
nextEventCalendars: nextEventCalendars,
dateProvider: dateProvider,
calendarService: calendarService,
workspace: workspace,
Expand Down
4 changes: 2 additions & 2 deletions Calendr/MenuBar/NextEventViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class NextEventViewModel {

init(
settings: NextEventSettings,
enabledCalendars: Observable<[String]>,
nextEventCalendars: Observable<[String]>,
dateProvider: DateProviding,
calendarService: CalendarServiceProviding,
workspace: WorkspaceServiceProviding,
Expand All @@ -53,7 +53,7 @@ class NextEventViewModel {
let eventsObservable = settings.showEventStatusItem
.flatMapLatest { isEnabled -> Observable<[EventModel]> in

!isEnabled ? .just([]) : enabledCalendars
!isEnabled ? .just([]) : nextEventCalendars
.repeat(when: calendarService.changeObservable)
.flatMapLatest { calendars -> Observable<[EventModel]> in
let start = dateProvider.calendar.startOfDay(for: dateProvider.now)
Expand Down
80 changes: 66 additions & 14 deletions Calendr/Settings/CalendarPickerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,55 +72,107 @@ class CalendarPickerViewController: NSViewController {

private func setUpBindings() {

viewModel.calendars
Observable
.combineLatest(
viewModel.calendars,
viewModel.showNextEvent
)
.observe(on: MainScheduler.instance)
.compactMap { [weak self] calendars -> [NSView]? in
.compactMap { [weak self] calendars, showNextEvent -> [NSView]? in
guard let self = self else { return nil }

return Dictionary(grouping: calendars, by: { $0.account })
.sorted(by: \.key.localizedLowercase)
.flatMap { account, calendars in
self.makeCalendarSection(
title: account,
calendars: calendars.sorted(by: \.title.localizedLowercase)
calendars: calendars.sorted(by: \.title.localizedLowercase),
showNextEvent: showNextEvent
)
}
}
.bind(to: contentStackView.rx.arrangedSubviews)
.disposed(by: disposeBag)
}

private func makeCalendarSection(title: String, calendars: [CalendarModel]) -> [NSView] {
private func makeCalendarSection(title: String, calendars: [CalendarModel], showNextEvent: Bool) -> [NSView] {

let label = Label(text: title, font: .systemFont(ofSize: 11, weight: .semibold))
label.textColor = .secondaryLabelColor

let stackView = NSStackView(
views: calendars.compactMap(makeCalendarItem)
views: calendars.compactMap {
NSStackView(
views: [
makeCalendarItemEnabled($0),
showNextEvent ? makeCalendarItemNextEvent($0) : nil
]
.compact()
)
}
)
.with(orientation: .vertical)
.with(alignment: .left)

return [label, NSStackView(views: [.dummy, stackView])]
}

private func makeCalendarItem(_ calendar: CalendarModel) -> NSView {
private func bindCalendarItem(
button: NSButton,
identifier: String,
selected: Observable<[String]>,
toggle: AnyObserver<String>
) {
selected
.map { $0.contains(identifier) ? .on : .off }
.bind(to: button.rx.state)
.disposed(by: disposeBag)

button.rx.click
.bind { toggle.onNext(identifier) }
.disposed(by: disposeBag)
}

private func makeCalendarItemEnabled(_ calendar: CalendarModel) -> NSView {

let checkbox = Checkbox(title: calendar.title)
checkbox.setTitleColor(color: calendar.color)

viewModel.enabledCalendars
.map { $0.contains(calendar.identifier) ? .on : .off }
.bind(to: checkbox.rx.state)
bindCalendarItem(
button: checkbox,
identifier: calendar.identifier,
selected: viewModel.enabledCalendars,
toggle: viewModel.toggleCalendar
)

return checkbox
}

private func makeCalendarItemNextEvent(_ calendar: CalendarModel) -> NSView {

let selectedIcon = Icons.CalendarPicker.nextEventSelected.with(size: 11)
let unselectedIcon = Icons.CalendarPicker.nextEventUnselected.with(size: 11)
let button = ImageButton()
button.setButtonType(.toggle)

view.rx.updateLayer
.map { unselectedIcon.with(color: .secondaryLabelColor) }
.bind(to: button.rx.image)
.disposed(by: disposeBag)

checkbox.rx.tap
.bind { [viewModel] in
viewModel.toggleCalendar.onNext(calendar.identifier)
}
view.rx.updateLayer
.map { selectedIcon.with(color: .textColor) }
.bind(to: button.rx.alternateImage)
.disposed(by: disposeBag)

return checkbox
bindCalendarItem(
button: button,
identifier: calendar.identifier,
selected: viewModel.nextEventCalendars,
toggle: viewModel.toggleNextEvent
)

return button
}

required init?(coder: NSCoder) {
Expand Down
Loading

0 comments on commit eef3aa4

Please sign in to comment.