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.

1062 lines
40 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!"
}
class ParentPanelController: NSWindowController {
6 years ago
private var futureSliderObserver: NSKeyValueObservation?
private var userFontSizeSelectionObserver: NSKeyValueObservation?
private var futureSliderRangeObserver: NSKeyValueObservation?
6 years ago
private var eventStoreChangedNotification: NSObjectProtocol?
var dateFormatter = DateFormatter()
var futureSliderValue: Int {
return futureSlider.integerValue
}
var parentTimer: Repeater?
var showReviewCell: Bool = false
var previousPopoverRow: Int = -1
var morePopover: NSPopover?
var datasource: TimezoneDataSource?
6 years ago
private lazy var feedbackWindow: AppFeedbackWindowController = AppFeedbackWindowController.shared()
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 futureSlider: NSSlider!
@IBOutlet var scrollViewHeight: NSLayoutConstraint!
@IBOutlet var calendarColorView: NSView!
@IBOutlet var futureSliderView: NSView!
@IBOutlet var upcomingEventView: NSView?
@IBOutlet var reviewView: NSView!
@IBOutlet var leftField: NSTextField!
@IBOutlet var nextEventLabel: NSTextField!
@IBOutlet var whiteRemoveButton: NSButton!
@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 calendarButton: NSButton!
@IBOutlet var sliderDatePicker: NSDatePicker!
6 years ago
@IBOutlet var roundedDateView: NSView!
// Modern Slider
@IBOutlet var modernSlider: NSCollectionView!
@IBOutlet var modernSliderLabel: NSTextField!
@IBOutlet var modernContainerView: NSView!
var defaultPreferences: [Data] {
return DataStore.shared().timezones()
}
deinit {
datasource = nil
6 years ago
if let eventStoreNotif = eventStoreChangedNotification {
NotificationCenter.default.removeObserver(eventStoreNotif)
}
6 years ago
[futureSliderObserver, userFontSizeSelectionObserver, futureSliderRangeObserver].forEach {
$0?.invalidate()
}
}
private func setupObservers() {
6 years ago
futureSliderObserver = UserDefaults.standard.observe(\.displayFutureSlider, options: [.new]) { _, change in
6 years ago
if let changedValue = change.newValue {
if changedValue == 0 {
self.futureSliderView.isHidden = true
self.modernContainerView.isHidden = false
} else if changedValue == 1 {
self.futureSliderView.isHidden = false
self.modernContainerView.isHidden = true
} else {
self.futureSliderView.isHidden = true
self.modernContainerView.isHidden = true
}
6 years ago
}
}
6 years ago
userFontSizeSelectionObserver = UserDefaults.standard.observe(\.userFontSize, options: [.new]) { _, change in
6 years ago
if let newFontSize = change.newValue {
Logger.log(object: ["FontSize": newFontSize], for: "User Font Size Preference")
self.mainTableView.reloadData()
self.setScrollViewConstraint()
}
}
6 years ago
futureSliderRangeObserver = UserDefaults.standard.observe(\.sliderDayRange, options: [.new]) { _, change in
6 years ago
if change.newValue != nil {
self.adjustFutureSliderBasedOnPreferences()
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
mainTableView.backgroundColor = NSColor.clear
mainTableView.selectionHighlightStyle = .none
mainTableView.enclosingScrollView?.hasVerticalScroller = false
let sharedThemer = Themer.shared()
shutdownButton.image = sharedThemer.shutdownImage()
preferencesButton.image = sharedThemer.preferenceImage()
pinButton.image = sharedThemer.pinImage()
sharingButton.image = sharedThemer.sharingImage()
if let upcomingView = upcomingEventView {
upcomingView.setAccessibility("UpcomingEventView")
}
6 years ago
setupObservers()
updateReviewViewFontColor()
futureSliderView.wantsLayer = true
reviewView.wantsLayer = true
NotificationCenter.default.addObserver(self,
selector: #selector(themeChanged),
name: Notification.Name.themeDidChange,
object: nil)
determineUpcomingViewVisibility()
themeChanged()
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 {
if value.intValue == 1 {
futureSliderView.isHidden = false
modernContainerView.isHidden = true
} else if value.intValue == 0 {
futureSliderView.isHidden = true
modernContainerView.isHidden = false
}
}
sharingButton.sendAction(on: .leftMouseDown)
adjustFutureSliderBasedOnPreferences()
NotificationCenter.default.addObserver(self,
selector: #selector(timezoneGonnaChange),
name: NSNotification.Name.NSSystemTimeZoneDidChange,
object: nil)
6 years ago
if #available(OSX 11.0, *) {
mainTableView.style = .fullWidth
}
4 years ago
if modernSlider != nil {
modernSlider.enclosingScrollView?.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 100, right: 0)
modernSlider.delegate = self
modernSlider.postsBoundsChangedNotifications = true
NotificationCenter.default.addObserver(self,
selector: #selector(collectionViewDidScroll(_:)),
name: NSView.boundsDidChangeNotification,
object: modernSlider.superview)
setModernSliderLabel(0)
4 years ago
}
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
}
6 years ago
@objc func timezoneGonnaChange() {
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)
}
}
6 years ago
for timezoneObject in timezoneObjects where timezoneObject.isSystemTimezone == true {
timezoneObject.setLabel(customLabel)
timezoneObject.formattedAddress = customLabel
6 years ago
if let latlong = coordinates {
timezoneObject.longitude = latlong.longitude
timezoneObject.latitude = latlong.latitude
}
}
var datas: [Data] = []
for updatedObject in timezoneObjects {
let dataObject = NSKeyedArchiver.archivedData(withRootObject: updatedObject)
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 {
upcomingEventView?.isHidden = true
} else {
upcomingEventView?.isHidden = false
setupUpcomingEventView()
6 years ago
eventStoreChangedNotification = NotificationCenter.default.addObserver(forName: NSNotification.Name.EKEventStoreChanged, object: self, queue: OperationQueue.main) { _ in
self.fetchCalendarEvents()
}
}
}
private func adjustFutureSliderBasedOnPreferences() {
// Setting up Slider's Date Picker
sliderDatePicker.minDate = Date()
guard let sliderRange = DataStore.shared().retrieve(key: CLFutureSliderRange) as? NSNumber else {
sliderDatePicker.maxDate = Date(timeInterval: 1 * 24 * 60 * 60, since: Date())
return
}
sliderDatePicker.maxDate = Date(timeInterval: (sliderRange.doubleValue + 1) * 24 * 60 * 60, since: Date())
futureSlider.maxValue = (sliderRange.doubleValue + 1) * 24 * 60
futureSlider.integerValue = 0
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() {
nextEventLabel.stringValue = NSLocalizedString("See your next Calendar event here.",
comment: "Next Event Label for no Calendar access")
setCalendarButtonTitle(buttonTitle: NSLocalizedString("Click here to start.",
comment: "Button Title for no Calendar access"))
calendarColorView.layer?.backgroundColor = NSColor(red: 97 / 255.0, green: 194 / 255.0, blue: 80 / 255.0, alpha: 1.0).cgColor
} 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,
6 years ago
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
futureSliderView.layer?.backgroundColor = NSColor.clear.cgColor
}
@objc func themeChanged() {
let sharedThemer = Themer.shared()
if upcomingEventView?.isHidden == false {
upcomingEventView?.layer?.backgroundColor = NSColor.clear.cgColor
nextEventLabel.textColor = sharedThemer.mainTextColor()
whiteRemoveButton.image = sharedThemer.removeImage()
setCalendarButtonTitle(buttonTitle: calendarButton.title)
}
shutdownButton.image = sharedThemer.shutdownImage()
preferencesButton.image = sharedThemer.preferenceImage()
pinButton.image = sharedThemer.pinImage()
sharingButton.image = sharedThemer.sharingImage()
sliderDatePicker.textColor = sharedThemer.mainTextColor()
4 years ago
if roundedDateView != nil {
roundedDateView.layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor
}
6 years ago
updateReviewViewFontColor()
}
override func windowDidLoad() {
super.windowDidLoad()
morePopover = NSPopover()
}
private func setCalendarButtonTitle(buttonTitle: String) {
let style = NSMutableParagraphStyle()
style.alignment = .left
style.lineBreakMode = .byTruncatingTail
if let boldFont = NSFont(name: "Avenir", size: 12) {
let attributes = [NSAttributedString.Key.foregroundColor: NSColor.lightGray, NSAttributedString.Key.paragraphStyle: style, NSAttributedString.Key.font: boldFont]
let attributedString = NSAttributedString(string: buttonTitle, attributes: attributes)
calendarButton.attributedTitle = attributedString
calendarButton.toolTip = attributedString.string
}
}
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
}
6 years ago
private func getAdjustedRowHeight(for object: TimezoneData?, _ currentHeight: CGFloat) -> CGFloat {
let userFontSize: NSNumber = DataStore.shared().retrieve(key: CLUserFontSizePreference) as? NSNumber ?? 4
let shouldShowSunrise = DataStore.shared().shouldDisplay(.sunrise)
6 years ago
var newHeight = currentHeight
6 years ago
if newHeight <= 68.0 {
newHeight = 60.0
6 years ago
}
6 years ago
if newHeight >= 68.0 {
newHeight = userFontSize == 4 ? 68.0 : 68.0
6 years ago
if let note = object?.note, note.isEmpty == false {
newHeight += 20
} else if DataStore.shared().shouldDisplay(.dstTransitionInfo),
4 years ago
let obj = object,
TimezoneDataOperations(with: obj).nextDaylightSavingsTransitionIfAvailable(with: futureSliderValue) != nil {
newHeight += 20
}
6 years ago
}
6 years ago
if newHeight >= 88.0 {
// Set it to 90 expicity in case the row height is calculated be higher.
newHeight = 88.0
6 years ago
if let note = object?.note, note.isEmpty, DataStore.shared().shouldDisplay(.dstTransitionInfo) == false, let obj = object, TimezoneDataOperations(with: obj).nextDaylightSavingsTransitionIfAvailable(with: futureSliderValue) == nil {
newHeight -= 20.0
}
6 years ago
}
if shouldShowSunrise, object?.selectionType == .city {
newHeight += 8.0
}
if object?.isSystemTimezone == true {
newHeight += 5
}
6 years ago
newHeight += mainTableView.intercellSpacing.height
6 years ago
return newHeight
}
6 years ago
func setScrollViewConstraint() {
var totalHeight: CGFloat = 0.0
let preferences = defaultPreferences
6 years ago
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
6 years ago
if preferences.isEmpty {
scrollViewHeight.constant = 100.0
return
}
if let userFontSize = DataStore.shared().retrieve(key: CLUserFontSizePreference) 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)
}
}
}
func updateDefaultPreferences() {
if #available(OSX 10.14, *) {
PerfLogger.startMarker("Update Default Preferences")
}
updatePanelColor()
let defaults = DataStore.shared().timezones()
let convertedTimezones = defaults.map { (data) -> TimezoneData in
TimezoneData.customObject(from: data)!
}
datasource = TimezoneDataSource(items: convertedTimezones)
mainTableView.dataSource = datasource
mainTableView.delegate = datasource
mainTableView.panelDelegate = datasource
updateDatasource(with: convertedTimezones)
if #available(OSX 10.14, *) {
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: CLUserFontSizePreference) as? NSNumber {
scrollViewHeight.constant = CGFloat(timezones.count) * (mainTableView.rowHeight + CGFloat(userFontSize.floatValue * 1.5))
setScrollViewConstraint()
mainTableView.reloadData()
}
}
func updatePanelColor() {
window?.alphaValue = 1.0
}
@IBAction func sliderMoved(_: Any) {
let currentCalendar = Calendar(identifier: .gregorian)
guard let newDate = currentCalendar.date(byAdding: .minute,
value: Int(futureSlider.doubleValue),
to: Date()) else {
assertionFailure("Data was unexpectedly nil")
return
}
setTimezoneDatasourceSlider(sliderValue: futureSliderValue)
sliderDatePicker.dateValue = newDate
mainTableView.reloadData()
}
func setTimezoneDatasourceSlider(sliderValue: Int) {
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
openPreferences()
}
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 = MenubarHandler()
@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.title = menubarTitleHandler.titleForMenubar()
}
}
}
let preferences = store.timezones()
stride(from: 0, to: preferences.count, by: 1).forEach {
let current = preferences[$0]
6 years ago
if $0 < mainTableView.numberOfRows,
let cellView = mainTableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? TimezoneCellView,
let model = TimezoneData.customObject(from: current) {
if let futureSliderCell = futureSlider.cell as? CustomSliderCell, futureSliderCell.tracking == true {
return
}
let dataOperation = TimezoneDataOperations(with: model)
cellView.time.stringValue = dataOperation.time(with: futureSliderValue)
cellView.sunriseSetTime.stringValue = dataOperation.formattedSunriseTime(with: futureSliderValue)
cellView.sunriseSetTime.lineBreakMode = .byClipping
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()
if let note = model.note, !note.isEmpty {
cellView.noteLabel.stringValue = note
} else if DataStore.shared().shouldDisplay(.dstTransitionInfo),
4 years ago
let value = TimezoneDataOperations(with: model).nextDaylightSavingsTransitionIfAvailable(with: futureSliderValue) {
cellView.noteLabel.stringValue = value
} else {
cellView.noteLabel.stringValue = CLEmptyString
}
cellView.layout(with: model)
updateDatePicker()
}
}
}
private func updateDatePicker() {
sliderDatePicker.minDate = Date()
guard let sliderRange = DataStore.shared().retrieve(key: CLFutureSliderRange) as? NSNumber else {
sliderDatePicker.maxDate = Date(timeInterval: 1 * 24 * 60 * 60, since: Date())
return
}
sliderDatePicker.maxDate = Date(timeInterval: (sliderRange.doubleValue + 1) * 24 * 60 * 60, since: Date())
}
@discardableResult
func showNotesPopover(forRow row: Int, relativeTo _: NSRect, andButton target: NSButton!) -> Bool {
let defaults = DataStore.shared().timezones()
guard let popover = morePopover else {
assertionFailure("Data was unexpectedly nil")
return false
}
var correctRow = row
target.image = Themer.shared().extraOptionsHighlightedImage()
popover.animates = true
6 years ago
if notePopover == nil {
notePopover = NotesPopover(nibName: NSNib.Name.notesPopover, bundle: nil)
popover.behavior = .applicationDefined
popover.delegate = self
}
6 years ago
// 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()
}
private func openPreferences() {
oneWindow?.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func dismissNextEventLabel(_: NSButton) {
removeUpcomingEventView()
}
func removeUpcomingEventView() {
OperationQueue.main.addOperation {
if self.stackView.arrangedSubviews.contains(self.upcomingEventView!), self.upcomingEventView?.isHidden == false {
self.upcomingEventView?.isHidden = true
UserDefaults.standard.set("NO", forKey: CLShowUpcomingEventView)
Logger.log(object: ["Removed": "YES"], for: "Removed Upcoming Event View")
}
}
}
@IBAction func calendarButtonAction(_: NSButton) {
if calendarButton.title == NSLocalizedString("Click here to start.",
comment: "Button Title for no Calendar access") {
showPermissionsWindow()
} else {
retrieveCalendarEvents()
}
}
private func showPermissionsWindow() {
oneWindow?.openPermissions()
NSApp.activate(ignoringOtherApps: true)
}
func retrieveCalendarEvents() {
if #available(OSX 10.14, *) {
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()
}
if #available(OSX 10.14, *) {
PerfLogger.endMarker("Retrieve Calendar Events")
}
}
@IBAction func shareAction(_ sender: NSButton) {
let promotionText = "Keep track of your friends and colleagues in different timezones using Clocker: appstore.com/mac/clockermenubarworldclock"
guard let url = URL(string: "https://goo.gl/xyLA4j") else {
assertionFailure("Data was unexpectedly nil")
return
}
let servicePicker = NSSharingServicePicker(items: [promotionText, url])
servicePicker.delegate = self
servicePicker.show(relativeTo: sender.bounds,
of: sender,
preferredEdge: .minY)
}
@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: CLShowAppInForeground)
close()
if inverseSelection.isEqual(to: NSNumber(value: 1)) {
sharedDelegate.setupFloatingWindow()
} else {
sharedDelegate.closeFloatingWindow()
sharedDelegate.setPanelDefaults()
}
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.upcomingEventView, upcomingView.isHidden {
self.upcomingEventView?.isHidden = false
UserDefaults.standard.set("YES", forKey: CLShowUpcomingEventView)
Logger.log(object: ["Shown": "YES"], for: "Added Upcoming Event View")
self.themeChanged()
}
}
}
private func fetchCalendarEvents() {
if #available(OSX 10.14, *) {
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 {
guard let upcomingEvent = eventCenter.nextOccuring(events) else {
self.setPlaceholdersForUpcomingCalendarView()
if #available(OSX 10.14, *) {
PerfLogger.endMarker("Fetch Calendar Events")
}
return
}
self.calendarColorView.layer?.backgroundColor = upcomingEvent.calendar.color.cgColor
self.nextEventLabel.stringValue = upcomingEvent.title
self.nextEventLabel.toolTip = upcomingEvent.title
if upcomingEvent.isAllDay == true {
let title = events.count == 1 ? "All-Day" : "All Day - Total \(events.count) events today"
self.setCalendarButtonTitle(buttonTitle: title)
if #available(OSX 10.14, *) {
PerfLogger.endMarker("Fetch Calendar Events")
}
return
}
let timeSince = Date().timeAgo(since: upcomingEvent.startDate)
let withoutAn = timeSince.replacingOccurrences(of: "an", with: CLEmptyString)
let withoutAgo = withoutAn.replacingOccurrences(of: "ago", with: CLEmptyString)
self.setCalendarButtonTitle(buttonTitle: "in \(withoutAgo.lowercased())")
if #available(OSX 10.14, *) {
PerfLogger.endMarker("Fetch Calendar Events")
}
}
} else {
setPlaceholdersForUpcomingCalendarView()
if #available(OSX 10.14, *) {
PerfLogger.endMarker("Fetch Calendar Events")
}
}
}
private func setPlaceholdersForUpcomingCalendarView() {
let eventCenter = EventCenter.sharedCenter()
var tomorrow = DateComponents()
tomorrow.day = 1
guard let tomorrowDate = Calendar.autoupdatingCurrent.date(byAdding: tomorrow, to: Date()) else {
setCalendarButtonTitle(buttonTitle: "You have no events scheduled for tomorrow.")
return
}
5 years ago
nextEventLabel.stringValue = NSLocalizedString("No upcoming event.",
comment: "Title when there's no upcoming event")
calendarColorView.layer?.backgroundColor = NSColor(red: 97 / 255.0, green: 194 / 255.0, blue: 80 / 255.0, alpha: 1.0).cgColor
let events = eventCenter.filteredEvents[NSCalendar.autoupdatingCurrent.startOfDay(for: tomorrowDate)]
if let count = events?.count, count > 1 {
let suffix = "events coming up tomorrow."
setCalendarButtonTitle(buttonTitle: "\(count) \(suffix)")
} else if let first = events?.first?.event.title {
setCalendarButtonTitle(buttonTitle: "\(first) coming up.")
} else {
5 years ago
setCalendarButtonTitle(buttonTitle: NSLocalizedString("You have no events scheduled for tomorrow.",
comment: "Title when there's no event scheduled for tomorrow"))
}
}
// 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()
}
morePopover = 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.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
} else {
updateReviewView()
ReviewController.prompt()
if let countryCode = Locale.autoupdatingCurrent.regionCode {
Logger.log(object: ["CurrentCountry": countryCode], for: "Remind Later for Feedback")
}
}
}
private func updateReviewView() {
reviewView.isHidden = true
showReviewCell = false
leftField.stringValue = NSLocalizedString("Enjoy using Clocker?",
5 years ago
comment: "Title asking users if they like the app")
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let styleAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
6 years ago
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
}
6 years ago
NSAnimationContext.runAnimationGroup({ context in
context.duration = 1
context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
leftButton.animator().alphaValue = 0.0
rightButton.animator().alphaValue = 0.0
6 years ago
}, completionHandler: {
field.stringValue = title
6 years ago
NSAnimationContext.runAnimationGroup({ context in
context.duration = 1
context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
6 years ago
self.runAnimationCompletionBlock(leftTitle, rightTitle)
}, completionHandler: {})
})
}
6 years ago
private func runAnimationCompletionBlock(_ leftButtonTitle: String, _ rightButtonTitle: String) {
6 years ago
leftButton.animator().alphaValue = 1.0
rightButton.animator().alphaValue = 1.0
6 years ago
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
6 years ago
let styleAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
6 years ago
NSAttributedString.Key.font: NSFont(name: "Avenir-Light", size: 13)!,
6 years ago
]
6 years ago
if leftButton.attributedTitle.string == "Not Really" {
leftButton.animator().attributedTitle = NSAttributedString(string: PanelConstants.noThanksTitle, attributes: styleAttributes)
6 years ago
}
6 years ago
if rightButton.attributedTitle.string == PanelConstants.yesWithExclamation {
rightButton.animator().attributedTitle = NSAttributedString(string: "Yes, sure", attributes: styleAttributes)
}
6 years ago
6 years ago
leftButton.animator().attributedTitle = NSAttributedString(string: leftButtonTitle, attributes: styleAttributes)
rightButton.animator().attributedTitle = NSAttributedString(string: rightButtonTitle, attributes: styleAttributes)
}
// MARK: Date Picker + Slider
@IBAction func sliderPickerChanged(_: Any) {
let minutesDifference = minutes(from: Date(), other: sliderDatePicker.dateValue)
futureSlider.integerValue = minutesDifference
setTimezoneDatasourceSlider(sliderValue: minutesDifference)
updateTableContent()
}
func minutes(from date: Date, other: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: other).minute ?? 0
}
@IBAction func resetDatePicker(_: Any) {
futureSlider.integerValue = 0
sliderDatePicker.dateValue = Date()
setTimezoneDatasourceSlider(sliderValue: 0)
}
4 years ago
@objc func terminateClocker() {
NSApplication.shared.terminate(nil)
}
@objc func reportIssue() {
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")
}
4 years ago
@objc func rate() {
guard let sourceURL = URL(string: AboutUsConstants.AppStoreLink) else { return }
NSWorkspace.shared.open(sourceURL)
}
4 years ago
@IBAction func showMoreOptions(_ sender: NSButton) {
let menuItem = NSMenu(title: "More Options")
let terminateOption = NSMenuItem(title: "Quit Clocker",
action: #selector(terminateClocker), keyEquivalent: "")
4 years ago
let rateClocker = NSMenuItem(title: "Support Clocker...",
action: #selector(rate), keyEquivalent: "")
let sendFeedback = NSMenuItem(title: "Send Feedback...",
4 years ago
action: #selector(reportIssue), keyEquivalent: "")
4 years ago
let localizeClocker = NSMenuItem(title: "Localize Clocker...",
4 years ago
action: #selector(openCrowdin), keyEquivalent: "")
4 years ago
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
4 years ago
menuItem.addItem(rateClocker)
menuItem.addItem(sendFeedback)
menuItem.addItem(localizeClocker)
4 years ago
menuItem.addItem(NSMenuItem.separator())
menuItem.addItem(clockerVersionInfo)
menuItem.addItem(NSMenuItem.separator())
menuItem.addItem(terminateOption)
4 years ago
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
}
}