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.
 
 
 
 
 

494 lines
17 KiB

// 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(macOS 10.16, *) {
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?.timezoneID 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.timezoneID!,
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)
}
}