|
|
|
// Copyright © 2015 Abhishek Banthia
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
|
|
|
|
struct PreferencesConstants {
|
|
|
|
static let timezoneNameIdentifier = "formattedAddress"
|
|
|
|
static let customLabelIdentifier = "label"
|
|
|
|
static let availableTimezoneIdentifier = "availableTimezones"
|
|
|
|
static let noTimezoneSelectedErrorMessage = "Please select a timezone!"
|
|
|
|
static let maxTimezonesErrorMessage = "Maximum 100 timezones allowed!"
|
|
|
|
static let maxCharactersAllowed = "Only 50 characters allowed!"
|
|
|
|
static let noInternetConnectivityError = "You're offline, maybe?"
|
|
|
|
static let tryAgainMessage = "Try again, maybe?"
|
|
|
|
static let offlineErrorMessage = "The Internet connection appears to be offline."
|
|
|
|
}
|
|
|
|
|
|
|
|
enum RowType {
|
|
|
|
case timezoneHeader
|
|
|
|
case cityHeader
|
|
|
|
case city
|
|
|
|
case timezone
|
|
|
|
}
|
|
|
|
|
|
|
|
struct TimezoneMetadata {
|
|
|
|
let timezone: NSTimeZone
|
|
|
|
let tags: Set<String>
|
|
|
|
let formattedName: String
|
|
|
|
}
|
|
|
|
|
|
|
|
class PreferencesViewController: ParentViewController {
|
|
|
|
private var isActivityInProgress = false {
|
|
|
|
didSet {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.isActivityInProgress ? self.progressIndicator.startAnimation(nil) : self.progressIndicator.stopAnimation(nil)
|
|
|
|
self.availableTimezoneTableView.isEnabled = !self.isActivityInProgress
|
|
|
|
self.addButton.isEnabled = !self.isActivityInProgress
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var selectedTimeZones: [Data] {
|
|
|
|
return DataStore.shared().timezones()
|
|
|
|
}
|
|
|
|
|
|
|
|
private lazy var startupManager = StartupManager()
|
|
|
|
private var filteredArray: [Any] = []
|
|
|
|
private var timezoneArray: [TimezoneMetadata] = []
|
|
|
|
private var timezoneFilteredArray: [TimezoneMetadata] = []
|
|
|
|
private var dataTask: URLSessionDataTask? = .none
|
|
|
|
|
|
|
|
private lazy var notimezoneView: NoTimezoneView? = {
|
|
|
|
NoTimezoneView(frame: tableview.frame)
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Sorting
|
|
|
|
private var arePlacesSortedInAscendingOrder = false
|
|
|
|
private var arePlacesSortedInAscendingTimezoneOrder = false
|
|
|
|
private var isTimezoneSortOptionSelected = false
|
|
|
|
private var isTimezoneNameSortOptionSelected = false
|
|
|
|
private var isLabelOptionSelected = false
|
|
|
|
|
|
|
|
@IBOutlet private var placeholderLabel: NSTextField!
|
|
|
|
@IBOutlet private var timezoneTableView: NSTableView!
|
|
|
|
@IBOutlet private var availableTimezoneTableView: NSTableView!
|
|
|
|
@IBOutlet private var timezonePanel: Panelr!
|
|
|
|
@IBOutlet private var stackView: NSStackView!
|
|
|
|
@IBOutlet private var progressIndicator: NSProgressIndicator!
|
|
|
|
@IBOutlet private var addButton: NSButton!
|
|
|
|
@IBOutlet private var recorderControl: SRRecorderControl!
|
|
|
|
|
|
|
|
@IBOutlet private var timezoneSortButton: NSButton!
|
|
|
|
@IBOutlet private var timezoneNameSortButton: NSButton!
|
|
|
|
@IBOutlet private var labelSortButton: NSButton!
|
|
|
|
@IBOutlet private var deleteButton: NSButton!
|
|
|
|
@IBOutlet private var addTimezoneButton: NSButton!
|
|
|
|
|
|
|
|
@IBOutlet private var searchField: NSSearchField!
|
|
|
|
@IBOutlet private var messageLabel: NSTextField!
|
|
|
|
|
|
|
|
@IBOutlet private var headerView: NSView!
|
|
|
|
@IBOutlet private var tableview: NSView!
|
|
|
|
@IBOutlet private var additionalSortOptions: NSView!
|
|
|
|
@IBOutlet var startAtLoginLabel: NSTextField!
|
|
|
|
|
|
|
|
@IBOutlet var startupCheckbox: NSButton!
|
|
|
|
@IBOutlet var headerLabel: NSTextField!
|
|
|
|
|
|
|
|
@IBOutlet var sortToggle: NSButton!
|
|
|
|
private var themeDidChangeNotification: NSObjectProtocol?
|
|
|
|
private var selectionsDataSource: PreferencesDataSource!
|
|
|
|
private var finalArray: [RowType] = []
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
|
selector: #selector(refreshTimezoneTableView),
|
|
|
|
name: NSNotification.Name.customLabelChanged,
|
|
|
|
object: nil)
|
|
|
|
|
|
|
|
refreshTimezoneTableView()
|
|
|
|
|
|
|
|
setup()
|
|
|
|
|
|
|
|
setupShortcutObserver()
|
|
|
|
|
|
|
|
darkModeChanges()
|
|
|
|
|
|
|
|
themeDidChangeNotification = NotificationCenter.default.addObserver(forName: .themeDidChangeNotification, object: nil, queue: OperationQueue.main) { _ in
|
|
|
|
self.setup()
|
|
|
|
}
|
|
|
|
|
|
|
|
searchField.placeholderString = "Enter city, state, country or timezone name"
|
|
|
|
setupTimezoneDatasource()
|
|
|
|
|
|
|
|
selectionsDataSource = PreferencesDataSource(callbackDelegate: self)
|
|
|
|
timezoneTableView.dataSource = selectionsDataSource
|
|
|
|
timezoneTableView.delegate = selectionsDataSource
|
|
|
|
|
|
|
|
reloadSearchResults()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func calculateArray() {
|
|
|
|
finalArray = []
|
|
|
|
|
|
|
|
func addTimezonesIfNeeded(_ data: [TimezoneMetadata]) {
|
|
|
|
if !data.isEmpty {
|
|
|
|
finalArray.append(.timezoneHeader)
|
|
|
|
}
|
|
|
|
data.forEach { _ in
|
|
|
|
finalArray.append(.timezone)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if searchField.stringValue.isEmpty {
|
|
|
|
addTimezonesIfNeeded(timezoneArray)
|
|
|
|
} else {
|
|
|
|
if !filteredArray.isEmpty {
|
|
|
|
finalArray.append(.cityHeader)
|
|
|
|
}
|
|
|
|
filteredArray.forEach { _ in
|
|
|
|
finalArray.append(.city)
|
|
|
|
}
|
|
|
|
addTimezonesIfNeeded(timezoneFilteredArray)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
// We still need to remove observers set using NotificationCenter block: APIs
|
|
|
|
if let themeDidChangeNotif = themeDidChangeNotification {
|
|
|
|
NotificationCenter.default.removeObserver(themeDidChangeNotif)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func darkModeChanges() {
|
|
|
|
if #available(macOS 10.14, *) {
|
|
|
|
addTimezoneButton.image = NSImage(named: .addDynamicIcon)
|
|
|
|
sortToggle.image = NSImage(named: .sortToggleIcon)
|
|
|
|
sortToggle.alternateImage = NSImage(named: .sortToggleAlternateIcon)
|
|
|
|
deleteButton.image = NSImage(named: NSImage.Name("Remove Dynamic"))!
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupLocalizedText() {
|
|
|
|
startAtLoginLabel.stringValue = "Start Clocker at Login"
|
|
|
|
headerLabel.stringValue = "Selected Timezones"
|
|
|
|
timezoneSortButton.title = "Sort by Time Difference"
|
|
|
|
timezoneNameSortButton.title = "Sort by Name"
|
|
|
|
labelSortButton.title = "Sort by Label"
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func refreshTimezoneTableView() {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.build()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func refreshMainTable() {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.refresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func refresh() {
|
|
|
|
if DataStore.shared().shouldDisplay(ViewType.showAppInForeground) {
|
|
|
|
updateFloatingWindow()
|
|
|
|
} else {
|
|
|
|
guard let panel = PanelController.panel() else { return }
|
|
|
|
panel.updateDefaultPreferences()
|
|
|
|
panel.updateTableContent()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateFloatingWindow() {
|
|
|
|
let current = FloatingWindowController.shared()
|
|
|
|
current.updateDefaultPreferences()
|
|
|
|
current.updateTableContent()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func build() {
|
|
|
|
if DataStore.shared().timezones() == [] {
|
|
|
|
housekeeping()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if selectedTimeZones.isEmpty == false {
|
|
|
|
headerView.isHidden = false
|
|
|
|
|
|
|
|
if tableview.subviews.count > 1, let zeroView = notimezoneView, tableview.subviews.contains(zeroView) {
|
|
|
|
zeroView.removeFromSuperview()
|
|
|
|
timezoneTableView.enclosingScrollView?.isHidden = false
|
|
|
|
}
|
|
|
|
timezoneTableView.reloadData()
|
|
|
|
} else {
|
|
|
|
housekeeping()
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanup()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func housekeeping() {
|
|
|
|
timezoneTableView.enclosingScrollView?.isHidden = true
|
|
|
|
headerView.isHidden = true
|
|
|
|
showNoTimezoneState()
|
|
|
|
cleanup()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
private func cleanup() {
|
|
|
|
timezoneTableView.scrollRowToVisible(selectedTimeZones.count - 1)
|
|
|
|
updateMenubarTitles() // Update the menubar titles, the custom labels might have changed.
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateMenubarTitles() {
|
|
|
|
let defaultTimezones = DataStore.shared().timezones()
|
|
|
|
UserDefaults.standard.set([], forKey: CLMenubarFavorites)
|
|
|
|
|
|
|
|
let menubarTimes = defaultTimezones.compactMap { (data) -> TimezoneData? in
|
|
|
|
if let model = TimezoneData.customObject(from: data), model.isFavourite == 1 {
|
|
|
|
return model
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
let archivedObjects = menubarTimes.map { (timezone) -> Data in
|
|
|
|
NSKeyedArchiver.archivedData(withRootObject: timezone)
|
|
|
|
}
|
|
|
|
|
|
|
|
UserDefaults.standard.set(archivedObjects, forKey: CLMenubarFavorites)
|
|
|
|
|
|
|
|
// Update appereance if in compact menubar mode
|
|
|
|
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
|
|
|
|
appDelegate.setupMenubarTimer()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setup() {
|
|
|
|
setupAccessibilityIdentifiers()
|
|
|
|
|
|
|
|
deleteButton.isEnabled = false
|
|
|
|
|
|
|
|
[placeholderLabel, additionalSortOptions].forEach { $0.isHidden = true }
|
|
|
|
|
|
|
|
messageLabel.stringValue = CLEmptyString
|
|
|
|
|
|
|
|
timezoneTableView.registerForDraggedTypes([.dragSession])
|
|
|
|
|
|
|
|
progressIndicator.usesThreadedAnimation = true
|
|
|
|
|
|
|
|
setupLocalizedText()
|
|
|
|
|
|
|
|
setupColor()
|
|
|
|
|
|
|
|
startupCheckbox.integerValue = DataStore.shared().retrieve(key: CLStartAtLogin) as? Int ?? 0
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupColor() {
|
|
|
|
let themer = Themer.shared()
|
|
|
|
|
|
|
|
headerLabel.textColor = themer.mainTextColor()
|
|
|
|
startAtLoginLabel.textColor = Themer.shared().mainTextColor()
|
|
|
|
|
|
|
|
[timezoneNameSortButton, labelSortButton, timezoneSortButton].forEach {
|
|
|
|
$0?.attributedTitle = NSAttributedString(string: $0?.title ?? CLEmptyString, attributes: [
|
|
|
|
NSAttributedString.Key.foregroundColor: Themer.shared().mainTextColor(),
|
|
|
|
NSAttributedString.Key.font: NSFont(name: "Avenir-Light", size: 13)!,
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
addTimezoneButton.image = themer.addImage()
|
|
|
|
deleteButton.image = themer.removeImage()
|
|
|
|
sortToggle.image = themer.additionalPreferencesImage()
|
|
|
|
sortToggle.alternateImage = themer.additionalPreferencesHighlightedImage()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupShortcutObserver() {
|
|
|
|
let defaults = NSUserDefaultsController.shared
|
|
|
|
|
|
|
|
recorderControl.bind(NSBindingName.value,
|
|
|
|
to: defaults,
|
|
|
|
withKeyPath: "values.globalPing",
|
|
|
|
options: nil)
|
|
|
|
|
|
|
|
recorderControl.delegate = self
|
|
|
|
}
|
|
|
|
|
|
|
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
|
|
|
|
if let path = keyPath, path == "values.globalPing" {
|
|
|
|
let hotKeyCenter = PTHotKeyCenter.shared()
|
|
|
|
let oldHotKey = hotKeyCenter?.hotKey(withIdentifier: path)
|
|
|
|
hotKeyCenter?.unregisterHotKey(oldHotKey)
|
|
|
|
|
|
|
|
guard let newObject = object as? NSObject, let newShortcut = newObject.value(forKeyPath: path) as? [AnyHashable: Any] else {
|
|
|
|
assertionFailure("Unable to recognize shortcuts")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let newHotKey: PTHotKey = PTHotKey(identifier: keyPath,
|
|
|
|
keyCombo: newShortcut,
|
|
|
|
target: self,
|
|
|
|
action: #selector(ping(_:)))
|
|
|
|
|
|
|
|
hotKeyCenter?.register(newHotKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func ping(_ sender: Any) {
|
|
|
|
guard let delegate = NSApplication.shared.delegate as? AppDelegate else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
delegate.togglePanel(sender)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func showNoTimezoneState() {
|
|
|
|
if let zeroView = notimezoneView {
|
|
|
|
notimezoneView?.wantsLayer = true
|
|
|
|
tableview.addSubview(zeroView)
|
|
|
|
Logger.log(object: ["Showing Empty View": "YES"], for: "Showing Empty View")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupAccessibilityIdentifiers() {
|
|
|
|
timezoneTableView.setAccessibilityIdentifier("TimezoneTableView")
|
|
|
|
availableTimezoneTableView.setAccessibilityIdentifier("AvailableTimezoneTableView")
|
|
|
|
searchField.setAccessibilityIdentifier("AvailableSearchField")
|
|
|
|
timezoneSortButton.setAccessibility("SortByDifference")
|
|
|
|
labelSortButton.setAccessibility("SortByLabelButton")
|
|
|
|
timezoneNameSortButton.setAccessibility("SortByTimezoneName")
|
|
|
|
}
|
|
|
|
|
|
|
|
override var acceptsFirstResponder: Bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate {
|
|
|
|
func numberOfRows(in _: NSTableView) -> Int {
|
|
|
|
return numberOfSearchResults()
|
|
|
|
}
|
|
|
|
|
|
|
|
func tableView(_: NSTableView, isGroupRow row: Int) -> Bool {
|
|
|
|
let currentRowType = finalArray[row]
|
|
|
|
return
|
|
|
|
currentRowType == .timezoneHeader ||
|
|
|
|
currentRowType == .cityHeader
|
|
|
|
}
|
|
|
|
|
|
|
|
func tableView(_: NSTableView, shouldSelectRow row: Int) -> Bool {
|
|
|
|
print("Should Select Row")
|
|
|
|
let currentRowType = finalArray[row]
|
|
|
|
return !(currentRowType == .timezoneHeader || currentRowType == .cityHeader)
|
|
|
|
}
|
|
|
|
|
|
|
|
func tableView(_: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet {
|
|
|
|
// print("Selection Indexes for Proposed Selection: \(proposedSelectionIndexes.first!)")
|
|
|
|
return proposedSelectionIndexes
|
|
|
|
}
|
|
|
|
|
|
|
|
func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? {
|
|
|
|
let currentRowType = finalArray[row]
|
|
|
|
|
|
|
|
switch currentRowType {
|
|
|
|
case .timezoneHeader, .cityHeader:
|
|
|
|
return headerCell(tableView, currentRowType)
|
|
|
|
case .timezone:
|
|
|
|
return timezoneCell(tableView, currentRowType, row)
|
|
|
|
case .city:
|
|
|
|
return cityCell(tableView, currentRowType, row)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func timezoneCell(_ tableView: NSTableView, _: RowType, _ row: Int) -> NSView? {
|
|
|
|
if let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCell"), owner: self) as? SearchResultTableViewCell {
|
|
|
|
let datasource = searchField.stringValue.isEmpty ? timezoneArray : timezoneFilteredArray
|
|
|
|
guard !datasource.isEmpty else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
let index = searchField.stringValue.isEmpty ? row - 1 : row
|
|
|
|
message.sourceName.stringValue = datasource[index % datasource.count].formattedName
|
|
|
|
return message
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
private func cityCell(_ tableView: NSTableView, _: RowType, _ row: Int) -> NSView? {
|
|
|
|
if let cityCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCell"), owner: self) as? SearchResultTableViewCell {
|
|
|
|
guard let timezoneData = filteredArray[row % filteredArray.count] as? TimezoneData else {
|
|
|
|
assertionFailure()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
cityCell.sourceName.stringValue = timezoneData.formattedAddress ?? "Error"
|
|
|
|
return cityCell
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
private func headerCell(_ tableView: NSTableView, _ headerType: RowType) -> NSView? {
|
|
|
|
if let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "headerCell"), owner: self) as? HeaderTableViewCell {
|
|
|
|
message.headerField.stringValue = headerType == .timezoneHeader ? "Timezones" : "Places"
|
|
|
|
return message
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
private func _markAsFavorite(_ dataObject: TimezoneData) {
|
|
|
|
guard let menubarTitles = DataStore.shared().retrieve(key: CLMenubarFavorites) as? [Data] else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var mutableArray = menubarTitles
|
|
|
|
let archivedObject = NSKeyedArchiver.archivedData(withRootObject: dataObject)
|
|
|
|
mutableArray.append(archivedObject)
|
|
|
|
|
|
|
|
UserDefaults.standard.set(mutableArray, forKey: CLMenubarFavorites)
|
|
|
|
|
|
|
|
if dataObject.customLabel != nil {
|
|
|
|
Logger.log(object: ["label": dataObject.customLabel ?? "Error"], for: "favouriteSelected")
|
|
|
|
}
|
|
|
|
|
|
|
|
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
|
|
|
|
appDelegate.setupMenubarTimer()
|
|
|
|
}
|
|
|
|
|
|
|
|
if mutableArray.count > 1 {
|
|
|
|
showAlertIfMoreThanOneTimezoneHasBeenAddedToTheMenubar()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func _unfavourite(_ dataObject: TimezoneData) {
|
|
|
|
guard let menubarTimers = DataStore.shared().retrieve(key: CLMenubarFavorites) as? [Data] else {
|
|
|
|
assertionFailure("Menubar timers is unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
Logger.log(object: ["label": dataObject.customLabel ?? "Error"],
|
|
|
|
for: "favouriteRemoved")
|
|
|
|
|
|
|
|
let filteredMenubars = menubarTimers.filter {
|
|
|
|
guard let current = NSKeyedUnarchiver.unarchiveObject(with: $0) as? TimezoneData else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return current.isEqual(dataObject) == false
|
|
|
|
}
|
|
|
|
|
|
|
|
UserDefaults.standard.set(filteredMenubars, forKey: CLMenubarFavorites)
|
|
|
|
|
|
|
|
if let appDelegate = NSApplication.shared.delegate as? AppDelegate,
|
|
|
|
let menubarFavourites = DataStore.shared().retrieve(key: CLMenubarFavorites) as? [Data],
|
|
|
|
menubarFavourites.isEmpty,
|
|
|
|
DataStore.shared().shouldDisplay(.showMeetingInMenubar) == false {
|
|
|
|
appDelegate.invalidateMenubarTimer(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
|
|
|
|
appDelegate.setupMenubarTimer()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func showAlertIfMoreThanOneTimezoneHasBeenAddedToTheMenubar() {
|
|
|
|
let isUITestRunning = ProcessInfo.processInfo.arguments.contains(CLUITestingLaunchArgument)
|
|
|
|
|
|
|
|
// If we have seen displayed the message before, abort!
|
|
|
|
let haveWeSeenThisMessageBefore = UserDefaults.standard.bool(forKey: CLLongStatusBarWarningMessage)
|
|
|
|
|
|
|
|
if haveWeSeenThisMessageBefore, !isUITestRunning {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the user is already using the compact mode, abort.
|
|
|
|
if DataStore.shared().shouldDisplay(.menubarCompactMode), !isUITestRunning {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Time to display the alert.
|
|
|
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
|
|
|
|
|
|
|
let infoText = """
|
|
|
|
Multiple timezones occupy space and if macOS determines Clocker is occupying too much space, it'll hide Clocker entirely!
|
|
|
|
Enable Menubar Compact Mode to fit in more timezones in less space.
|
|
|
|
"""
|
|
|
|
|
|
|
|
let alert = NSAlert()
|
|
|
|
alert.showsSuppressionButton = true
|
|
|
|
alert.messageText = "More than one location added to the menubar 😅"
|
|
|
|
alert.informativeText = infoText
|
|
|
|
alert.addButton(withTitle: "Enable Compact Mode")
|
|
|
|
alert.addButton(withTitle: "Cancel")
|
|
|
|
|
|
|
|
let response = alert.runModal()
|
|
|
|
|
|
|
|
if response.rawValue == 1000 {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
UserDefaults.standard.set(0, forKey: CLMenubarCompactMode)
|
|
|
|
|
|
|
|
if alert.suppressionButton?.state == NSControl.StateValue.on {
|
|
|
|
UserDefaults.standard.set(true, forKey: CLLongStatusBarWarningMessage)
|
|
|
|
}
|
|
|
|
|
|
|
|
self.updateStatusBarAppearance()
|
|
|
|
|
|
|
|
Logger.log(object: ["Context": ">1 Menubar Timezone in Preferences"], for: "Switched to Compact Mode")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension PreferencesViewController {
|
|
|
|
@objc private func search() {
|
|
|
|
let searchString = searchField.stringValue
|
|
|
|
|
|
|
|
if searchString.isEmpty {
|
|
|
|
dataTask?.cancel()
|
|
|
|
resetSearchView()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if dataTask?.state == .running {
|
|
|
|
dataTask?.cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
if self.availableTimezoneTableView.isHidden {
|
|
|
|
self.availableTimezoneTableView.isHidden = false
|
|
|
|
}
|
|
|
|
|
|
|
|
self.placeholderLabel.isHidden = false
|
|
|
|
|
|
|
|
if NetworkManager.isConnected() == false {
|
|
|
|
self.placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.isActivityInProgress = true
|
|
|
|
|
|
|
|
self.placeholderLabel.placeholderString = "Searching for \(searchString)"
|
|
|
|
|
|
|
|
self.dataTask = NetworkManager.task(with: self.generateSearchURL(),
|
|
|
|
completionHandler: { [weak self] response, error in
|
|
|
|
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
if let errorPresent = error {
|
|
|
|
self.presentError(errorPresent.localizedDescription)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let data = response else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let searchResults = self.decode(from: data)
|
|
|
|
|
|
|
|
if searchResults?.status == "ZERO_RESULTS" {
|
|
|
|
self.placeholderLabel.placeholderString = "No results! 😔 Try entering the exact name."
|
|
|
|
self.findLocalSearchResultsForTimezones()
|
|
|
|
self.reloadSearchResults()
|
|
|
|
self.isActivityInProgress = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.appendResultsToFilteredArray(searchResults!.results)
|
|
|
|
self.findLocalSearchResultsForTimezones()
|
|
|
|
self.prepareUIForPresentingResults()
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func findLocalSearchResultsForTimezones() {
|
|
|
|
timezoneFilteredArray = []
|
|
|
|
let lowercasedSearchString = searchField.stringValue.lowercased()
|
|
|
|
|
|
|
|
timezoneFilteredArray = timezoneArray.filter { (timezoneMetadata) -> Bool in
|
|
|
|
let tags = timezoneMetadata.tags
|
|
|
|
for tag in tags where tag.contains(lowercasedSearchString) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
print(timezoneFilteredArray)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func generateSearchURL() -> String {
|
|
|
|
let userPreferredLanguage = Locale.preferredLanguages.first ?? "en-US"
|
|
|
|
|
|
|
|
var searchString = searchField.stringValue
|
|
|
|
let words = searchString.components(separatedBy: CharacterSet.whitespacesAndNewlines)
|
|
|
|
searchString = words.joined(separator: CLEmptyString)
|
|
|
|
|
|
|
|
let url = "https://maps.googleapis.com/maps/api/geocode/json?address=\(searchString)&key=\(CLGeocodingKey)&language=\(userPreferredLanguage)"
|
|
|
|
return url
|
|
|
|
}
|
|
|
|
|
|
|
|
private func presentError(_ errorMessage: String) {
|
|
|
|
if errorMessage == PreferencesConstants.offlineErrorMessage {
|
|
|
|
placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError
|
|
|
|
} else {
|
|
|
|
placeholderLabel.placeholderString = PreferencesConstants.tryAgainMessage
|
|
|
|
}
|
|
|
|
|
|
|
|
isActivityInProgress = false
|
|
|
|
}
|
|
|
|
|
|
|
|
private func appendResultsToFilteredArray(_ results: [SearchResult.Result]) {
|
|
|
|
results.forEach {
|
|
|
|
let location = $0.geometry.location
|
|
|
|
let latitude = location.lat
|
|
|
|
let longitude = location.lng
|
|
|
|
let formattedAddress = $0.formattedAddress
|
|
|
|
|
|
|
|
let totalPackage = [
|
|
|
|
"latitude": latitude,
|
|
|
|
"longitude": longitude,
|
|
|
|
CLTimezoneName: formattedAddress,
|
|
|
|
CLCustomLabel: formattedAddress,
|
|
|
|
CLTimezoneID: CLEmptyString,
|
|
|
|
CLPlaceIdentifier: $0.placeId,
|
|
|
|
] as [String: Any]
|
|
|
|
|
|
|
|
self.filteredArray.append(TimezoneData(with: totalPackage))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func prepareUIForPresentingResults() {
|
|
|
|
placeholderLabel.placeholderString = CLEmptyString
|
|
|
|
isActivityInProgress = false
|
|
|
|
reloadSearchResults()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func reloadSearchResults() {
|
|
|
|
print("Reloading Search Results")
|
|
|
|
calculateArray()
|
|
|
|
availableTimezoneTableView.reloadData()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extracting this out for tests
|
|
|
|
private func decode(from data: Data) -> SearchResult? {
|
|
|
|
let jsonDecoder = JSONDecoder()
|
|
|
|
do {
|
|
|
|
let decodedObject = try jsonDecoder.decode(SearchResult.self, from: data)
|
|
|
|
return decodedObject
|
|
|
|
} catch {
|
|
|
|
print("decodedObject error: \n\(error)")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extracting this out for tests
|
|
|
|
private func decodeTimezone(from data: Data) -> Timezone? {
|
|
|
|
let jsonDecoder = JSONDecoder()
|
|
|
|
do {
|
|
|
|
let decodedObject = try jsonDecoder.decode(Timezone.self, from: data)
|
|
|
|
return decodedObject
|
|
|
|
} catch {
|
|
|
|
print("decodedObject error: \n\(error)")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func resetSearchView() {
|
|
|
|
if dataTask?.state == .running {
|
|
|
|
dataTask?.cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
isActivityInProgress = false
|
|
|
|
placeholderLabel.placeholderString = CLEmptyString
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getTimezone(for latitude: Double, and longitude: Double) {
|
|
|
|
if placeholderLabel.isHidden {
|
|
|
|
placeholderLabel.isHidden = false
|
|
|
|
}
|
|
|
|
|
|
|
|
if NetworkManager.isConnected() == false || ProcessInfo.processInfo.arguments.contains("mockTimezoneDown") {
|
|
|
|
resetStateAndShowDisconnectedMessage()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
searchField.placeholderString = "Fetching data might take some time!"
|
|
|
|
placeholderLabel.placeholderString = "Retrieving timezone data"
|
|
|
|
availableTimezoneTableView.isHidden = true
|
|
|
|
|
|
|
|
let tuple = "\(latitude),\(longitude)"
|
|
|
|
let timeStamp = Date().timeIntervalSince1970
|
|
|
|
let urlString = "https://maps.googleapis.com/maps/api/timezone/json?location=\(tuple)×tamp=\(timeStamp)&key=\(CLGeocodingKey)"
|
|
|
|
|
|
|
|
NetworkManager.task(with: urlString) { [weak self] response, error in
|
|
|
|
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
if self.handleEdgeCase(for: response) == true {
|
|
|
|
self.reloadSearchResults()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if error == nil, let json = response, let timezone = self.decodeTimezone(from: json) {
|
|
|
|
if self.availableTimezoneTableView.selectedRow >= 0 {
|
|
|
|
self.installTimezone(timezone)
|
|
|
|
}
|
|
|
|
self.updateViewState()
|
|
|
|
} else {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
if error?.localizedDescription == "The Internet connection appears to be offline." {
|
|
|
|
self.placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError
|
|
|
|
} else {
|
|
|
|
self.placeholderLabel.placeholderString = PreferencesConstants.tryAgainMessage
|
|
|
|
}
|
|
|
|
|
|
|
|
self.isActivityInProgress = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func installTimezone(_ timezone: Timezone) {
|
|
|
|
guard let dataObject = self.filteredArray[self.availableTimezoneTableView.selectedRow % filteredArray.count] as? TimezoneData else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var filteredAddress = "Error"
|
|
|
|
|
|
|
|
if let address = dataObject.formattedAddress {
|
|
|
|
filteredAddress = address.filteredName()
|
|
|
|
}
|
|
|
|
|
|
|
|
let newTimeZone = [
|
|
|
|
CLTimezoneID: timezone.timeZoneId,
|
|
|
|
CLTimezoneName: filteredAddress,
|
|
|
|
CLPlaceIdentifier: dataObject.placeID!,
|
|
|
|
"latitude": dataObject.latitude!,
|
|
|
|
"longitude": dataObject.longitude!,
|
|
|
|
"nextUpdate": CLEmptyString,
|
|
|
|
CLCustomLabel: filteredAddress,
|
|
|
|
] as [String: Any]
|
|
|
|
|
|
|
|
let timezoneObject = TimezoneData(with: newTimeZone)
|
|
|
|
let operationsObject = TimezoneDataOperations(with: timezoneObject)
|
|
|
|
operationsObject.saveObject()
|
|
|
|
|
|
|
|
Logger.log(object: ["PlaceName": filteredAddress, "Timezone": timezone.timeZoneId], for: "Filtered Address")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func resetStateAndShowDisconnectedMessage() {
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.showMessage()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func showMessage() {
|
|
|
|
placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError
|
|
|
|
isActivityInProgress = false
|
|
|
|
filteredArray = []
|
|
|
|
reloadSearchResults()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if there's an error.
|
|
|
|
private func handleEdgeCase(for response: Data?) -> Bool {
|
|
|
|
guard let json = response, let jsonUnserialized = try? JSONSerialization.jsonObject(with: json, options: .allowFragments), let unwrapped = jsonUnserialized as? [String: Any] else {
|
|
|
|
setErrorPlaceholders()
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if let status = unwrapped["status"] as? String, status == "ZERO_RESULTS" {
|
|
|
|
setErrorPlaceholders()
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setErrorPlaceholders() {
|
|
|
|
placeholderLabel.placeholderString = "No timezone found! Try entering an exact name."
|
|
|
|
searchField.placeholderString = "Enter a city, state or country name"
|
|
|
|
isActivityInProgress = false
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateViewState() {
|
|
|
|
filteredArray = []
|
|
|
|
reloadSearchResults()
|
|
|
|
refreshTimezoneTableView()
|
|
|
|
refreshMainTable()
|
|
|
|
timezonePanel.close()
|
|
|
|
placeholderLabel.placeholderString = CLEmptyString
|
|
|
|
searchField.placeholderString = "Enter a city, state or country name"
|
|
|
|
availableTimezoneTableView.isHidden = false
|
|
|
|
isActivityInProgress = false
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupTimezoneDatasource() {
|
|
|
|
timezoneArray = []
|
|
|
|
|
|
|
|
let anywhereOnEarth = TimezoneMetadata(timezone: NSTimeZone(abbreviation: "GMT-1200")!,
|
|
|
|
tags: ["aoe", "anywhere on earth"],
|
|
|
|
formattedName: "Anywhere on Earth")
|
|
|
|
timezoneArray.append(anywhereOnEarth)
|
|
|
|
|
|
|
|
for (abbreviation, timezone) in TimeZone.abbreviationDictionary {
|
|
|
|
var tags: Set<String> = [abbreviation.lowercased(), timezone.lowercased()]
|
|
|
|
var extraTags: [String] = []
|
|
|
|
if abbreviation == "IST" {
|
|
|
|
extraTags = ["india", "indian", "kolkata", "calcutta", "mumbai", "delhi", "hyderabad", "noida"]
|
|
|
|
}
|
|
|
|
|
|
|
|
if abbreviation == "PST" {
|
|
|
|
extraTags = ["los", "los angeles", "california", "san francisco", "bay area", "pacific standard time"]
|
|
|
|
}
|
|
|
|
|
|
|
|
if abbreviation == "UTC" {
|
|
|
|
extraTags = ["utc", "universal"]
|
|
|
|
}
|
|
|
|
|
|
|
|
if abbreviation == "EST" {
|
|
|
|
extraTags = ["florida", "new york"]
|
|
|
|
}
|
|
|
|
|
|
|
|
extraTags.forEach { tag in
|
|
|
|
tags.insert(tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
let timezoneIdentifier = NSTimeZone(name: timezone)!
|
|
|
|
let timezoneMetadata = TimezoneMetadata(timezone: timezoneIdentifier, tags: tags, formattedName: timezone)
|
|
|
|
timezoneArray.append(timezoneMetadata)
|
|
|
|
}
|
|
|
|
|
|
|
|
print(TimeZone.knownTimeZoneIdentifiers.count)
|
|
|
|
print(timezoneArray.count)
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func addTimeZone(_: NSButton) {
|
|
|
|
filteredArray = []
|
|
|
|
view.window?.beginSheet(timezonePanel,
|
|
|
|
completionHandler: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func addToFavorites(_: NSButton) {
|
|
|
|
isActivityInProgress = true
|
|
|
|
|
|
|
|
if availableTimezoneTableView.selectedRow == -1 {
|
|
|
|
messageLabel.stringValue = PreferencesConstants.noTimezoneSelectedErrorMessage
|
|
|
|
|
|
|
|
Timer.scheduledTimer(withTimeInterval: 5,
|
|
|
|
repeats: false) { _ in
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.messageLabel.stringValue = CLEmptyString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
isActivityInProgress = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if selectedTimeZones.count >= 100 {
|
|
|
|
messageLabel.stringValue = PreferencesConstants.maxTimezonesErrorMessage
|
|
|
|
Timer.scheduledTimer(withTimeInterval: 5,
|
|
|
|
repeats: false) { _ in
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.messageLabel.stringValue = CLEmptyString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
isActivityInProgress = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if searchField.stringValue.isEmpty {
|
|
|
|
addTimezoneIfSearchStringIsEmpty()
|
|
|
|
} else {
|
|
|
|
addTimezoneIfSearchStringIsNotEmpty()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func addTimezoneIfSearchStringIsEmpty() {
|
|
|
|
let currentRowType = finalArray[availableTimezoneTableView.selectedRow]
|
|
|
|
|
|
|
|
switch currentRowType {
|
|
|
|
case .timezoneHeader, .cityHeader:
|
|
|
|
isActivityInProgress = false
|
|
|
|
return
|
|
|
|
case .timezone:
|
|
|
|
cleanupAfterInstallingTimezone()
|
|
|
|
default:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func addTimezoneIfSearchStringIsNotEmpty() {
|
|
|
|
let currentRowType = finalArray[availableTimezoneTableView.selectedRow]
|
|
|
|
|
|
|
|
switch currentRowType {
|
|
|
|
case .timezoneHeader, .cityHeader:
|
|
|
|
isActivityInProgress = false
|
|
|
|
return
|
|
|
|
case .timezone:
|
|
|
|
cleanupAfterInstallingTimezone()
|
|
|
|
case .city:
|
|
|
|
cleanupAfterInstallingCity()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func cleanupAfterInstallingCity() {
|
|
|
|
guard let dataObject = filteredArray[availableTimezoneTableView.selectedRow % filteredArray.count] as? TimezoneData else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if messageLabel.stringValue.isEmpty {
|
|
|
|
searchField.stringValue = CLEmptyString
|
|
|
|
|
|
|
|
guard let latitude = dataObject.latitude, let longitude = dataObject.longitude else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
getTimezone(for: latitude, and: longitude)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func cleanupAfterInstallingTimezone() {
|
|
|
|
let data = TimezoneData()
|
|
|
|
data.setLabel(CLEmptyString)
|
|
|
|
|
|
|
|
if searchField.stringValue.isEmpty == false {
|
|
|
|
let currentSelection = timezoneFilteredArray[availableTimezoneTableView.selectedRow % timezoneFilteredArray.count]
|
|
|
|
|
|
|
|
let metaInfo = metadata(for: currentSelection)
|
|
|
|
data.timezoneID = metaInfo.0
|
|
|
|
data.formattedAddress = metaInfo.1.formattedName
|
|
|
|
|
|
|
|
} else {
|
|
|
|
let currentSelection = timezoneArray[availableTimezoneTableView.selectedRow - 1]
|
|
|
|
|
|
|
|
let metaInfo = metadata(for: currentSelection)
|
|
|
|
data.timezoneID = metaInfo.0
|
|
|
|
data.formattedAddress = metaInfo.1.formattedName
|
|
|
|
}
|
|
|
|
|
|
|
|
data.selectionType = .timezone
|
|
|
|
|
|
|
|
let operationObject = TimezoneDataOperations(with: data)
|
|
|
|
operationObject.saveObject()
|
|
|
|
|
|
|
|
filteredArray = []
|
|
|
|
timezoneFilteredArray = []
|
|
|
|
placeholderLabel.placeholderString = CLEmptyString
|
|
|
|
searchField.stringValue = CLEmptyString
|
|
|
|
|
|
|
|
reloadSearchResults()
|
|
|
|
refreshTimezoneTableView()
|
|
|
|
refreshMainTable()
|
|
|
|
|
|
|
|
timezonePanel.close()
|
|
|
|
searchField.placeholderString = "Enter a city, state or country name"
|
|
|
|
availableTimezoneTableView.isHidden = false
|
|
|
|
isActivityInProgress = false
|
|
|
|
}
|
|
|
|
|
|
|
|
private func metadata(for selection: TimezoneMetadata) -> (String, TimezoneMetadata) {
|
|
|
|
if selection.formattedName == "Anywhere on Earth" {
|
|
|
|
return ("GMT-1200", selection)
|
|
|
|
} else if selection.formattedName == "UTC" {
|
|
|
|
return ("GMT", selection)
|
|
|
|
} else {
|
|
|
|
return (selection.formattedName, selection)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func closePanel(_: NSButton) {
|
|
|
|
filteredArray = []
|
|
|
|
timezoneFilteredArray = []
|
|
|
|
searchField.stringValue = CLEmptyString
|
|
|
|
placeholderLabel.placeholderString = CLEmptyString
|
|
|
|
searchField.placeholderString = "Enter a city, state or country name"
|
|
|
|
|
|
|
|
reloadSearchResults()
|
|
|
|
|
|
|
|
timezonePanel.close()
|
|
|
|
isActivityInProgress = false
|
|
|
|
addTimezoneButton.state = .off
|
|
|
|
|
|
|
|
// The table might be hidden because of an early exit especially
|
|
|
|
// if we are not able to fetch an associated timezone
|
|
|
|
// For eg. Europe doesn't have an associated timezone
|
|
|
|
availableTimezoneTableView.isHidden = false
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func removeFromFavourites(_: NSButton) {
|
|
|
|
// If the user is editing a row, and decides to delete the row then we have a crash
|
|
|
|
if timezoneTableView.editedRow != -1 || timezoneTableView.editedColumn != -1 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if timezoneTableView.selectedRow == -1, selectedTimeZones.count <= timezoneTableView.selectedRow {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let currentObject = selectedTimeZones[timezoneTableView.selectedRow]
|
|
|
|
guard let model = TimezoneData.customObject(from: currentObject) else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if model.isFavourite == 1 {
|
|
|
|
removeFromMenubarFavourites(object: model)
|
|
|
|
}
|
|
|
|
|
|
|
|
var newDefaults = selectedTimeZones
|
|
|
|
|
|
|
|
let objectsToRemove = timezoneTableView.selectedRowIndexes.map { (index) -> Data in
|
|
|
|
selectedTimeZones[index]
|
|
|
|
}
|
|
|
|
|
|
|
|
newDefaults = newDefaults.filter { !objectsToRemove.contains($0) }
|
|
|
|
|
|
|
|
DataStore.shared().setTimezones(newDefaults)
|
|
|
|
|
|
|
|
timezoneTableView.reloadData()
|
|
|
|
|
|
|
|
refreshTimezoneTableView()
|
|
|
|
|
|
|
|
refreshMainTable()
|
|
|
|
|
|
|
|
if selectedTimeZones.isEmpty {
|
|
|
|
UserDefaults.standard.set(nil, forKey: CLMenubarFavorites)
|
|
|
|
}
|
|
|
|
|
|
|
|
updateStatusBarAppearance()
|
|
|
|
|
|
|
|
updateStatusItem()
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: This probably does not need to be used
|
|
|
|
private func updateStatusItem() {
|
|
|
|
guard let statusItem = (NSApplication.shared.delegate as? AppDelegate)?.statusItemForPanel() else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
statusItem.performTimerWork()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateStatusBarAppearance() {
|
|
|
|
guard let statusItem = (NSApplication.shared.delegate as? AppDelegate)?.statusItemForPanel() else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
statusItem.setupStatusItem()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func removeFromMenubarFavourites(object: TimezoneData?) {
|
|
|
|
guard let model = object else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if model.isFavourite == 1 {
|
|
|
|
if let menubarTitles = DataStore.shared().retrieve(key: CLMenubarFavorites) as? [Data] {
|
|
|
|
let updated = menubarTitles.filter { (data) -> Bool in
|
|
|
|
let current = TimezoneData.customObject(from: data)
|
|
|
|
return current != model
|
|
|
|
}
|
|
|
|
|
|
|
|
UserDefaults.standard.set(updated, forKey: CLMenubarFavorites)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func filterArray(_: Any?) {
|
|
|
|
messageLabel.stringValue = CLEmptyString
|
|
|
|
|
|
|
|
filteredArray = []
|
|
|
|
|
|
|
|
if searchField.stringValue.count > 50 {
|
|
|
|
isActivityInProgress = false
|
|
|
|
messageLabel.stringValue = PreferencesConstants.maxCharactersAllowed
|
|
|
|
reloadSearchResults()
|
|
|
|
Timer.scheduledTimer(withTimeInterval: 5,
|
|
|
|
repeats: false) { _ in
|
|
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.messageLabel.stringValue = CLEmptyString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if searchField.stringValue.isEmpty == false {
|
|
|
|
dataTask?.cancel()
|
|
|
|
NSObject.cancelPreviousPerformRequests(withTarget: self)
|
|
|
|
perform(#selector(search), with: nil, afterDelay: 0.5)
|
|
|
|
} else {
|
|
|
|
resetSearchView()
|
|
|
|
}
|
|
|
|
|
|
|
|
reloadSearchResults()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension PreferencesViewController {
|
|
|
|
@IBAction func loginPreferenceChanged(_ sender: NSButton) {
|
|
|
|
startupManager.toggleLogin(sender.state == .on)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sorting
|
|
|
|
extension PreferencesViewController {
|
|
|
|
@IBAction func sortOptions(_: NSButton) {
|
|
|
|
additionalSortOptions.isHidden.toggle()
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func sortByTime(_ sender: NSButton) {
|
|
|
|
let sortedByTime = selectedTimeZones.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.timezoneID!)
|
|
|
|
let timezone2 = NSTimeZone(name: object2.timezoneID!)
|
|
|
|
|
|
|
|
let difference1 = system.secondsFromGMT() - timezone1!.secondsFromGMT
|
|
|
|
let difference2 = system.secondsFromGMT() - timezone2!.secondsFromGMT
|
|
|
|
|
|
|
|
return arePlacesSortedInAscendingTimezoneOrder ? difference1 > difference2 : difference1 < difference2
|
|
|
|
}
|
|
|
|
|
|
|
|
sender.image = arePlacesSortedInAscendingTimezoneOrder ? NSImage(named: NSImage.Name("NSDescendingSortIndicator"))! : NSImage(named: NSImage.Name("NSAscendingSortIndicator"))!
|
|
|
|
|
|
|
|
arePlacesSortedInAscendingTimezoneOrder.toggle()
|
|
|
|
|
|
|
|
DataStore.shared().setTimezones(sortedByTime)
|
|
|
|
|
|
|
|
updateAfterSorting()
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func sortByLabel(_ sender: NSButton) {
|
|
|
|
let sortedLabels = selectedTimeZones.sorted { (obj1, obj2) -> Bool in
|
|
|
|
|
|
|
|
guard let object1 = TimezoneData.customObject(from: obj1),
|
|
|
|
let object2 = TimezoneData.customObject(from: obj2) else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return isLabelOptionSelected ? object1.customLabel! > object2.customLabel! : object1.customLabel! < object2.customLabel!
|
|
|
|
}
|
|
|
|
|
|
|
|
sender.image = isLabelOptionSelected ?
|
|
|
|
NSImage(named: NSImage.Name("NSDescendingSortIndicator"))! :
|
|
|
|
NSImage(named: NSImage.Name("NSAscendingSortIndicator"))!
|
|
|
|
|
|
|
|
isLabelOptionSelected.toggle()
|
|
|
|
|
|
|
|
DataStore.shared().setTimezones(sortedLabels)
|
|
|
|
|
|
|
|
updateAfterSorting()
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func sortByFormattedAddress(_ sender: NSButton) {
|
|
|
|
let sortedByAddress = selectedTimeZones.sorted { (obj1, obj2) -> Bool in
|
|
|
|
|
|
|
|
guard let object1 = TimezoneData.customObject(from: obj1),
|
|
|
|
let object2 = TimezoneData.customObject(from: obj2) else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return isTimezoneNameSortOptionSelected ? object1.formattedAddress! > object2.formattedAddress! : object1.formattedAddress! < object2.formattedAddress!
|
|
|
|
}
|
|
|
|
|
|
|
|
sender.image = isTimezoneNameSortOptionSelected ? NSImage(named: NSImage.Name("NSDescendingSortIndicator"))! : NSImage(named: NSImage.Name("NSAscendingSortIndicator"))!
|
|
|
|
|
|
|
|
isTimezoneNameSortOptionSelected.toggle()
|
|
|
|
|
|
|
|
DataStore.shared().setTimezones(sortedByAddress)
|
|
|
|
|
|
|
|
updateAfterSorting()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateAfterSorting() {
|
|
|
|
let newDefaults = selectedTimeZones
|
|
|
|
DataStore.shared().setTimezones(newDefaults)
|
|
|
|
refreshTimezoneTableView()
|
|
|
|
refreshMainTable()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension PreferencesViewController: SRRecorderControlDelegate {}
|
|
|
|
|
|
|
|
// Helpers
|
|
|
|
extension PreferencesViewController {
|
|
|
|
private func numberOfSearchResults() -> Int {
|
|
|
|
return finalArray.count
|
|
|
|
}
|
|
|
|
|
|
|
|
private func insert(timezone: TimezoneData, at index: Int) {
|
|
|
|
let encodedObject = NSKeyedArchiver.archivedData(withRootObject: timezone)
|
|
|
|
var newDefaults = selectedTimeZones
|
|
|
|
newDefaults[index] = encodedObject
|
|
|
|
DataStore.shared().setTimezones(newDefaults)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class SearchResultTableViewCell: NSTableCellView {
|
|
|
|
@IBOutlet var sourceName: NSTextField!
|
|
|
|
}
|
|
|
|
|
|
|
|
class HeaderTableViewCell: NSTableCellView {
|
|
|
|
@IBOutlet var headerField: NSTextField!
|
|
|
|
}
|
|
|
|
|
|
|
|
extension PreferencesViewController: PreferenceSelectionUpdates {
|
|
|
|
func markAsFavorite(_ dataObject: TimezoneData) {
|
|
|
|
_markAsFavorite(dataObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
func unfavourite(_ dataObject: TimezoneData) {
|
|
|
|
_unfavourite(dataObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
func refreshTimezoneTable() {
|
|
|
|
refreshTimezoneTableView()
|
|
|
|
}
|
|
|
|
|
|
|
|
func refreshMainTableView() {
|
|
|
|
refreshMainTable()
|
|
|
|
}
|
|
|
|
|
|
|
|
func tableViewSelectionDidChange(_ status: Bool) {
|
|
|
|
deleteButton.isEnabled = !status
|
|
|
|
}
|
|
|
|
|
|
|
|
func table(didClick tableColumn: NSTableColumn) {
|
|
|
|
if tableColumn.identifier.rawValue == "favouriteTimezone" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let sortedTimezones = selectedTimeZones.sorted { (obj1, obj2) -> Bool in
|
|
|
|
|
|
|
|
guard let object1 = TimezoneData.customObject(from: obj1),
|
|
|
|
let object2 = TimezoneData.customObject(from: obj2) else {
|
|
|
|
assertionFailure("Data was unexpectedly nil")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if tableColumn.identifier.rawValue == "formattedAddress" {
|
|
|
|
return arePlacesSortedInAscendingOrder ?
|
|
|
|
object1.formattedAddress! > object2.formattedAddress! :
|
|
|
|
object1.formattedAddress! < object2.formattedAddress!
|
|
|
|
} else {
|
|
|
|
return arePlacesSortedInAscendingOrder ?
|
|
|
|
object1.customLabel! > object2.customLabel! :
|
|
|
|
object1.customLabel! < object2.customLabel!
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let indicatorImage = arePlacesSortedInAscendingOrder ?
|
|
|
|
NSImage(named: NSImage.Name("NSDescendingSortIndicator"))! :
|
|
|
|
NSImage(named: NSImage.Name("NSAscendingSortIndicator"))!
|
|
|
|
|
|
|
|
timezoneTableView.setIndicatorImage(indicatorImage, in: tableColumn)
|
|
|
|
|
|
|
|
arePlacesSortedInAscendingOrder.toggle()
|
|
|
|
|
|
|
|
DataStore.shared().setTimezones(sortedTimezones)
|
|
|
|
|
|
|
|
updateAfterSorting()
|
|
|
|
}
|
|
|
|
}
|