Browse Source

One step closer to unified search.

pull/92/head
Abhishek 6 years ago
parent
commit
e095ad0d7a
  1. 4
      Clocker/AppDelegate.swift
  2. 20
      Clocker/ClockerUITests/AboutUsTests.swift
  3. 27
      Clocker/ClockerUITests/PreferencesTest.swift
  4. 3
      Clocker/Panel/Data Layer/TimezoneData.swift
  5. 1
      Clocker/Preferences/General/PreferencesDataSource.swift
  6. 230
      Clocker/Preferences/General/PreferencesViewController.swift

4
Clocker/AppDelegate.swift

@ -47,7 +47,7 @@ open class AppDelegate: NSObject, NSApplicationDelegate {
AppDefaults.initialize()
// Check if we can show the onboarding flow!
showOnboardingFlow()
showOnboardingFlowIfEligible()
// Ratings Controller initialization
RateController.applicationDidLaunch(UserDefaults.standard)
@ -92,7 +92,7 @@ open class AppDelegate: NSObject, NSApplicationDelegate {
return onboardingStoryboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("onboardingFlow")) as? OnboardingController
}()
private func showOnboardingFlow() {
private func showOnboardingFlowIfEligible() {
let shouldLaunchOnboarding = (DataStore.shared().retrieve(key: CLShowOnboardingFlow) == nil && DataStore.shared().timezones().isEmpty)
|| ProcessInfo.processInfo.arguments.contains(CLOnboaringTestsLaunchArgument)

20
Clocker/ClockerUITests/AboutUsTests.swift

@ -109,7 +109,7 @@ extension XCTestCase {
func addAPlace(place: String, to app: XCUIApplication, shouldSleep: Bool = true) {
// Let's first check if the place is already present in the list
let matchPredicate = NSPredicate(format: "value == %@", place)
let matchPredicate = NSPredicate(format: "value contains %@", place)
let matchingFields = app.windows["Clocker"].tables["TimezoneTableView"].textFields.matching(matchPredicate)
if matchingFields.count > 0 {
return
@ -122,15 +122,22 @@ extension XCTestCase {
let searchField = app.searchFields["AvailableSearchField"]
searchField.reset(text: place)
let firstResult = app.tables["AvailableTimezoneTableView"].tableRows.firstMatch
let results = app.tables["AvailableTimezoneTableView"].cells.staticTexts.matching(matchPredicate)
let waiter = XCTWaiter()
let isHittable = NSPredicate(format: "exists == true", "")
let addExpectation = expectation(for: isHittable,
evaluatedWith: firstResult,
handler: nil)
evaluatedWith: results.firstMatch) { () -> Bool in
print("Handler called")
return true
}
waiter.wait(for: [addExpectation], timeout: 5)
app.tables["AvailableTimezoneTableView"].click()
if results.count > 0 {
results.firstMatch.click()
}
app.buttons["AddAvailableTimezone"].click()
if shouldSleep {
@ -154,7 +161,8 @@ extension XCTestCase {
}
func deleteAPlace(place: String, for app: XCUIApplication, shouldSleep: Bool = true) {
let row = app.windows["Clocker"].textFields[place].firstMatch
let matchPredicate = NSPredicate(format: "value == %@", place)
let row = app.tables["TimezoneTableView"].textFields.matching(matchPredicate).firstMatch
row.click()
row.typeKey(XCUIKeyboardKey.delete, modifierFlags: XCUIElement.KeyModifierFlags())
if shouldSleep {

27
Clocker/ClockerUITests/PreferencesTest.swift

@ -43,12 +43,10 @@ class PreferencesTest: XCTestCase {
app.windows["Clocker"].checkBoxes["AddTimezone"].click()
}
app.sheets.radioGroups.radioButtons["Search by Timezone(s)"].click()
addAPlace(place: "UTC", to: app)
let matchPredicate = NSPredicate(format: "value == %@", "UTC")
let matchingFields = app.windows["Clocker"].textFields.matching(matchPredicate)
let matchingFields = app.tables["TimezoneTableView"].textFields.matching(matchPredicate)
XCTAssertTrue(matchingFields.count > 0, "Matching Fields count was zero")
deleteAPlace(place: "UTC", for: app)
@ -225,7 +223,7 @@ class PreferencesTest: XCTestCase {
let searchField = app.searchFields["AvailableSearchField"]
searchField.reset(text: "StuJjlqh7AcJFnBuOdgNa2dQ4WrIajP9Mo8R83FV7fIZ3B8zE2n")
sleep(1)
sleep(2)
let maxCharacterCountPredicate = NSPredicate(format: "value like %@", "Only 50 characters allowed!")
let currentSheets = app.sheets.firstMatch.staticTexts
@ -253,27 +251,6 @@ class PreferencesTest: XCTestCase {
deleteAPlace(place: "Cambodia", for: app, shouldSleep: false)
}
func testPlaceholderStrings() {
app.tapMenubarIcon()
app.tables["mainTableView"].typeKey(",", modifierFlags: .command)
if app.sheets.count == 0 {
app.windows["Clocker"].checkBoxes["AddTimezone"].click()
}
app.sheets.radioGroups.radioButtons["Search by Timezone(s)"].click()
let expectedPlaceholder = "Enter a timezone name"
let currentPlaceholder = app.sheets.searchFields["AvailableSearchField"]
XCTAssertTrue(currentPlaceholder.exists, "Search Field doesn't exist")
XCTAssertEqual(currentPlaceholder.placeholderValue!, expectedPlaceholder)
let newPlaceholderValue = "Enter a city, state or country name"
app.sheets.radioGroups.radioButtons["Search By City"].click()
let newPlaceholder = app.sheets.searchFields["AvailableSearchField"]
XCTAssertTrue(newPlaceholder.exists, "Search Field doesn't exist")
XCTAssertEqual(newPlaceholder.placeholderValue!, newPlaceholderValue)
}
func testNoTimezone() {
app.tapMenubarIcon()
app.buttons["Preferences"].click()

3
Clocker/Panel/Data Layer/TimezoneData.swift

@ -218,8 +218,6 @@ class TimezoneData: NSObject, NSCoding {
// Now point it to new models
DataStore.shared().setTimezones(newModels)
print("Successfully converted: \(newModels.count) timezones")
}
}
@ -246,6 +244,7 @@ class TimezoneData: NSObject, NSCoding {
let old = NSKeyedUnarchiver.unarchiveObject(with: timezone)
if let oldModel = old as? CLTimezoneData {
// Convert it to new model and add it
print("We're still using old Objective-C models");
let newTimezone = TimezoneData(with: oldModel)
newModels.append(newTimezone)
} else if let newModel = old as? TimezoneData {

1
Clocker/Preferences/General/PreferencesDataSource.swift

@ -3,7 +3,6 @@
import Cocoa
protocol PreferenceSelectionUpdates: AnyObject {
func didAddTimezone(_ timezone: TimezoneData)
func markAsFavorite(_ dataObject: TimezoneData)
func unfavourite(_ dataObject: TimezoneData)
func refreshTimezoneTable()

230
Clocker/Preferences/General/PreferencesViewController.swift

@ -14,6 +14,18 @@ struct PreferencesConstants {
static let offlineErrorMessage = "The Internet connection appears to be offline."
}
enum RowType {
case timezoneHeader
case cityHeader
case city
case timezone
}
struct TimezoneMetadata {
let timezone: Timezone
let tages: Set<String> = Set()
}
class PreferencesViewController: ParentViewController {
private var isActivityInProgress = false {
didSet {
@ -33,7 +45,6 @@ class PreferencesViewController: ParentViewController {
private var filteredArray: [Any] = []
private var timezoneArray: [String] = []
private var timezoneFilteredArray: [String] = []
private var columnName = "Place(s)"
private var dataTask: URLSessionDataTask? = .none
private lazy var notimezoneView: NoTimezoneView? = {
@ -75,8 +86,8 @@ class PreferencesViewController: ParentViewController {
@IBOutlet var sortToggle: NSButton!
private var themeDidChangeNotification: NSObjectProtocol?
private var selectionsDataSource: PreferencesDataSource!
private var finalArray: [RowType] = []
override func viewDidLoad() {
super.viewDidLoad()
@ -90,8 +101,6 @@ class PreferencesViewController: ParentViewController {
setup()
availableTimezoneTableView.reloadData()
setupShortcutObserver()
darkModeChanges()
@ -106,9 +115,37 @@ class PreferencesViewController: ParentViewController {
selectionsDataSource = PreferencesDataSource(callbackDelegate: self)
timezoneTableView.dataSource = selectionsDataSource
timezoneTableView.delegate = selectionsDataSource
reloadSearchResults()
}
private func calculateArray() {
finalArray = []
func addTimezonesIfNeeded(_ data: [String]) {
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)
}
@ -223,12 +260,6 @@ class PreferencesViewController: ParentViewController {
[placeholderLabel, additionalSortOptions].forEach { $0.isHidden = true }
if timezoneArray.isEmpty {
timezoneArray.append("UTC")
timezoneArray.append("Anywhere on Earth")
timezoneArray.append(contentsOf: NSTimeZone.knownTimeZoneNames)
}
messageLabel.stringValue = CLEmptyString
timezoneTableView.registerForDraggedTypes([.dragSession])
@ -326,59 +357,71 @@ extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate
return numberOfSearchResults()
}
func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row _: Int) -> NSView? {
if let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "resultCell"), owner: self) as? SearchResultTableViewCell {
message.sourceName.stringValue = "Nicaragua"
return message
func tableView(_ tableView: NSTableView, isGroupRow row: Int) -> Bool {
let currentRowType = finalArray[row]
return
currentRowType == .timezoneHeader ||
currentRowType == .cityHeader
}
return nil
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
print("Should Select Row")
let currentRowType = finalArray[row]
return !(currentRowType == .timezoneHeader || currentRowType == .cityHeader)
}
func tableView(_: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
var dataSource: TimezoneData?
if filteredArray.count > row, let currentFilteredObject = filteredArray[row] as? TimezoneData {
dataSource = currentFilteredObject
func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet {
// print("Selection Indexes for Proposed Selection: \(proposedSelectionIndexes.first!)")
return proposedSelectionIndexes
}
if tableColumn?.identifier.rawValue == PreferencesConstants.availableTimezoneIdentifier {
if filteredArray.isEmpty {
return timezoneArray[row]
}
func tableView(_ tableView: NSTableView, viewFor _: NSTableColumn?, row: Int) -> NSView? {
let currentRowType = finalArray[row]
return dataSource != nil ?
handleAvailableTimezoneColumn(for: row, dataSource) :
filteredArray[row] as? String
switch currentRowType {
case .timezoneHeader, .cityHeader:
return headerCell(tableView, currentRowType)
case .timezone:
return timezoneCell(tableView, currentRowType, row)
case .city:
return cityCell(tableView, currentRowType, row)
}
if tableColumn?.identifier.rawValue == "abbreviation" {
return handleAbbreviationColumn(for: row)
}
private func timezoneCell(_ tableView: NSTableView, _ rowType: 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
}
private func handleAvailableTimezoneColumn(for row: Int, _ dataSource: TimezoneData?) -> Any? {
if row < filteredArray.count {
return dataSource?.formattedAddress
let index = searchField.stringValue.isEmpty ? row - 1 : row
message.sourceName.stringValue = datasource[index % datasource.count]
return message
}
return nil
}
private func handleAbbreviationColumn(for row: Int) -> Any? {
if timezoneArray.count > row {
// Special return for manually inserted 'UTC'
if timezoneArray[row] == "UTC" {
return "UTC"
private func cityCell(_ tableView: NSTableView, _ rowType: 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
}
if timezoneArray[row] == "Anywhere on Earth" {
return "AoE"
return nil
}
return NSTimeZone(name: timezoneArray[row])?.abbreviation ?? "Error"
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
}
@ -534,6 +577,8 @@ extension PreferencesViewController {
if searchResults?.status == "ZERO_RESULTS" {
self.placeholderLabel.placeholderString = "No results! 😔 Try entering the exact name."
self.findLocalSearchResultsForTimezones()
self.reloadSearchResults()
self.isActivityInProgress = false
return
}
@ -551,7 +596,6 @@ extension PreferencesViewController {
timezoneFilteredArray = []
let lowercasedSearchString = searchField.stringValue.lowercased()
timezoneFilteredArray = timezoneArray.filter { $0.lowercased().contains(lowercasedSearchString) }
filteredArray.append(contentsOf: timezoneFilteredArray)
}
private func generateSearchURL() -> String {
@ -598,6 +642,12 @@ extension PreferencesViewController {
private func prepareUIForPresentingResults() {
placeholderLabel.placeholderString = CLEmptyString
isActivityInProgress = false
reloadSearchResults()
}
private func reloadSearchResults() {
print("Reloading Search Results")
calculateArray()
availableTimezoneTableView.reloadData()
}
@ -658,11 +708,12 @@ extension PreferencesViewController {
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.availableTimezoneTableView.selectedRow < self.filteredArray.count {
if self.availableTimezoneTableView.selectedRow >= 0 {
self.installTimezone(timezone)
}
self.updateViewState()
@ -682,7 +733,7 @@ extension PreferencesViewController {
}
private func installTimezone(_ timezone: Timezone) {
guard let dataObject = self.filteredArray[self.availableTimezoneTableView.selectedRow] as? TimezoneData else {
guard let dataObject = self.filteredArray[self.availableTimezoneTableView.selectedRow % filteredArray.count] as? TimezoneData else {
assertionFailure("Data was unexpectedly nil")
return
}
@ -720,7 +771,7 @@ extension PreferencesViewController {
placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError
isActivityInProgress = false
filteredArray = []
availableTimezoneTableView.reloadData()
reloadSearchResults()
}
/// Returns true if there's an error.
@ -745,7 +796,7 @@ extension PreferencesViewController {
private func updateViewState() {
filteredArray = []
availableTimezoneTableView.reloadData()
reloadSearchResults()
refreshTimezoneTableView()
refreshMainTable()
timezonePanel.close()
@ -798,12 +849,43 @@ extension PreferencesViewController {
return
}
if let dataObject = filteredArray[availableTimezoneTableView.selectedRow] as? String, dataObject != nil {
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
}
}
guard let dataObject = filteredArray[availableTimezoneTableView.selectedRow] as? TimezoneData else {
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
}
@ -825,23 +907,14 @@ extension PreferencesViewController {
data.setLabel(CLEmptyString)
if searchField.stringValue.isEmpty == false {
if filteredArray.count <= availableTimezoneTableView.selectedRow {
return
}
let currentSelection = filteredArray[availableTimezoneTableView.selectedRow]
guard let selection = currentSelection as? String else {
assertionFailure()
return
}
let currentSelection = timezoneFilteredArray[availableTimezoneTableView.selectedRow % timezoneFilteredArray.count]
let metaInfo = metadata(for: selection)
let metaInfo = metadata(for: currentSelection)
data.timezoneID = metaInfo.0
data.formattedAddress = metaInfo.1
} else {
let currentSelection = timezoneArray[availableTimezoneTableView.selectedRow]
let currentSelection = timezoneArray[availableTimezoneTableView.selectedRow - 1]
let metaInfo = metadata(for: currentSelection)
data.timezoneID = metaInfo.0
@ -856,7 +929,7 @@ extension PreferencesViewController {
filteredArray = []
timezoneFilteredArray = []
availableTimezoneTableView.reloadData()
reloadSearchResults()
refreshTimezoneTableView()
@ -887,21 +960,15 @@ extension PreferencesViewController {
@IBAction func closePanel(_: NSButton) {
filteredArray = []
columnName = "Place(s)"
availableTimezoneTableView.reloadData()
timezoneFilteredArray = []
searchField.stringValue = CLEmptyString
placeholderLabel.placeholderString = CLEmptyString
searchField.placeholderString = "Enter a city, state or country name"
timezonePanel.close()
reloadSearchResults()
timezonePanel.close()
isActivityInProgress = false
addTimezoneButton.state = .off
// The table might be hidden because of an early exit especially
@ -994,7 +1061,6 @@ extension PreferencesViewController {
@IBAction func filterTimezoneArray(_: Any?) {
let lowercasedSearchString = searchField.stringValue.lowercased()
timezoneFilteredArray = timezoneArray.filter { $0.lowercased().contains(lowercasedSearchString) }
availableTimezoneTableView.reloadData()
}
@IBAction func filterArray(_: Any?) {
@ -1005,6 +1071,7 @@ extension PreferencesViewController {
if searchField.stringValue.count > 50 {
isActivityInProgress = false
messageLabel.stringValue = PreferencesConstants.maxCharactersAllowed
reloadSearchResults()
Timer.scheduledTimer(withTimeInterval: 5,
repeats: false) { _ in
OperationQueue.main.addOperation {
@ -1022,7 +1089,7 @@ extension PreferencesViewController {
resetSearchView()
}
availableTimezoneTableView.reloadData()
reloadSearchResults()
}
}
@ -1079,7 +1146,9 @@ extension PreferencesViewController {
return isLabelOptionSelected ? object1.customLabel! > object2.customLabel! : object1.customLabel! < object2.customLabel!
}
sender.image = isLabelOptionSelected ? NSImage(named: NSImage.Name("NSDescendingSortIndicator"))! : NSImage(named: NSImage.Name("NSAscendingSortIndicator"))!
sender.image = isLabelOptionSelected ?
NSImage(named: NSImage.Name("NSDescendingSortIndicator"))! :
NSImage(named: NSImage.Name("NSAscendingSortIndicator"))!
isLabelOptionSelected.toggle()
@ -1111,9 +1180,7 @@ extension PreferencesViewController {
private func updateAfterSorting() {
let newDefaults = selectedTimeZones
DataStore.shared().setTimezones(newDefaults)
refreshTimezoneTableView()
refreshMainTable()
}
@ -1124,11 +1191,7 @@ extension PreferencesViewController: SRRecorderControlDelegate {}
// Helpers
extension PreferencesViewController {
private func numberOfSearchResults() -> Int {
if searchField.stringValue.isEmpty == false {
return filteredArray.count
}
return timezoneArray.count
return finalArray.count
}
private func insert(timezone: TimezoneData, at index: Int) {
@ -1143,8 +1206,11 @@ class SearchResultTableViewCell: NSTableCellView {
@IBOutlet var sourceName: NSTextField!
}
class HeaderTableViewCell: NSTableCellView {
@IBOutlet var headerField: NSTextField!
}
extension PreferencesViewController: PreferenceSelectionUpdates {
func didAddTimezone(_: TimezoneData) {}
func markAsFavorite(_ dataObject: TimezoneData) {
_markAsFavorite(dataObject)
}

Loading…
Cancel
Save