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
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" |
|
} |
|
}
|
|
|