// C o p y r i g h t © 2 0 1 5 A b h i s h e k B a n t h i a
import Cocoa
import CoreLoggerKit
import CoreModelKit
import StartupKit
struct PreferencesConstants {
static let noTimezoneSelectedErrorMessage = NSLocalizedString ( " No Timezone Selected " ,
comment : " Message shown when the user taps on Add without selecting a timezone " )
static let maxTimezonesErrorMessage = NSLocalizedString ( " Max Timezones Selected " ,
comment : " Max Timezones Error Message " )
static let maxCharactersAllowed = NSLocalizedString ( " Max Search Characters " ,
comment : " Max Character Count Allowed Error Message " )
static let noInternetConnectivityError = " You're offline, maybe? " . localized ( )
static let tryAgainMessage = " Try again, maybe? " . localized ( )
static let offlineErrorMessage = " The Internet connection appears to be offline. " . localized ( )
static let hotKeyPathIdentifier = " values.globalPing "
}
class TableHeaderViewCell : NSTableHeaderCell {
var backgroundColour : NSColor = NSColor . black {
didSet {
backgroundColor = backgroundColour
}
}
override init ( textCell : String ) {
super . init ( textCell : textCell )
let attributedParagraphStyle = NSMutableParagraphStyle ( )
attributedParagraphStyle . alignment = . left
attributedStringValue = NSAttributedString ( string : textCell ,
attributes : [ . foregroundColor : Themer . shared ( ) . mainTextColor ( ) ,
. font : NSFont ( name : " Avenir " , size : 14 ) ! ,
. paragraphStyle : attributedParagraphStyle ] )
backgroundColor = Themer . shared ( ) . textBackgroundColor ( )
}
required init ( coder _ : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
override func draw ( withFrame cellFrame : NSRect , in controlView : NSView ) {
super . draw ( withFrame : cellFrame , in : controlView )
if ! controlView . isHidden {
backgroundColor ? . setFill ( )
cellFrame . fill ( )
drawInterior ( withFrame : cellFrame , in : controlView )
}
}
override func drawInterior ( withFrame cellFrame : NSRect , in controlView : NSView ) {
if ! controlView . isHidden {
if let avenirFont = NSFont ( name : " Avenir " , size : 14 ) {
font = avenirFont
}
textColor = NSColor . white
let rect = titleRect ( forBounds : cellFrame )
attributedStringValue . draw ( in : rect )
}
}
}
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 dataTask : URLSessionDataTask ? = . none
private lazy var notimezoneView : NoTimezoneView ? = {
NoTimezoneView ( frame : tableview . frame )
} ( )
private var geocodingKey : String = {
guard let path = Bundle . main . path ( forResource : " Keys " , ofType : " plist " ) ,
let dictionary = NSDictionary ( contentsOfFile : path ) ,
let apiKey = dictionary [ " GeocodingKey " ] as ? String
else {
assertionFailure ( " Unable to find the API key " )
return " "
}
return apiKey
} ( )
// S o r t i n g
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 progressIndicator : NSProgressIndicator !
@IBOutlet private var addButton : NSButton !
@IBOutlet private var recorderControl : SRRecorderControl !
@IBOutlet private var closeButton : NSButton !
@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 tableview : NSView !
@IBOutlet private var additionalSortOptions : NSView !
@IBOutlet var startAtLoginLabel : NSTextField !
@IBOutlet var startupCheckbox : NSButton !
private var themeDidChangeNotification : NSObjectProtocol ?
// S e l e c t e d T i m e z o n e s D a t a S o u r c e
private var selectionsDataSource : PreferencesDataSource !
// S e a r c h R e s u l t s D a t a S o u r c e H a n d l e r
private var searchResultsDataSource : SearchDataSource !
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 "
selectionsDataSource = PreferencesDataSource ( callbackDelegate : self )
timezoneTableView . dataSource = selectionsDataSource
timezoneTableView . delegate = selectionsDataSource
searchResultsDataSource = SearchDataSource ( with : searchField )
availableTimezoneTableView . dataSource = searchResultsDataSource
availableTimezoneTableView . delegate = searchResultsDataSource
}
deinit {
// W e s t i l l n e e d t o r e m o v e o b s e r v e r s s e t u s i n g N o t i f i c a t i o n C e n t e r b l o c k : A P I s
if let themeDidChangeNotif = themeDidChangeNotification {
NotificationCenter . default . removeObserver ( themeDidChangeNotif )
}
}
private func darkModeChanges ( ) {
if #available ( macOS 10.14 , * ) {
addTimezoneButton . image = Themer . shared ( ) . addImage ( )
deleteButton . image = Themer . shared ( ) . removeImage ( )
}
}
private func setupLocalizedText ( ) {
startAtLoginLabel . stringValue = NSLocalizedString ( " Start at Login " ,
comment : " Start at Login " )
timezoneSortButton . title = NSLocalizedString ( " Sort by Time Difference " ,
comment : " Start at Login " )
timezoneNameSortButton . title = NSLocalizedString ( " Sort by Name " ,
comment : " Start at Login " )
labelSortButton . title = NSLocalizedString ( " Sort by Label " ,
comment : " Start at Login " )
addButton . title = NSLocalizedString ( " Add Button Title " ,
comment : " Button to add a location " )
closeButton . title = NSLocalizedString ( " Close Button Title " ,
comment : " Button to close the panel " )
}
@objc func refreshTimezoneTableView ( _ shouldSelectNewlyInsertedTimezone : Bool = false ) {
OperationQueue . main . addOperation {
self . build ( shouldSelectNewlyInsertedTimezone )
}
}
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 ( _ shouldSelectLastRow : Bool = false ) {
if DataStore . shared ( ) . timezones ( ) = = [ ] {
housekeeping ( )
return
}
if selectedTimeZones . isEmpty = = false {
additionalSortOptions . isHidden = false
if tableview . subviews . count > 1 , let zeroView = notimezoneView , tableview . subviews . contains ( zeroView ) {
zeroView . removeFromSuperview ( )
timezoneTableView . enclosingScrollView ? . isHidden = false
}
timezoneTableView . reloadData ( )
if shouldSelectLastRow {
selectNewlyInsertedTimezone ( )
}
} else {
housekeeping ( )
}
cleanup ( )
}
private func housekeeping ( ) {
timezoneTableView . enclosingScrollView ? . isHidden = true
showNoTimezoneState ( )
cleanup ( )
}
private func cleanup ( ) {
updateMenubarTitles ( ) // U p d a t e t h e m e n u b a r t i t l e s , t h e c u s t o m l a b e l s m i g h t h a v e c h a n g e d .
}
private func updateMenubarTitles ( ) {
if let appDelegate = NSApplication . shared . delegate as ? AppDelegate {
appDelegate . setupMenubarTimer ( )
}
}
private func setup ( ) {
setupAccessibilityIdentifiers ( )
deleteButton . isEnabled = false
[ placeholderLabel ] . 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 ( )
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 ) ! ,
] )
}
timezoneTableView . backgroundColor = Themer . shared ( ) . mainBackgroundColor ( )
availableTimezoneTableView . backgroundColor = Themer . shared ( ) . textBackgroundColor ( )
timezonePanel . backgroundColor = Themer . shared ( ) . textBackgroundColor ( )
timezonePanel . contentView ? . wantsLayer = true
timezonePanel . contentView ? . layer ? . backgroundColor = Themer . shared ( ) . textBackgroundColor ( ) . cgColor
addTimezoneButton . image = themer . addImage ( )
deleteButton . image = themer . removeImage ( )
}
private func setupShortcutObserver ( ) {
let defaults = NSUserDefaultsController . shared
recorderControl . setAccessibility ( " ShortcutControl " )
recorderControl . bind ( NSBindingName . value ,
to : defaults ,
withKeyPath : PreferencesConstants . hotKeyPathIdentifier ,
options : nil )
recorderControl . delegate = self
}
override func observeValue ( forKeyPath keyPath : String ? , of object : Any ? , change _ : [ NSKeyValueChangeKey : Any ] ? , context _ : UnsafeMutableRawPointer ? ) {
if let path = keyPath , path = = PreferencesConstants . hotKeyPathIdentifier {
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 ( 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 " )
}
additionalSortOptions . isHidden = true
}
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 {
private func _markAsFavorite ( _ dataObject : TimezoneData ) {
if dataObject . customLabel != nil {
Logger . log ( object : [ " label " : dataObject . customLabel ? ? " Error " ] , for : " favouriteSelected " )
}
if let appDelegate = NSApplication . shared . delegate as ? AppDelegate {
appDelegate . setupMenubarTimer ( )
}
if let menubarTimezones = DataStore . shared ( ) . menubarTimezones ( ) , menubarTimezones . count > 1 {
showAlertIfMoreThanOneTimezoneHasBeenAddedToTheMenubar ( )
}
}
private func _unfavourite ( _ dataObject : TimezoneData ) {
Logger . log ( object : [ " label " : dataObject . customLabel ? ? " Error " ] ,
for : " favouriteRemoved " )
if let appDelegate = NSApplication . shared . delegate as ? AppDelegate ,
let menubarFavourites = DataStore . shared ( ) . menubarTimezones ( ) ,
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 )
// I f w e h a v e s e e n d i s p l a y e d t h e m e s s a g e b e f o r e , a b o r t !
let haveWeSeenThisMessageBefore = UserDefaults . standard . bool ( forKey : CLLongStatusBarWarningMessage )
if haveWeSeenThisMessageBefore , ! isUITestRunning {
return
}
// I f t h e u s e r i s a l r e a d y u s i n g t h e c o m p a c t m o d e , a b o r t .
if DataStore . shared ( ) . shouldDisplay ( . menubarCompactMode ) , ! isUITestRunning {
return
}
// T i m e t o d i s p l a y t h e a l e r t .
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
self . isActivityInProgress = true
self . placeholderLabel . placeholderString = " Searching for \( searchString ) "
Logger . info ( self . placeholderLabel . placeholderString ? ? " " )
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 . findLocalSearchResultsForTimezones ( )
if self . searchResultsDataSource . timezoneFilteredArray . isEmpty {
self . presentError ( errorPresent . localizedDescription )
return
}
self . prepareUIForPresentingResults ( )
return
}
guard let data = response , let searchResults = data . decode ( ) else {
assertionFailure ( " Data was unexpectedly nil " )
return
}
// l e t s e a r c h R e s u l t s = d a t a . d e c o d e ( )
if searchResults . status = = ResultStatus . zeroResults {
self . findLocalSearchResultsForTimezones ( )
self . placeholderLabel . placeholderString = self . searchResultsDataSource . timezoneFilteredArray . isEmpty ? " No results! 😔 Try entering the exact name. " : CLEmptyString
self . reloadSearchResults ( )
self . isActivityInProgress = false
return
} else if searchResults . status = = ResultStatus . requestDenied && searchResults . results . isEmpty {
self . findLocalSearchResultsForTimezones ( )
self . placeholderLabel . placeholderString = self . searchResultsDataSource . timezoneFilteredArray . isEmpty ? " Update Clocker to get a faster experience 😃 " : CLEmptyString
self . reloadSearchResults ( )
self . isActivityInProgress = false
return
}
self . appendResultsToFilteredArray ( searchResults . results )
self . findLocalSearchResultsForTimezones ( )
self . prepareUIForPresentingResults ( )
}
} )
}
}
private func findLocalSearchResultsForTimezones ( ) {
let lowercasedSearchString = searchField . stringValue . lowercased ( )
searchResultsDataSource . searchTimezones ( lowercasedSearchString )
}
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= \( geocodingKey ) &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 ] ) {
var finalResults : [ TimezoneData ] = [ ]
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 ]
finalResults . append ( TimezoneData ( with : totalPackage ) )
}
searchResultsDataSource . setFilteredArrayValue ( finalResults )
}
private func prepareUIForPresentingResults ( ) {
placeholderLabel . placeholderString = CLEmptyString
isActivityInProgress = false
reloadSearchResults ( )
}
private func reloadSearchResults ( ) {
if searchResultsDataSource . calculateChangesets ( ) {
Logger . info ( " Reloading Search Results " )
availableTimezoneTableView . reloadData ( )
}
}
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
}
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= \( geocodingKey ) "
NetworkManager . task ( with : urlString ) { [ weak self ] response , error in
guard let strongSelf = self else { return }
OperationQueue . main . addOperation {
if strongSelf . handleEdgeCase ( for : response ) = = true {
strongSelf . reloadSearchResults ( )
return
}
if error = = nil , let json = response , let timezone = json . decodeTimezone ( ) {
if strongSelf . availableTimezoneTableView . selectedRow >= 0 {
strongSelf . installTimezone ( timezone )
}
strongSelf . updateViewState ( )
} else {
OperationQueue . main . addOperation {
if error ? . localizedDescription = = " The Internet connection appears to be offline. " {
strongSelf . placeholderLabel . placeholderString = PreferencesConstants . noInternetConnectivityError
} else {
strongSelf . placeholderLabel . placeholderString = PreferencesConstants . tryAgainMessage
}
strongSelf . isActivityInProgress = false
}
}
}
}
}
private func installTimezone ( _ timezone : Timezone ) {
guard let dataObject = searchResultsDataSource . retrieveFilteredResultFromGoogleAPI ( availableTimezoneTableView . selectedRow ) 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 ]
// M a r k i f t h e t i m e z o n e i s s a m e a s l o c a l t i m e z o n e
let timezoneObject = TimezoneData ( with : newTimeZone )
timezoneObject . isSystemTimezone = timezoneObject . timezone ( ) = = NSTimeZone . system . identifier
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
searchResultsDataSource . cleanupFilterArray ( )
reloadSearchResults ( )
}
// / R e t u r n s t r u e i f t h e r e ' s a n e r r o r .
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 = = ResultStatus . zeroResults {
setErrorPlaceholders ( )
return true
}
return false
}
private func setErrorPlaceholders ( ) {
placeholderLabel . placeholderString = " No timezone found! Try entering an exact name. "
searchField . placeholderString = NSLocalizedString ( " Search Field Placeholder " ,
comment : " Search Field Placeholder " )
isActivityInProgress = false
}
private func updateViewState ( ) {
searchResultsDataSource . cleanupFilterArray ( )
reloadSearchResults ( )
refreshTimezoneTableView ( true )
refreshMainTable ( )
timezonePanel . close ( )
placeholderLabel . placeholderString = CLEmptyString
searchField . placeholderString = NSLocalizedString ( " Search Field Placeholder " ,
comment : " Search Field Placeholder " )
availableTimezoneTableView . isHidden = false
isActivityInProgress = false
}
@IBAction func addTimeZone ( _ : NSButton ) {
searchResultsDataSource . cleanupFilterArray ( )
view . window ? . beginSheet ( timezonePanel ,
completionHandler : nil )
}
@IBAction func addToFavorites ( _ : NSButton ) {
isActivityInProgress = true
if availableTimezoneTableView . selectedRow = = - 1 {
timezonePanel . contentView ? . makeToast ( PreferencesConstants . noTimezoneSelectedErrorMessage )
isActivityInProgress = false
return
}
if selectedTimeZones . count >= 100 {
timezonePanel . contentView ? . makeToast ( PreferencesConstants . maxTimezonesErrorMessage )
isActivityInProgress = false
return
}
if searchField . stringValue . isEmpty {
addTimezoneIfSearchStringIsEmpty ( )
} else {
addTimezoneIfSearchStringIsNotEmpty ( )
}
}
private func addTimezoneIfSearchStringIsEmpty ( ) {
let currentRowType = searchResultsDataSource . placeForRow ( availableTimezoneTableView . selectedRow )
switch currentRowType {
case . timezone :
cleanupAfterInstallingTimezone ( )
default :
return
}
}
private func addTimezoneIfSearchStringIsNotEmpty ( ) {
let currentRowType = searchResultsDataSource . placeForRow ( availableTimezoneTableView . selectedRow )
switch currentRowType {
case . timezone :
cleanupAfterInstallingTimezone ( )
return
case . city :
cleanupAfterInstallingCity ( )
}
}
private func cleanupAfterInstallingCity ( ) {
guard let dataObject = searchResultsDataSource . retrieveFilteredResultFromGoogleAPI ( availableTimezoneTableView . selectedRow ) 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 )
let currentSelection = searchResultsDataSource . retrieveSelectedTimezone ( availableTimezoneTableView . selectedRow )
let metaInfo = metadata ( for : currentSelection )
data . timezoneID = metaInfo . 0. name
data . formattedAddress = metaInfo . 1. formattedName
data . selectionType = . timezone
data . isSystemTimezone = metaInfo . 0. name = = NSTimeZone . system . identifier
let operationObject = TimezoneDataOperations ( with : data )
operationObject . saveObject ( )
searchResultsDataSource . cleanupFilterArray ( )
searchResultsDataSource . timezoneFilteredArray = [ ]
placeholderLabel . placeholderString = CLEmptyString
searchField . stringValue = CLEmptyString
reloadSearchResults ( )
refreshTimezoneTableView ( true )
refreshMainTable ( )
timezonePanel . close ( )
searchField . placeholderString = NSLocalizedString ( " Search Field Placeholder " ,
comment : " Search Field Placeholder " )
availableTimezoneTableView . isHidden = false
isActivityInProgress = false
}
private func selectNewlyInsertedTimezone ( ) {
// L e t ' s h i g h l i g h t t h e n e w l y a d d e d r o w . I f t h e n u m b e r o f t i m e z o n e s i s g r e a t e r t h a n 6 , t h e n e w l y a d d e d t i m e z o n e i s n ' t v i s i b l e . S i n c e w e h i d e t h e s c r o l l b a r s a s w e l l , t h e u s e r m i g h t g e t t h e i m p r e s s i o n t h a t s o m e t h i n g i s b r o k e n !
if timezoneTableView . numberOfRows > 6 {
timezoneTableView . scrollRowToVisible ( timezoneTableView . numberOfRows - 1 )
}
let indexSet = IndexSet ( integer : IndexSet . Element ( timezoneTableView . numberOfRows - 1 ) )
timezoneTableView . selectRowIndexes ( indexSet , byExtendingSelection : false )
}
private func metadata ( for selection : TimezoneMetadata ) -> ( NSTimeZone , TimezoneMetadata ) {
if selection . formattedName = = " Anywhere on Earth " {
return ( NSTimeZone ( name : " GMT-1200 " ) ! , selection )
} else if selection . formattedName = = " UTC " {
return ( NSTimeZone ( name : " GMT " ) ! , selection )
} else {
return ( selection . timezone , selection )
}
}
@IBAction func closePanel ( _ : NSButton ) {
searchResultsDataSource . cleanupFilterArray ( )
searchResultsDataSource . timezoneFilteredArray = [ ]
searchField . stringValue = CLEmptyString
placeholderLabel . placeholderString = CLEmptyString
searchField . placeholderString = NSLocalizedString ( " Search Field Placeholder " ,
comment : " Search Field Placeholder " )
reloadSearchResults ( )
timezonePanel . close ( )
isActivityInProgress = false
addTimezoneButton . state = . off
// T h e t a b l e m i g h t b e h i d d e n b e c a u s e o f a n e a r l y e x i t e s p e c i a l l y
// i f w e a r e n o t a b l e t o f e t c h a n a s s o c i a t e d t i m e z o n e
// F o r e g . E u r o p e d o e s n ' t h a v e a n a s s o c i a t e d t i m e z o n e
availableTimezoneTableView . isHidden = false
}
@IBAction func removeFromFavourites ( _ : NSButton ) {
// I f t h e u s e r i s e d i t i n g a r o w , a n d d e c i d e s t o d e l e t e t h e r o w t h e n w e h a v e a c r a s h
if timezoneTableView . editedRow != - 1 || timezoneTableView . editedColumn != - 1 {
return
}
if timezoneTableView . selectedRow = = - 1 , selectedTimeZones . count <= timezoneTableView . selectedRow {
assertionFailure ( " Data was unexpectedly nil " )
return
}
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 ( )
updateStatusBarAppearance ( )
updateStatusItem ( )
}
// TODO: T h i s p r o b a b l y d o e s n o t n e e d t o b e u s e d
private func updateStatusItem ( ) {
guard let statusItem = ( NSApplication . shared . delegate as ? AppDelegate ) ? . statusItemForPanel ( ) else {
return
}
statusItem . refresh ( )
}
private func updateStatusBarAppearance ( ) {
guard let statusItem = ( NSApplication . shared . delegate as ? AppDelegate ) ? . statusItemForPanel ( ) else {
return
}
statusItem . setupStatusItem ( )
}
@IBAction func filterArray ( _ : Any ? ) {
searchResultsDataSource . cleanupFilterArray ( )
if searchField . stringValue . count > 50 {
isActivityInProgress = false
reloadSearchResults ( )
timezonePanel . contentView ? . makeToast ( PreferencesConstants . maxCharactersAllowed )
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 )
}
}
// S o r t i n g
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 . timezone ( ) )
let timezone2 = NSTimeZone ( name : object2 . timezone ( ) )
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 { }
// H e l p e r s
extension PreferencesViewController {
private func insert ( timezone : TimezoneData , at index : Int ) {
let encodedObject = NSKeyedArchiver . archivedData ( withRootObject : timezone )
var newDefaults = selectedTimeZones
newDefaults [ index ] = encodedObject
DataStore . shared ( ) . setTimezones ( newDefaults )
}
}
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 ( )
}
}