You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1088 lines
43 KiB

// Copyright © 2015 Abhishek Banthia
import Cocoa
import CoreLoggerKit
import CoreModelKit
import EventKit
struct PanelConstants {
static let notReallyButtonTitle = "Not Really"
static let feedbackString = "Mind giving feedback?"
static let noThanksTitle = "No, thanks"
static let yesWithQuestionMark = "Yes?"
static let yesWithExclamation = "Yes!"
static let modernSliderPointsInADay = 96
}
class ParentPanelController: NSWindowController {
private var futureSliderObserver: NSKeyValueObservation?
private var userFontSizeSelectionObserver: NSKeyValueObservation?
private var futureSliderRangeObserver: NSKeyValueObservation?
private var eventStoreChangedNotification: NSObjectProtocol?
var dateFormatter = DateFormatter()
var futureSliderValue: Int = 0
var parentTimer: Repeater?
var showReviewCell: Bool = false
var previousPopoverRow: Int = -1
var additionalOptionsPopover: NSPopover?
var datasource: TimezoneDataSource?
private var feedbackWindow: AppFeedbackWindowController?
private var notePopover: NotesPopover?
private lazy var oneWindow: OneWindowController? = {
let preferencesStoryboard = NSStoryboard(name: "Preferences", bundle: nil)
return preferencesStoryboard.instantiateInitialController() as? OneWindowController
}()
@IBOutlet var mainTableView: PanelTableView!
@IBOutlet var stackView: NSStackView!
@IBOutlet var scrollViewHeight: NSLayoutConstraint!
@IBOutlet var reviewView: NSView!
@IBOutlet var leftField: NSTextField!
@IBOutlet var sharingButton: NSButton!
@IBOutlet var leftButton: NSButton!
@IBOutlet var rightButton: NSButton!
@IBOutlet var shutdownButton: NSButton!
@IBOutlet var preferencesButton: NSButton!
@IBOutlet var pinButton: NSButton!
@IBOutlet var roundedDateView: NSView!
// Modern Slider
public var currentCenterIndexPath: Int = -1
public var closestQuarterTimeRepresentation: Date?
@IBOutlet var modernSlider: NSCollectionView!
@IBOutlet var modernSliderLabel: NSTextField!
@IBOutlet var modernContainerView: ModernSliderContainerView!
@IBOutlet var goBackwardsButton: NSButton!
@IBOutlet var goForwardButton: NSButton!
@IBOutlet var resetModernSliderButton: NSButton!
// Upcoming Events
@IBOutlet var upcomingEventCollectionView: NSCollectionView!
@IBOutlet var upcomingEventContainerView: NSView!
public var upcomingEventsDataSource: UpcomingEventsDataSource?
var defaultPreferences: [Data] {
return DataStore.shared().timezones()
}
deinit {
datasource = nil
if let eventStoreNotif = eventStoreChangedNotification {
NotificationCenter.default.removeObserver(eventStoreNotif)
}
[futureSliderObserver, userFontSizeSelectionObserver, futureSliderRangeObserver].forEach {
$0?.invalidate()
}
}
private func setupObservers() {
futureSliderObserver = UserDefaults.standard.observe(\.displayFutureSlider, options: [.new]) { _, change in
if let changedValue = change.newValue {
if changedValue == 0 {
if self.modernContainerView != nil {
self.modernContainerView.isHidden = false
}
} else if changedValue == 1 {
if self.modernContainerView != nil {
self.modernContainerView.isHidden = true
}
} else {
if self.modernContainerView != nil {
self.modernContainerView.isHidden = true
}
}
}
}
userFontSizeSelectionObserver = UserDefaults.standard.observe(\.userFontSize, options: [.new]) { _, change in
if let newFontSize = change.newValue {
Logger.log(object: ["FontSize": newFontSize], for: "User Font Size Preference")
self.mainTableView.reloadData()
self.setScrollViewConstraint()
}
}
futureSliderRangeObserver = UserDefaults.standard.observe(\.sliderDayRange, options: [.new]) { _, change in
if change.newValue != nil {
self.adjustFutureSliderBasedOnPreferences()
if self.modernSlider != nil {
self.modernSlider.reloadData()
}
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Setup table
mainTableView.backgroundColor = NSColor.clear
mainTableView.selectionHighlightStyle = .none
mainTableView.enclosingScrollView?.hasVerticalScroller = false
if #available(OSX 11.0, *) {
mainTableView.style = .plain
}
// Setup images
let sharedThemer = Themer.shared()
shutdownButton.image = sharedThemer.shutdownImage()
preferencesButton.image = sharedThemer.preferenceImage()
pinButton.image = sharedThemer.pinImage()
sharingButton.image = sharedThemer.sharingImage()
sharingButton.alternateImage = sharedThemer.sharingImageAlternate()
// Setup KVO observers for user default changes
setupObservers()
updateReviewViewFontColor()
// Set the background color of the bottom buttons view to something different to indicate we're not in a release candidate
#if DEBUG
stackView.arrangedSubviews.last?.layer?.backgroundColor = NSColor(deviceRed: 255.0 / 255.0,
green: 150.0 / 255.0,
blue: 122.0 / 255.0,
alpha: 0.5).cgColor
stackView.arrangedSubviews.last?.toolTip = "Debug Mode"
#endif
// Setup layers
reviewView.wantsLayer = true
// Setup notifications
NotificationCenter.default.addObserver(self,
selector: #selector(themeChanged),
name: Notification.Name.themeDidChange,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(systemTimezoneDidChange),
name: NSNotification.Name.NSSystemTimeZoneDidChange,
object: nil)
NotificationCenter.default.addObserver(forName: DataStore.didSyncFromExternalSourceNotification,
object: self,
queue: OperationQueue.main)
{ [weak self] _ in
if let sSelf = self {
sSelf.mainTableView.reloadData()
sSelf.setScrollViewConstraint()
}
}
// Setup upcoming events view
upcomingEventContainerView.setAccessibility("UpcomingEventView")
determineUpcomingViewVisibility()
setupUpcomingEventViewCollectionViewIfNeccesary()
// Setup colors based on the curren theme
themeChanged()
// UI adjustments based on user preferences
if DataStore.shared().timezones().isEmpty || DataStore.shared().shouldDisplay(.futureSlider) == false {
if modernContainerView != nil {
modernContainerView.isHidden = true
}
} else if let value = DataStore.shared().retrieve(key: UserDefaultKeys.displayFutureSliderKey) as? NSNumber {
if value.intValue == 1 {
if modernContainerView != nil {
modernContainerView.isHidden = true
}
} else if value.intValue == 0 {
// Floating Window doesn't support modern slider yet!
if modernContainerView != nil {
modernContainerView.isHidden = false
}
}
}
// More UI adjustments
sharingButton.sendAction(on: .leftMouseDown)
adjustFutureSliderBasedOnPreferences()
setupModernSliderIfNeccessary()
if roundedDateView != nil {
setupRoundedDateView()
}
}
private func setupRoundedDateView() {
roundedDateView.wantsLayer = true
roundedDateView.layer?.cornerRadius = 12.0
roundedDateView.layer?.masksToBounds = false
roundedDateView.layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor
}
@objc func systemTimezoneDidChange() {
OperationQueue.main.addOperation {
/*
let locationController = LocationController.sharedController()
locationController.determineAndRequestLocationAuthorization()*/
self.updateHomeObject(with: TimeZone.autoupdatingCurrent.identifier,
coordinates: nil)
}
}
private func updateHomeObject(with customLabel: String, coordinates: CLLocationCoordinate2D?) {
let timezones = DataStore.shared().timezones()
var timezoneObjects: [TimezoneData] = []
for timezone in timezones {
if let model = TimezoneData.customObject(from: timezone) {
timezoneObjects.append(model)
}
}
for timezoneObject in timezoneObjects where timezoneObject.isSystemTimezone == true {
timezoneObject.setLabel(customLabel)
timezoneObject.formattedAddress = customLabel
if let latlong = coordinates {
timezoneObject.longitude = latlong.longitude
timezoneObject.latitude = latlong.latitude
}
}
var datas: [Data] = []
for updatedObject in timezoneObjects {
guard let dataObject = NSKeyedArchiver.clocker_archive(with: updatedObject) else {
continue
}
datas.append(dataObject)
}
DataStore.shared().setTimezones(datas)
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
appDelegate.setupMenubarTimer()
}
}
func determineUpcomingViewVisibility() {
let showUpcomingEventView = DataStore.shared().shouldDisplay(ViewType.upcomingEventView)
if showUpcomingEventView == false {
upcomingEventContainerView?.isHidden = true
} else {
upcomingEventContainerView?.isHidden = false
setupUpcomingEventView()
eventStoreChangedNotification = NotificationCenter.default.addObserver(forName: NSNotification.Name.EKEventStoreChanged, object: self, queue: OperationQueue.main) { _ in
self.fetchCalendarEvents()
}
}
}
private func adjustFutureSliderBasedOnPreferences() {
setTimezoneDatasourceSlider(sliderValue: 0)
updateTableContent()
}
private func setupUpcomingEventView() {
let eventCenter = EventCenter.sharedCenter()
if eventCenter.calendarAccessGranted() {
// Nice. Events will be retrieved when we open the panel
} else if eventCenter.calendarAccessNotDetermined() {
upcomingEventCollectionView.reloadData()
} else {
removeUpcomingEventView()
}
themeChanged()
}
private func updateReviewViewFontColor() {
let textColor = Themer.shared().mainTextColor()
leftField.textColor = textColor
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let styleAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: NSFont(name: "Avenir-Light", size: 13) ?? NSFont.systemFont(ofSize: 13),
]
let leftButtonAttributedTitle = NSAttributedString(string: leftButton.title, attributes: styleAttributes)
leftButton.attributedTitle = leftButtonAttributedTitle
let rightButtonAttributedTitle = NSAttributedString(string: rightButton.title, attributes: styleAttributes)
rightButton.attributedTitle = rightButtonAttributedTitle
}
@objc func themeChanged() {
let sharedThemer = Themer.shared()
if upcomingEventContainerView?.isHidden == false {
upcomingEventContainerView?.layer?.backgroundColor = NSColor.clear.cgColor
}
shutdownButton.image = sharedThemer.shutdownImage()
preferencesButton.image = sharedThemer.preferenceImage()
pinButton.image = sharedThemer.pinImage()
sharingButton.image = sharedThemer.sharingImage()
sharingButton.alternateImage = sharedThemer.sharingImageAlternate()
if roundedDateView != nil {
roundedDateView.layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor
}
updateReviewViewFontColor()
}
override func windowDidLoad() {
super.windowDidLoad()
additionalOptionsPopover = NSPopover()
}
func screenHeight() -> CGFloat {
guard let main = NSScreen.main else { return 100 }
let mouseLocation = NSEvent.mouseLocation
var current = main.frame.height
let activeScreens = NSScreen.screens.filter { current -> Bool in
NSMouseInRect(mouseLocation, current.frame, false)
}
if let main = activeScreens.first {
current = main.frame.height
}
return current
}
func invalidateMenubarTimer() {
parentTimer = nil
}
private func getAdjustedRowHeight(for object: TimezoneData?, _ currentHeight: CGFloat) -> CGFloat {
let userFontSize: NSNumber = DataStore.shared().retrieve(key: UserDefaultKeys.userFontSizePreference) as? NSNumber ?? 4
let shouldShowSunrise = DataStore.shared().shouldDisplay(.sunrise)
var newHeight = currentHeight
if newHeight <= 68.0 {
newHeight = 60.0
}
if newHeight >= 68.0 {
newHeight = userFontSize == 4 ? 68.0 : 68.0
if let note = object?.note, note.isEmpty == false {
newHeight += 20
} else if let obj = object,
TimezoneDataOperations(with: obj, store: DataStore.shared()).nextDaylightSavingsTransitionIfAvailable(with: futureSliderValue) != nil
{
newHeight += 20
}
}
if newHeight >= 88.0 {
// Set it to 90 expicity in case the row height is calculated be higher.
newHeight = 88.0
if let note = object?.note, note.isEmpty, let obj = object, TimezoneDataOperations(with: obj, store: DataStore.shared()).nextDaylightSavingsTransitionIfAvailable(with: futureSliderValue) == nil {
newHeight -= 20.0
}
}
if shouldShowSunrise, object?.selectionType == .city {
newHeight += 8.0
}
if object?.isSystemTimezone == true {
newHeight += 5
}
newHeight += mainTableView.intercellSpacing.height
return newHeight
}
func setScrollViewConstraint() {
var totalHeight: CGFloat = 0.0
let preferences = defaultPreferences
for cellIndex in 0 ..< preferences.count {
let currentObject = TimezoneData.customObject(from: preferences[cellIndex])
let rowRect = mainTableView.rect(ofRow: cellIndex)
totalHeight += getAdjustedRowHeight(for: currentObject, rowRect.size.height)
}
// This is for the Add Cell View case
if preferences.isEmpty {
scrollViewHeight.constant = 100.0
return
}
if let userFontSize = DataStore.shared().retrieve(key: UserDefaultKeys.userFontSizePreference) as? NSNumber {
if userFontSize == 4 {
scrollViewHeight.constant = totalHeight + CGFloat(userFontSize.intValue * 2)
} else {
scrollViewHeight.constant = totalHeight + CGFloat(userFontSize.intValue * 2) * 3.0
}
}
if DataStore.shared().shouldDisplay(ViewType.upcomingEventView) {
if scrollViewHeight.constant > (screenHeight() - 160) {
scrollViewHeight.constant = (screenHeight() - 160)
}
} else {
if scrollViewHeight.constant > (screenHeight() - 100) {
scrollViewHeight.constant = (screenHeight() - 100)
}
}
if DataStore.shared().shouldDisplay(.futureSlider) {
let isModernSliderDisplayed = DataStore.shared().retrieve(key: UserDefaultKeys.displayFutureSliderKey) as? NSNumber ?? 0
if isModernSliderDisplayed == 0 {
if scrollViewHeight.constant >= (screenHeight() - 200) {
scrollViewHeight.constant = (screenHeight() - 300)
}
} else {
if scrollViewHeight.constant >= (screenHeight() - 200) {
scrollViewHeight.constant = (screenHeight() - 200)
}
}
}
}
func updateDefaultPreferences() {
PerfLogger.startMarker("Update Default Preferences")
updatePanelColor()
let store = DataStore.shared()
let defaults = store.timezones()
let convertedTimezones = defaults.map { data -> TimezoneData in
TimezoneData.customObject(from: data)!
}
datasource = TimezoneDataSource(items: convertedTimezones, store: store)
mainTableView.dataSource = datasource
mainTableView.delegate = datasource
mainTableView.panelDelegate = datasource
updateDatasource(with: convertedTimezones)
PerfLogger.endMarker("Update Default Preferences")
}
func updateDatasource(with timezones: [TimezoneData]) {
datasource?.setItems(items: timezones)
datasource?.setSlider(value: futureSliderValue)
if let userFontSize = DataStore.shared().retrieve(key: UserDefaultKeys.userFontSizePreference) as? NSNumber {
scrollViewHeight.constant = CGFloat(timezones.count) * (mainTableView.rowHeight + CGFloat(userFontSize.floatValue * 1.5))
setScrollViewConstraint()
mainTableView.reloadData()
}
}
func updatePanelColor() {
window?.alphaValue = 1.0
}
func setTimezoneDatasourceSlider(sliderValue: Int) {
futureSliderValue = sliderValue
datasource?.setSlider(value: sliderValue)
}
@IBAction func openPreferences(_: NSButton) {
updatePopoverDisplayState() // Popover's class has access to all timezones. Need to close the popover, so that we don't have two copies of selections
openPreferencesWindow()
}
func deleteTimezone(at row: Int) {
var defaults = defaultPreferences
// Remove from panel
defaults.remove(at: row)
DataStore.shared().setTimezones(defaults)
updateDefaultPreferences()
NotificationCenter.default.post(name: Notification.Name.customLabelChanged,
object: nil)
// Now log!
Logger.log(object: nil, for: "Deleted Timezone Through Swipe")
}
private lazy var menubarTitleHandler = MenubarTitleProvider(with: DataStore.shared(), eventStore: EventCenter.sharedCenter())
private static let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: NSFont.monospacedDigitSystemFont(ofSize: 13.0, weight: NSFont.Weight.regular),
NSAttributedString.Key.baselineOffset: 0.1]
@objc func updateTime() {
let store = DataStore.shared()
let menubarCount = store.menubarTimezones()?.count ?? 0
if menubarCount >= 1 || store.shouldDisplay(.showMeetingInMenubar) == true {
if let status = (NSApplication.shared.delegate as? AppDelegate)?.statusItemForPanel() {
if store.shouldDisplay(.menubarCompactMode) {
status.updateCompactMenubar()
} else {
status.statusItem.button?.attributedTitle = NSAttributedString(string: menubarTitleHandler.titleForMenubar() ?? "",
attributes: ParentPanelController.attributes)
}
}
}
let preferences = store.timezones()
if modernSlider != nil, modernSlider.isHidden == false, modernContainerView.currentlyInFocus == false {
if currentCenterIndexPath != -1, currentCenterIndexPath != modernSlider.numberOfItems(inSection: 0) / 2 {
// User is currently scrolling, return!
return
}
}
let hoverRow = mainTableView.hoverRow
stride(from: 0, to: preferences.count, by: 1).forEach {
let current = preferences[$0]
if $0 < mainTableView.numberOfRows,
let cellView = mainTableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? TimezoneCellView,
let model = TimezoneData.customObject(from: current)
{
if modernContainerView != nil, modernSlider.isHidden == false, modernContainerView.currentlyInFocus {
return
}
let dataOperation = TimezoneDataOperations(with: model, store: DataStore.shared())
cellView.time.stringValue = dataOperation.time(with: futureSliderValue)
cellView.sunriseSetTime.stringValue = dataOperation.formattedSunriseTime(with: futureSliderValue)
cellView.sunriseSetTime.lineBreakMode = .byClipping
if $0 != hoverRow {
cellView.relativeDate.stringValue = dataOperation.date(with: futureSliderValue, displayType: .panel)
}
cellView.currentLocationIndicator.isHidden = !model.isSystemTimezone
cellView.sunriseImage.image = model.isSunriseOrSunset ? Themer.shared().sunriseImage() : Themer.shared().sunsetImage()
cellView.sunriseImage.contentTintColor = model.isSunriseOrSunset ? NSColor.systemYellow : NSColor.systemOrange
if let note = model.note, !note.isEmpty {
cellView.noteLabel.stringValue = note
} else if let value = TimezoneDataOperations(with: model, store: DataStore.shared()).nextDaylightSavingsTransitionIfAvailable(with: futureSliderValue) {
cellView.noteLabel.stringValue = value
} else {
cellView.noteLabel.stringValue = UserDefaultKeys.emptyString
}
cellView.layout(with: model)
// TODO: Update modern slider
}
}
}
@discardableResult
func showNotesPopover(forRow row: Int, relativeTo _: NSRect, andButton target: NSButton!) -> Bool {
let defaults = DataStore.shared().timezones()
guard let popover = additionalOptionsPopover else {
assertionFailure("Data was unexpectedly nil")
return false
}
var correctRow = row
target.image = Themer.shared().extraOptionsHighlightedImage()
popover.animates = true
if notePopover == nil {
notePopover = NotesPopover(nibName: NSNib.Name.notesPopover, bundle: nil)
popover.behavior = .applicationDefined
popover.delegate = self
}
// Found a case where row number was 8 but we had only 2 timezones
if correctRow >= defaults.count {
correctRow = defaults.count - 1
}
let current = defaults[correctRow]
if let model = TimezoneData.customObject(from: current) {
notePopover?.setDataSource(data: model)
notePopover?.setRow(row: correctRow)
notePopover?.set(timezones: defaults)
popover.contentViewController = notePopover
notePopover?.set(with: popover)
return true
}
return false
}
func dismissRowActions() {
mainTableView.rowActionsVisible = false
}
@objc func updateTableContent() {
mainTableView.reloadData()
}
@objc private func openPreferencesWindow() {
oneWindow?.openGeneralPane()
}
@IBAction func dismissNextEventLabel(_: NSButton) {
let eventCenter = EventCenter.sharedCenter()
let now = Date()
if let events = eventCenter.eventsForDate[NSCalendar.autoupdatingCurrent.startOfDay(for: now)], events.isEmpty == false {
if let upcomingEvent = eventCenter.nextOccuring(events), let meetingLink = upcomingEvent.meetingURL {
NSWorkspace.shared.open(meetingLink)
}
} else {
removeUpcomingEventView()
}
}
func removeUpcomingEventView() {
OperationQueue.main.addOperation {
if self.upcomingEventCollectionView != nil {
if self.stackView.arrangedSubviews.contains(self.upcomingEventContainerView!), self.upcomingEventContainerView?.isHidden == false {
self.upcomingEventContainerView?.isHidden = true
UserDefaults.standard.set("NO", forKey: UserDefaultKeys.showUpcomingEventView)
Logger.log(object: ["Removed": "YES"], for: "Removed Upcoming Event View")
}
}
}
}
@IBAction func calendarButtonAction(_ sender: NSButton) {
if sender.title == NSLocalizedString("Click here to start.",
comment: "Button Title for no Calendar access")
{
showPermissionsWindow()
} else {
retrieveCalendarEvents()
}
}
private func showPermissionsWindow() {
oneWindow?.openPermissionsPane()
NSApp.activate(ignoringOtherApps: true)
}
func retrieveCalendarEvents() {
PerfLogger.startMarker("Retrieve Calendar Events")
let eventCenter = EventCenter.sharedCenter()
if eventCenter.calendarAccessGranted() {
fetchCalendarEvents()
} else if eventCenter.calendarAccessNotDetermined() {
/* Wait till we get the thumbs up. */
} else {
removeUpcomingEventView()
}
PerfLogger.endMarker("Retrieve Calendar Events")
}
@IBAction func shareAction(_ sender: NSButton) {
let copyAllTimes = retrieveAllTimes()
let pasteboard = NSPasteboard.general
pasteboard.declareTypes([.string], owner: nil)
pasteboard.setString(copyAllTimes, forType: .string)
self.window?.contentView?.makeToast("Copied to Clipboard".localized())
}
@IBAction func convertToFloatingWindow(_: NSButton) {
guard let sharedDelegate = NSApplication.shared.delegate as? AppDelegate
else {
assertionFailure("Data was unexpectedly nil")
return
}
let showAppInForeground = DataStore.shared().shouldDisplay(ViewType.showAppInForeground)
let inverseSelection = showAppInForeground ? NSNumber(value: 0) : NSNumber(value: 1)
UserDefaults.standard.set(inverseSelection, forKey: UserDefaultKeys.showAppInForeground)
close()
if inverseSelection.isEqual(to: NSNumber(value: 1)) {
sharedDelegate.setupFloatingWindow(false)
} else {
sharedDelegate.setupFloatingWindow(true)
updateDefaultPreferences()
}
let mode = inverseSelection.isEqual(to: NSNumber(value: 1)) ? "Floating Mode" : "Menubar Mode"
Logger.log(object: ["displayMode": mode], for: "Clocker Mode")
}
func showUpcomingEventView() {
OperationQueue.main.addOperation {
if let upcomingView = self.upcomingEventContainerView, upcomingView.isHidden {
self.upcomingEventContainerView?.isHidden = false
UserDefaults.standard.set("YES", forKey: UserDefaultKeys.showUpcomingEventView)
Logger.log(object: ["Shown": "YES"], for: "Added Upcoming Event View")
self.themeChanged()
}
}
}
private func fetchCalendarEvents() {
PerfLogger.startMarker("Fetch Calendar Events")
let eventCenter = EventCenter.sharedCenter()
let now = Date()
if let events = eventCenter.eventsForDate[NSCalendar.autoupdatingCurrent.startOfDay(for: now)], events.isEmpty == false {
OperationQueue.main.addOperation {
if self.upcomingEventCollectionView != nil,
let upcomingEvents = eventCenter.upcomingEventsForDay(events)
{
self.upcomingEventsDataSource?.updateEventsDataSource(upcomingEvents)
self.upcomingEventCollectionView.reloadData()
return
}
PerfLogger.endMarker("Fetch Calendar Events")
}
} else {
if upcomingEventCollectionView != nil {
upcomingEventsDataSource?.updateEventsDataSource([])
upcomingEventCollectionView.reloadData()
return
}
PerfLogger.endMarker("Fetch Calendar Events")
}
}
// If the popover is displayed, close it
// Called when preferences are going to be displayed!
func updatePopoverDisplayState() {
if notePopover != nil, let isShown = notePopover?.popover?.isShown, isShown {
notePopover?.popover?.close()
}
additionalOptionsPopover = nil
}
// MARK: Review
@IBAction func actionOnNegativeFeedback(_ sender: NSButton) {
if sender.title == PanelConstants.notReallyButtonTitle {
setAnimated(title: PanelConstants.feedbackString,
field: leftField,
leftTitle: PanelConstants.noThanksTitle,
rightTitle: PanelConstants.yesWithQuestionMark)
} else {
updateReviewView()
ReviewController.prompted()
if let countryCode = Locale.autoupdatingCurrent.regionCode {
Logger.log(object: ["CurrentCountry": countryCode], for: "Remind Later for Feedback")
}
}
}
@IBAction func actionOnPositiveFeedback(_ sender: NSButton) {
if sender.title == PanelConstants.yesWithExclamation {
setAnimated(title: "Would you like to rate us?",
field: leftField,
leftTitle: PanelConstants.noThanksTitle,
rightTitle: "Yes")
} else if sender.title == PanelConstants.yesWithQuestionMark {
ReviewController.prompted()
updateReviewView()
feedbackWindow = AppFeedbackWindowController.shared()
feedbackWindow?.appFeedbackWindowDelegate = self
feedbackWindow?.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
} else {
updateReviewView()
ReviewController.prompt()
}
}
private func updateReviewView() {
reviewView.isHidden = true
showReviewCell = false
leftField.stringValue = NSLocalizedString("Enjoy using Clocker?",
comment: "Title asking users if they like the app")
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let styleAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: NSFont(name: "Avenir-Light", size: 13)!,
]
leftButton.attributedTitle = NSAttributedString(string: "Not Really", attributes: styleAttributes)
rightButton.attributedTitle = NSAttributedString(string: "Yes!", attributes: styleAttributes)
}
private func setAnimated(title: String, field: NSTextField, leftTitle: String, rightTitle: String) {
if field.stringValue == title {
return
}
NSAnimationContext.runAnimationGroup({ context in
context.duration = 1
context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
leftButton.animator().alphaValue = 0.0
rightButton.animator().alphaValue = 0.0
}, completionHandler: {
field.stringValue = title
NSAnimationContext.runAnimationGroup({ context in
context.duration = 1
context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
self.runAnimationCompletionBlock(leftTitle, rightTitle)
}, completionHandler: {})
})
}
private func runAnimationCompletionBlock(_ leftButtonTitle: String, _ rightButtonTitle: String) {
leftButton.animator().alphaValue = 1.0
rightButton.animator().alphaValue = 1.0
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let styleAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: NSFont(name: "Avenir-Light", size: 13)!,
]
if leftButton.attributedTitle.string == "Not Really" {
leftButton.animator().attributedTitle = NSAttributedString(string: PanelConstants.noThanksTitle, attributes: styleAttributes)
}
if rightButton.attributedTitle.string == PanelConstants.yesWithExclamation {
rightButton.animator().attributedTitle = NSAttributedString(string: "Yes, sure", attributes: styleAttributes)
}
leftButton.animator().attributedTitle = NSAttributedString(string: leftButtonTitle, attributes: styleAttributes)
rightButton.animator().attributedTitle = NSAttributedString(string: rightButtonTitle, attributes: styleAttributes)
}
// MARK: Date Picker + Slider
func minutes(from date: Date, other: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: other).minute ?? 0
}
@objc func terminateClocker() {
NSApplication.shared.terminate(nil)
}
@objc func reportIssue() {
feedbackWindow = AppFeedbackWindowController.shared()
feedbackWindow?.appFeedbackWindowDelegate = self
feedbackWindow?.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
window?.orderOut(nil)
if let countryCode = Locale.autoupdatingCurrent.regionCode {
let custom: [String: Any] = ["Country": countryCode]
Logger.log(object: custom, for: "Report Issue Opened")
}
}
@objc func openCrowdin() {
guard let localizationURL = URL(string: AboutUsConstants.CrowdInLocalizationLink),
let languageCode = Locale.preferredLanguages.first else { return }
NSWorkspace.shared.open(localizationURL)
// Log this
let custom: [String: Any] = ["Language": languageCode]
Logger.log(object: custom, for: "Opened Localization Link")
}
@objc func rate() {
guard let sourceURL = URL(string: AboutUsConstants.AppStoreLink) else { return }
NSWorkspace.shared.open(sourceURL)
}
@objc func openFAQs() {
guard let sourceURL = URL(string: AboutUsConstants.FAQsLink) else { return }
NSWorkspace.shared.open(sourceURL)
}
@IBAction func showMoreOptions(_ sender: NSButton) {
let menuItem = NSMenu(title: "More Options")
let terminateOption = NSMenuItem(title: "Quit Clocker",
action: #selector(terminateClocker), keyEquivalent: "")
let rateClocker = NSMenuItem(title: "Support Clocker...",
action: #selector(rate), keyEquivalent: "")
let sendFeedback = NSMenuItem(title: "Send Feedback...",
action: #selector(reportIssue), keyEquivalent: "")
let localizeClocker = NSMenuItem(title: "Localize Clocker...",
action: #selector(openCrowdin), keyEquivalent: "")
let openPreferences = NSMenuItem(title: "Settings",
action: #selector(openPreferencesWindow), keyEquivalent: "")
let appDisplayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") ?? "Clocker"
let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") ?? "N/A"
let longVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") ?? "N/A"
let versionInfo = "\(appDisplayName) \(shortVersion) (\(longVersion))"
let clockerVersionInfo = NSMenuItem(title: versionInfo, action: nil, keyEquivalent: "")
clockerVersionInfo.isEnabled = false
menuItem.addItem(openPreferences)
menuItem.addItem(rateClocker)
menuItem.addItem(withTitle: "FAQs", action: #selector(openFAQs), keyEquivalent: "")
menuItem.addItem(sendFeedback)
menuItem.addItem(localizeClocker)
menuItem.addItem(NSMenuItem.separator())
menuItem.addItem(clockerVersionInfo)
menuItem.addItem(NSMenuItem.separator())
menuItem.addItem(terminateOption)
NSMenu.popUpContextMenu(menuItem,
with: NSApp.currentEvent!,
for: sender)
}
}
extension ParentPanelController: NSPopoverDelegate {
func popoverShouldClose(_: NSPopover) -> Bool {
return false
}
}
extension ParentPanelController: NSSharingServicePickerDelegate {
func sharingServicePicker(_: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? {
Logger.log(object: ["Service Title": sharingService.title],
for: "Sharing Service Executed")
return self as? NSSharingServiceDelegate
}
func sharingServicePicker(_: NSSharingServicePicker, sharingServicesForItems _: [Any], proposedSharingServices proposed: [NSSharingService]) -> [NSSharingService] {
let themer = Themer.shared()
let copySharingService = NSSharingService(title: "Copy All Times",
image:themer.copyImage(),
alternateImage: themer.highlightedCopyImage()) { [weak self] in
guard let strongSelf = self else { return }
let clipboardCopy = strongSelf.retrieveAllTimes()
let pasteboard = NSPasteboard.general
pasteboard.declareTypes([.string], owner: nil)
pasteboard.setString(clipboardCopy, forType: .string)
}
let allowedServices: Set<String> = Set(["Messages", "Notes"])
let filteredServices = proposed.filter { service in
allowedServices.contains(service.title)
}
var newProposedServices: [NSSharingService] = [copySharingService]
newProposedServices.append(contentsOf: filteredServices)
return newProposedServices
}
/// Retrieves all the times from user's added timezones. Times are sorted by date. For eg:
/// Feb 5
/// California - 17:17:01
/// Feb 6
/// London - 01:17:01
private func retrieveAllTimes() -> String {
var clipboardCopy = String()
// Get all timezones
let timezones = DataStore.shared().timezones()
if timezones.isEmpty {
return clipboardCopy
}
// Sort them in ascending order
let sortedByTime = timezones.sorted { obj1, obj2 -> Bool in
let system = NSTimeZone.system
guard let object1 = TimezoneData.customObject(from: obj1),
let object2 = TimezoneData.customObject(from: obj2)
else {
assertionFailure("Data was unexpectedly nil")
return false
}
let timezone1 = NSTimeZone(name: object1.timezone())
let timezone2 = NSTimeZone(name: object2.timezone())
let difference1 = system.secondsFromGMT() - timezone1!.secondsFromGMT
let difference2 = system.secondsFromGMT() - timezone2!.secondsFromGMT
return difference1 > difference2
}
// Grab date in first place and store it as local variable
guard let earliestTimezone = TimezoneData.customObject(from: sortedByTime.first) else {
return clipboardCopy
}
let timezoneOperations = TimezoneDataOperations(with: earliestTimezone, store: DataStore.shared())
let futureSliderValue = datasource?.sliderValue ?? 0
var sectionTitle = timezoneOperations.todaysDate(with: futureSliderValue)
clipboardCopy.append("\(sectionTitle)\n")
stride(from: 0, to: sortedByTime.count, by: 1).forEach {
if $0 < sortedByTime.count,
let dataModel = TimezoneData.customObject(from: sortedByTime[$0])
{
let dataOperations = TimezoneDataOperations(with: dataModel, store: DataStore.shared())
let date = dataOperations.todaysDate(with: futureSliderValue)
let time = dataOperations.time(with: futureSliderValue)
if date != sectionTitle {
sectionTitle = date
clipboardCopy.append("\n\(sectionTitle)\n")
}
clipboardCopy.append("\(dataModel.formattedTimezoneLabel()) - \(time)\n")
}
}
return clipboardCopy
}
}
extension ParentPanelController: AppFeedbackWindowControllerDelegate {
func appFeedbackWindowWillClose() {
feedbackWindow = nil
}
func appFeedbackWindoEntryPoint() -> String {
return "parent_panel_controller"
}
}