|
|
|
// Copyright © 2015 Abhishek Banthia
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
|
|
|
|
class NotesPopover: NSViewController {
|
|
|
|
private enum OverrideType {
|
|
|
|
case timezoneFormat
|
|
|
|
case seconds
|
|
|
|
}
|
|
|
|
|
|
|
|
var dataObject: TimezoneData?
|
|
|
|
|
|
|
|
var timezoneObjects: [Data]?
|
|
|
|
|
|
|
|
var currentRow: Int = -1
|
|
|
|
|
|
|
|
weak var popover: NSPopover?
|
|
|
|
|
|
|
|
@IBOutlet var customLabel: NSTextField!
|
|
|
|
|
|
|
|
@IBOutlet var reminderPicker: NSDatePicker!
|
|
|
|
|
|
|
|
@IBOutlet var reminderView: NSView!
|
|
|
|
|
|
|
|
@IBOutlet var timeFormatTweakingView: NSView!
|
|
|
|
|
|
|
|
@IBOutlet var alertPopupButton: NSPopUpButton!
|
|
|
|
|
|
|
|
@IBOutlet var scriptExecutionIndicator: NSProgressIndicator!
|
|
|
|
|
|
|
|
@IBOutlet var saveButton: NSButton!
|
|
|
|
|
|
|
|
@IBOutlet var setReminderCheckbox: NSButton!
|
|
|
|
|
|
|
|
@IBOutlet var remindersButton: NSButton!
|
|
|
|
|
|
|
|
@IBOutlet var timeFormatControl: NSSegmentedControl!
|
|
|
|
|
|
|
|
@IBOutlet var secondsFormatControl: NSSegmentedControl!
|
|
|
|
|
|
|
|
@IBOutlet var notesTextView: TextViewWithPlaceholder!
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
|
|
setupAlarmTextField()
|
|
|
|
setupUI()
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
|
selector: #selector(themeChanged),
|
|
|
|
name: NSNotification.Name.themeDidChange,
|
|
|
|
object: nil)
|
|
|
|
|
|
|
|
let titles = [
|
|
|
|
"None",
|
|
|
|
"At time of the event",
|
|
|
|
"5 minutes before",
|
|
|
|
"10 minutes before",
|
|
|
|
"15 minutes before",
|
|
|
|
"30 minutes before",
|
|
|
|
"1 hour before",
|
|
|
|
"2 hour before",
|
|
|
|
"1 day before",
|
|
|
|
"2 days before",
|
|
|
|
]
|
|
|
|
|
|
|
|
alertPopupButton.removeAllItems()
|
|
|
|
alertPopupButton.addItems(withTitles: titles)
|
|
|
|
alertPopupButton.selectItem(at: 1)
|
|
|
|
|
|
|
|
// Set Accessibility Identifiers for UI tests
|
|
|
|
customLabel.setAccessibilityIdentifier("CustomLabel")
|
|
|
|
saveButton.setAccessibilityIdentifier("SaveButton")
|
|
|
|
notesTextView.setAccessibilityIdentifier("NotesTextView")
|
|
|
|
setReminderCheckbox.setAccessibilityIdentifier("ReminderCheckbox")
|
|
|
|
alertPopupButton.setAccessibilityIdentifier("RemindersAlertPopup")
|
|
|
|
reminderView.setAccessibilityIdentifier("RemindersView")
|
|
|
|
|
|
|
|
if #available(OSX 11.0, *) {
|
|
|
|
alertPopupButton.controlSize = .large
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override func viewWillAppear() {
|
|
|
|
super.viewWillAppear()
|
|
|
|
scriptExecutionIndicator.stopAnimation(nil)
|
|
|
|
updateContent()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupUI() {
|
|
|
|
if let saveCell = saveButton.cell as? NSButtonCell {
|
|
|
|
setCellState(buttonCell: saveCell)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let remindersCell = remindersButton.cell as? NSButtonCell {
|
|
|
|
setCellState(buttonCell: remindersCell)
|
|
|
|
}
|
|
|
|
|
|
|
|
notesTextView.font = NSFont(name: "Avenir", size: 14)
|
|
|
|
notesTextView.enclosingScrollView?.hasVerticalScroller = false
|
|
|
|
|
|
|
|
themeChanged()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setCellState(buttonCell: NSButtonCell) {
|
|
|
|
buttonCell.highlightsBy = .contentsCellMask
|
|
|
|
buttonCell.showsStateBy = .pushInCellMask
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupAlarmTextField() {
|
|
|
|
reminderPicker.datePickerStyle = .textField
|
|
|
|
reminderPicker.isBezeled = false
|
|
|
|
reminderPicker.isBordered = false
|
|
|
|
reminderPicker.drawsBackground = false
|
|
|
|
reminderPicker.datePickerElements = [.yearMonthDay, .hourMinute]
|
|
|
|
reminderPicker.target = self
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setInitialReminderTime() {
|
|
|
|
// If self.calSelectedDate is today, the initialStart is set to
|
|
|
|
// the next whole hour. Otherwise, 8am of self.calselectedDate.
|
|
|
|
|
|
|
|
getCurrentTimezoneDate { finalDate in
|
|
|
|
let currentDate = finalDate ?? Date()
|
|
|
|
self.continueProcess(with: currentDate)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func continueProcess(with currentDate: Date) {
|
|
|
|
let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)
|
|
|
|
var hour = 0
|
|
|
|
calendar?.getHour(&hour,
|
|
|
|
minute: nil,
|
|
|
|
second: nil,
|
|
|
|
nanosecond: nil,
|
|
|
|
from: currentDate)
|
|
|
|
|
|
|
|
hour = (hour == 23) ? 0 : hour + 1
|
|
|
|
|
|
|
|
guard let initialStart = calendar?.nextDate(after: currentDate,
|
|
|
|
matching: NSCalendar.Unit.hour,
|
|
|
|
value: hour,
|
|
|
|
options: NSCalendar.Options.matchPreviousTimePreservingSmallerUnits) else {
|
|
|
|
assertionFailure("Initial Date object was unexepectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
reminderPicker.minDate = currentDate
|
|
|
|
reminderPicker.dateValue = initialStart
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getCurrentTimezoneDate(completionHandler: @escaping (_ response: Date?) -> Void) {
|
|
|
|
guard let timezoneID = dataObject?.timezone() else {
|
|
|
|
assertionFailure("Unable to retrieve timezoneID from the model")
|
|
|
|
completionHandler(nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let currentCalendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)
|
|
|
|
|
|
|
|
guard let newDate = currentCalendar?.date(byAdding: NSCalendar.Unit.minute,
|
|
|
|
value: 0,
|
|
|
|
to: Date(),
|
|
|
|
options: NSCalendar.Options.matchLast) else {
|
|
|
|
assertionFailure("Initial Date object was unexepectedly nil")
|
|
|
|
completionHandler(nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
formatter.dateStyle = .medium
|
|
|
|
formatter.timeStyle = .short
|
|
|
|
formatter.locale = Locale(identifier: "en_US")
|
|
|
|
formatter.timeZone = TimeZone(identifier: timezoneID)
|
|
|
|
|
|
|
|
let dateStyle = formatter.string(from: newDate)
|
|
|
|
let type: NSTextCheckingResult.CheckingType = .date
|
|
|
|
|
|
|
|
do {
|
|
|
|
let detector = try NSDataDetector(types: type.rawValue)
|
|
|
|
detector.enumerateMatches(in: dateStyle,
|
|
|
|
options: NSRegularExpression.MatchingOptions.reportCompletion,
|
|
|
|
range: NSRange(location: 0, length: dateStyle.count),
|
|
|
|
using: { result, _, _ in
|
|
|
|
guard let completedDate = result?.date else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
completionHandler(completedDate)
|
|
|
|
|
|
|
|
})
|
|
|
|
} catch {
|
|
|
|
assertionFailure("Failed to successfully initialize DataDetector")
|
|
|
|
completionHandler(nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setAttributedTitle(title: String, for button: NSButton) {
|
|
|
|
let style = NSMutableParagraphStyle()
|
|
|
|
style.alignment = .center
|
|
|
|
|
|
|
|
guard let font = NSFont(name: "Avenir-Book", size: 13) else { return }
|
|
|
|
|
|
|
|
let attributesDictionary = [
|
|
|
|
NSAttributedString.Key.font: font,
|
|
|
|
NSAttributedString.Key.foregroundColor: Themer.shared().mainTextColor(),
|
|
|
|
NSAttributedString.Key.paragraphStyle: style,
|
|
|
|
]
|
|
|
|
|
|
|
|
button.attributedTitle = NSAttributedString(string: title,
|
|
|
|
attributes: attributesDictionary)
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func saveAction(_: Any) {
|
|
|
|
updateLabel()
|
|
|
|
|
|
|
|
dataObject?.note = notesTextView.string
|
|
|
|
dataObject?.setShouldOverrideGlobalTimeFormat(timeFormatControl.selectedSegment)
|
|
|
|
dataObject?.setShouldOverrideSecondsFormat(secondsFormatControl.selectedSegment)
|
|
|
|
insertTimezoneInDefaultPreferences()
|
|
|
|
|
|
|
|
if setReminderCheckbox.state == .on {
|
|
|
|
setReminderAlarm()
|
|
|
|
Logger.log(object: nil, for: "Reminder Set")
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshMainTableView()
|
|
|
|
|
|
|
|
NotificationCenter.default.post(name: NSNotification.Name.customLabelChanged, object: nil)
|
|
|
|
|
|
|
|
popover?.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func seeReminders(_: Any) {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.scriptExecutionIndicator.startAnimation(nil)
|
|
|
|
|
|
|
|
let source = """
|
|
|
|
tell application \"Reminders\"
|
|
|
|
tell default account
|
|
|
|
show (first list where name is \"Clocker Reminders\")
|
|
|
|
activate application
|
|
|
|
end tell
|
|
|
|
end tell
|
|
|
|
"""
|
|
|
|
|
|
|
|
var scriptExecutionErrors: NSDictionary? = .none
|
|
|
|
let remindersScript = NSAppleScript(source: source)
|
|
|
|
let eventDescriptor = remindersScript?.executeAndReturnError(&scriptExecutionErrors)
|
|
|
|
|
|
|
|
if let errors = scriptExecutionErrors, errors.allKeys.isEmpty == false {
|
|
|
|
if let convertedType = errors as? [String: Any] {
|
|
|
|
Logger.log(object: convertedType, for: "Script Execution Errors")
|
|
|
|
}
|
|
|
|
NSWorkspace.shared.launchApplication("Reminders")
|
|
|
|
} else if eventDescriptor == nil {
|
|
|
|
Logger.log(object: nil, for: "Event Description is unexpectedly nil")
|
|
|
|
NSWorkspace.shared.launchApplication("Reminders")
|
|
|
|
} else {
|
|
|
|
Logger.log(object: ["Successfully Executed Apple Script": "YES"], for: "Successfully Executed Apple Script")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.scriptExecutionIndicator.stopAnimation(nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func checkboxAction(_: Any) {
|
|
|
|
enableReminderView(!alertPopupButton.isEnabled)
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func customizeTimeFormat(_ sender: NSSegmentedControl) {
|
|
|
|
updateTimezoneInDefaultPreferences(with: sender.selectedSegment, .timezoneFormat)
|
|
|
|
refreshMainTableView()
|
|
|
|
|
|
|
|
// Update the display if the chosen menubar mode is compact!
|
|
|
|
if let delegate = NSApplication.shared.delegate as? AppDelegate {
|
|
|
|
let handler = delegate.statusItemForPanel()
|
|
|
|
handler.setupStatusItem()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func insertTimezoneInDefaultPreferences() {
|
|
|
|
guard let model = dataObject, var timezones = timezoneObjects else { return }
|
|
|
|
let encodedObject = NSKeyedArchiver.archivedData(withRootObject: model)
|
|
|
|
timezones[currentRow] = encodedObject
|
|
|
|
DataStore.shared().setTimezones(timezones)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateTimezoneInDefaultPreferences(with override: Int, _ overrideType: OverrideType) {
|
|
|
|
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.isEqual(dataObject) {
|
|
|
|
overrideType == .timezoneFormat ?
|
|
|
|
timezoneObject.setShouldOverrideGlobalTimeFormat(override) :
|
|
|
|
timezoneObject.setShouldOverrideSecondsFormat(override)
|
|
|
|
}
|
|
|
|
|
|
|
|
var datas: [Data] = []
|
|
|
|
|
|
|
|
for updatedObject in timezoneObjects {
|
|
|
|
let dataObject = NSKeyedArchiver.archivedData(withRootObject: updatedObject)
|
|
|
|
datas.append(dataObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
DataStore.shared().setTimezones(datas)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setReminderAlarm() {
|
|
|
|
let eventCenter = EventCenter.sharedCenter()
|
|
|
|
|
|
|
|
if eventCenter.reminderAccessNotDetermined() {
|
|
|
|
eventCenter.requestAccess(to: .reminder, completionHandler: { granted in
|
|
|
|
if granted {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.createReminder()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Logger.log(object: ["Reminder Access Not Granted": "YES"], for: "Reminder Access Not Granted")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
} else if eventCenter.reminderAccessGranted() {
|
|
|
|
createReminder()
|
|
|
|
} else {
|
|
|
|
showAlertForPermissionNotGiven()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func refreshMainTableView() {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
if DataStore.shared().shouldDisplay(ViewType.showAppInForeground) {
|
|
|
|
let currentInstance = FloatingWindowController.shared()
|
|
|
|
currentInstance.updateDefaultPreferences()
|
|
|
|
} else {
|
|
|
|
guard let panelController = PanelController.panel() else { return }
|
|
|
|
panelController.updateDefaultPreferences()
|
|
|
|
panelController.updateTableContent()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func createReminder() {
|
|
|
|
guard let model = dataObject else { return }
|
|
|
|
if setReminderCheckbox.state == .on {
|
|
|
|
let eventCenter = EventCenter.sharedCenter()
|
|
|
|
let alertIndex = alertPopupButton.indexOfSelectedItem
|
|
|
|
|
|
|
|
if eventCenter.createReminder(with: model.customLabel!,
|
|
|
|
timezone: model.timezone(),
|
|
|
|
alertIndex: alertIndex,
|
|
|
|
reminderDate: reminderPicker.dateValue,
|
|
|
|
additionalNotes: model.note) {
|
|
|
|
showSuccessMessage()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func showAlertForPermissionNotGiven() {
|
|
|
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
|
|
|
|
|
|
|
let alert = NSAlert()
|
|
|
|
alert.messageText = "Clocker needs access to Reminders 😅"
|
|
|
|
alert.informativeText = "Please go to System Preferences -> Security & Privacy -> Privacy -> Reminders to allow Clocker to set reminders."
|
|
|
|
alert.addButton(withTitle: "Okay")
|
|
|
|
|
|
|
|
let alertResponse = alert.runModal()
|
|
|
|
|
|
|
|
if alertResponse == .stop {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.popover?.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func showSuccessMessage() {
|
|
|
|
let reminderNotification = NSUserNotification()
|
|
|
|
reminderNotification.title = "Reminder Set".localized()
|
|
|
|
reminderNotification.subtitle = "Successfully set.".localized()
|
|
|
|
|
|
|
|
NSUserNotificationCenter.default.scheduleNotification(reminderNotification)
|
|
|
|
}
|
|
|
|
|
|
|
|
func setDataSource(data: TimezoneData) {
|
|
|
|
dataObject = data
|
|
|
|
|
|
|
|
if isViewLoaded {
|
|
|
|
updateContent()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func customizeSecondsFormat(_ sender: NSSegmentedControl) {
|
|
|
|
updateTimezoneInDefaultPreferences(with: sender.selectedSegment, .seconds)
|
|
|
|
refreshMainTableView()
|
|
|
|
|
|
|
|
// Update the display if the chosen menubar mode is compact!
|
|
|
|
if let delegate = NSApplication.shared.delegate as? AppDelegate {
|
|
|
|
let handler = delegate.statusItemForPanel()
|
|
|
|
handler.setupStatusItem()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension NotesPopover {
|
|
|
|
func setRow(row: Int) {
|
|
|
|
currentRow = row
|
|
|
|
}
|
|
|
|
|
|
|
|
func set(timezones: [Data]) {
|
|
|
|
timezoneObjects = timezones
|
|
|
|
}
|
|
|
|
|
|
|
|
func set(with popover: NSPopover) {
|
|
|
|
self.popover = popover
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func themeChanged() {
|
|
|
|
notesTextView.textColor = Themer.shared().mainTextColor()
|
|
|
|
customLabel.textColor = Themer.shared().mainTextColor()
|
|
|
|
reminderPicker.textColor = Themer.shared().mainTextColor()
|
|
|
|
popover?.appearance = Themer.shared().popoverAppearance()
|
|
|
|
setAttributedTitle(title: saveButton.title, for: saveButton)
|
|
|
|
setAttributedTitle(title: remindersButton.title, for: remindersButton)
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateContent() {
|
|
|
|
guard let model = dataObject else {
|
|
|
|
assertionFailure("Model object was unexepectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
enableReminderView(false)
|
|
|
|
setReminderCheckbox.state = .off
|
|
|
|
|
|
|
|
if let label = model.customLabel, !label.isEmpty {
|
|
|
|
customLabel.stringValue = label
|
|
|
|
} else {
|
|
|
|
customLabel.stringValue = model.formattedTimezoneLabel()
|
|
|
|
}
|
|
|
|
|
|
|
|
if let note = model.note, !note.isEmpty {
|
|
|
|
notesTextView.string = note
|
|
|
|
} else {
|
|
|
|
notesTextView.string = CLEmptyString
|
|
|
|
}
|
|
|
|
|
|
|
|
setInitialReminderTime()
|
|
|
|
updateTimeFormat()
|
|
|
|
updateSecondsFormat()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateTimeFormat() {
|
|
|
|
if let overrideFormat = dataObject?.overrideFormat.rawValue {
|
|
|
|
timeFormatControl.setSelected(true, forSegment: overrideFormat)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateSecondsFormat() {
|
|
|
|
if let overrideFormat = dataObject?.overrideSecondsFormat.rawValue {
|
|
|
|
secondsFormatControl.setSelected(true, forSegment: overrideFormat)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func enableReminderView(_ shouldEnable: Bool) {
|
|
|
|
reminderPicker.isEnabled = shouldEnable
|
|
|
|
alertPopupButton.isEnabled = shouldEnable
|
|
|
|
reminderPicker.alphaValue = shouldEnable ? 1.0 : 0.25
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension NotesPopover: NSTextFieldDelegate {
|
|
|
|
func controlTextDidChange(_: Notification) {
|
|
|
|
updateLabel()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateLabel() {
|
|
|
|
// We need to do a couple of things if the customLabel is updated
|
|
|
|
// 1. Update the userDefaults
|
|
|
|
// 2. Check if the timezone is displayed in the menubar; if so, update the model
|
|
|
|
guard let model = dataObject else { return }
|
|
|
|
model.setLabel(customLabel.stringValue)
|
|
|
|
|
|
|
|
insertTimezoneInDefaultPreferences()
|
|
|
|
|
|
|
|
NotificationCenter.default.post(name: NSNotification.Name.customLabelChanged,
|
|
|
|
object: nil)
|
|
|
|
}
|
|
|
|
}
|