I am very new to Cocoa. However, I am willing and eager to solve this problem on my own. All I want to know is the “way” to solve it.
There are some countries (the number of countries will keep increasing).
Each country has some cities (the number of cities is not constant)
Each city has a river (again, (the number of rivers will keep increasing)
I want that if I select a country “A” in ComboBox1/PopUpButton1, then ComboBox2/PopUpButton2 must show a list of cities belonging to country “A” only and not other countries. Likewise, after a city is selected, only the rivers belonging to that city must be seen in ComboBox3/PopUpButton3.
Please, tell me the basics like how I must create my arrays, dictionaries and should I use ComboBox or PopUpButton?
I’m bound to say you’ve picked a fairly complex place to start. My suggestion would be to start off by making an array of countries and getting one combobox working how you want. Then move on to adding others. There’s really no one right way – it depends a bit on what you are doing with the data, and so on.
Thanks for the reply, Shane.
To simplify the problem, let us say I do not want to add new items and I shall use pop up buttons for countries and cities.
I have a country array and cities array containing country and city names respectively.
How do I make them talk to each other? If one country is selected in countryPopUpButton, how do I have only the cities belonging to that country available in the cityPopUpButton. Do I have to create a mess of arrays such that each country has its own city array?
(Would learning how to use “Relationships” in Core Data help with such kind of problem?)
I was under the impression AppleScriptObjC can’t use core data.
“a mess of arrays” also known as a two dimensional array is probably the way to go about it. Having to deal with two dimensional arrays is not as bad as it sounds.
I’m learning ObjC at the moment. This strikes me as a typical ObjC learning project where you’d create a river class, a city class and a country class.
The simplest method would be to have one array of countries, and a matching one containing arrays of the cities in each. Then choosing a menu item would trigger an action handler that found the selection, and changed the other menu accordingly.
That’s simple, but it might prove limiting later on. You might be better to use records/dictionaries instead of arrays.
That probably is the typical Objective-C approach, but depending on what you want to do with the result, it can sometimes just be a recipe for more work.
I’m not sure what it is you want to do with this project, but I think the ability to have the choice in one list control what you see in another list is a general problem of interest, so I looked at how to implement this in ASOC. I have gotten as far as a project that has to combo boxes for countries and cities. I am using bindings to populate the combo box lists and to get the value chosen or typed in. So, the Content Values of the countries combo box is bound to Combos App Delegate with the property, theCountries, as the Model Key Path. Likewise, the city combo box has the property, theCities as its Model Key Path. I also have the properties countryChoice and cityChoice bound to the Value of the combo boxes, so these properties will reflect what is in the combo box text field, whether that was populated by typing in a new value or gotten by choosing from the list. The state of the combo boxes is set to “continuous” and the “Uses external Data Source” check box is left unchecked. The methods, choseCountry_ and choseCity_ , are connected to the country and city combo boxes respectively. I’m not sure if there are easier ways to implement this, but this is what I have so far:
script CombosAppDelegate
property parent : class "NSObject"
property NSMutableDictionary : class "NSMutableDictionary"
property pathToRoot : POSIX path of ((path to documents folder as string) & "Geography:")
property theCountries : missing value
property theCities : missing value
property countryChoice : missing value
property cityChoice : missing value
property allCities : {}
on applicationWillFinishLaunching_(aNotification)
set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Geography.plist")
if allCities is equal to missing value then
set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
end if
set my theCountries to allCities's allKeys() as list
writeToFile()
end applicationWillFinishLaunching_
on choseCountry_(sender)
if theCountries does not contain countryChoice then
if countryChoice is not missing value then
set my theCountries to theCountries & countryChoice
allCities's setObject_forKey_({}, countryChoice) --add a new key with an empty list for the object
writeToFile()
end if
end if
set my theCities to allCities's objectForKey_(countryChoice) as list
end choseCountry_
on choseCity_(sender)
if theCities does not contain (cityChoice as text) then
if cityChoice is not missing value then
set my theCities to ((allCities's objectForKey_(countryChoice) as list) & cityChoice)
allCities's setObject_forKey_(theCities, countryChoice)
writeToFile()
end if
end if
end choseCity_
on writeToFile()
set myFile to pathToRoot & "Geography.plist"
if not allCities's writeToFile_atomically_(myFile, true) then
set messageText to "There was an error writing to file"
display dialog messageText buttons {"Ok"} default button 1
end if
end writeToFile
end script
Nice one, Ric. Allow me to take it a step further and add sorting of entries:
on applicationWillFinishLaunching_(aNotification)
set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Geography.plist")
if allCities is equal to missing value then
set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
end if
set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
writeToFile()
end applicationWillFinishLaunching_
on choseCountry_(sender)
if (theCountries's containsObject_(countryChoice)) as boolean is false then
if countryChoice is not missing value then
tell allCities to setObject_forKey_({}, countryChoice) --add a new entry with an empty list for the object
set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
writeToFile()
end if
end if
set my theCities to allCities's objectForKey_(countryChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
end choseCountry_
on choseCity_(sender)
if (theCities's containsObject_(cityChoice)) as boolean is false then
if cityChoice is not missing value then
set oldCities to allCities's objectForKey_(countryChoice) -- get existing list
set oldCities to oldCities's arrayByAddingObject_(cityChoice) -- add new city
set my theCities to oldCities's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:") -- sort
tell allCities to setObject_forKey_(theCities, countryChoice) -- replace existing entry
writeToFile()
end if
end if
end choseCity_
Thanks Shane for the extension of the project – I wanted to do the sorting, but I didn’t really know how to do that. I tried adding the sorting to my code but it didn’t work, I think because the way I implemented “theCountries” and “theCities” they were AS lists instead of NSArrays, so I adopted your code there. I also changed the order of the “if” statements in the choseCities_ method to fix an error that occurred if you clicked out of the countries combo box without choosing or typing anything.
One other error occurred if you had a city name showing in the combo box and then went back to the country combo box and changed the country – the city from the previously chosen country was added to the new country’s list of cities. I fixed that by adding the “set cityChoice to missing value” line in the choseCountry method. So, the code that seems to work well now is :
script CombosAppDelegate
property parent : class "NSObject"
property NSMutableDictionary : class "NSMutableDictionary"
property pathToRoot : POSIX path of ((path to documents folder as string) & "Geography:")
property theCountries : missing value
property theCities : missing value
property countryChoice : missing value
property cityChoice : missing value
property allCities : {}
on applicationWillFinishLaunching_(aNotification)
set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Country.plist")
if allCities is equal to missing value then
set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
end if
set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
end applicationWillFinishLaunching_
on choseCountry_(sender)
set cityChoice to missing value
if countryChoice is not missing value then
if (theCountries's containsObject_(countryChoice)) as boolean is false then
allCities's setObject_forKey_({}, countryChoice) --add a new entry with an empty list for the object
set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
end if
set my theCities to allCities's objectForKey_(countryChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
end if
end choseCountry_
on choseCity_(sender)
if cityChoice is not missing value then
if (theCities's containsObject_(cityChoice)) as boolean is false then
set oldCities to allCities's objectForKey_(countryChoice) -- get existing list
set oldCities to oldCities's arrayByAddingObject_(cityChoice) -- add new city
set my theCities to oldCities's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:") -- sort
allCities's setObject_forKey_(theCities, countryChoice) -- replace existing entry
end if
end if
end choseCity_
on applicationShouldTerminateAfterLastWindowClosed_(sender)
return 1
end applicationShouldTerminateAfterLastWindowClosed_
on applicationShouldTerminate_(sender)
set myFile to pathToRoot & "Country.plist"
if not allCities's writeToFile_atomically_(myFile, true) then
set messageText to "There was an error writing to Country.plist"
display dialog messageText buttons {"Ok"} default button 1
end if
return 1
end applicationShouldTerminate_
end script
I’m not sure either why that worked either – in a further extension of the project to include a Rivers combo box, I did get the expected behavior with a river name in the text field when I changed a city name, but the name wasn’t cleared when I changed a country name ( I had “set cityChoice to missing value” and “set riverChoice to missing value” in the choseCountry_ method). So, I put the “my” in those statements, and now it works properly.
I added another dictionary, allRivers, with all the cities in the initial allCities dictionary as the keys. I also added another combo box that is bound, like the other 2, to two properties, theRivers and riverChoice. If you stop the program by closing the window, applicationShouldTerminateAfterLastWindowClosed_(sender) will get called which will, in turn, call applicationShouldTerminate_(sender) – that will write out the updated dictionaries to the 2 plists (stopping the program by clicking the stop sign icon will not call these). I also incorporated Shane’s suggestions to sort the lists and to ensure correct capitalization of the entries.
script CombosAppDelegate
property parent : class "NSObject"
property NSMutableDictionary : class "NSMutableDictionary"
property pathToRoot : POSIX path of ((path to documents folder as string) & "Geography:")
property theCountries : missing value
property theCities : missing value
property theRivers : missing value
property countryChoice : missing value
property cityChoice : missing value
property riverChoice : missing value
property allCities : {}
property allRiverKeys : {}
property allRivers : {}
on applicationWillFinishLaunching_(aNotification)
set my allCities to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "Country.plist")
if allCities is equal to missing value then
set my allCities to NSMutableDictionary's dictionaryWithObjects_forKeys_({{"New York", "Los Angeles", "Chicago"}, {"Vancouver", "Ottawa", "Calgary"}}, {"United States", "Canada"})
end if
set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
set my allRivers to NSMutableDictionary's alloc()'s initWithContentsOfFile_(pathToRoot & "River.plist")
if allRivers is equal to missing value then
set allRivers to NSMutableDictionary's dictionary()
repeat with aKey in (allCities's allKeys() as list) --Extract all the city names into a list
set allRiverKeys to allRiverKeys & (allCities's objectForKey_(aKey) as list)
end repeat
repeat with aKey in allRiverKeys
allRivers's addObject_forKey_({}, aKey) --creates empty rivers dictionary entries for all cities in the initial allCities dictionary
end repeat
end if
end applicationWillFinishLaunching_
on choseCountry_(sender)
set my cityChoice to missing value
set my riverChoice to missing value
if countryChoice is not missing value then
set my countryChoice to countryChoice's capitalizedString()
if (theCountries's containsObject_(countryChoice)) as boolean is false then
allCities's setObject_forKey_({}, countryChoice) --add a new entry with an empty list for the object
set my theCountries to allCities's allKeys()'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
end if
set my theCities to allCities's objectForKey_(countryChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
end if
end choseCountry_
on choseCity_(sender)
set my riverChoice to missing value
if cityChoice is not missing value then
set my cityChoice to cityChoice's capitalizedString()
if (theCities's containsObject_(cityChoice)) as boolean is false then
set oldCities to allCities's objectForKey_(countryChoice) -- get existing list
set oldCities to oldCities's arrayByAddingObject_(cityChoice) -- add new city
set my theCities to oldCities's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:") -- sort
allCities's setObject_forKey_(theCities, countryChoice) -- replace existing entry
allRivers's addObject_forKey_({}, cityChoice)
end if
set my theRivers to allRivers's objectForKey_(cityChoice)'s sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
end if
end choseCity_
on choseRiver_(sender)
if riverChoice is not missing value then
set my riverChoice to riverChoice's capitalizedString()
if (theRivers's containsObject_(riverChoice)) as boolean is false then
set oldRivers to allRivers's objectForKey_(cityChoice)
set oldRivers to oldRivers's arrayByAddingObject_(riverChoice)
set my theRivers to oldRivers's sortedArrayUsingSelector_("localizedCaseInsensitiveCompare:")
allRivers's setObject_forKey_(theRivers, cityChoice)
end if
end if
end choseRiver_
on applicationShouldTerminateAfterLastWindowClosed_(sender)
return 1
end applicationShouldTerminateAfterLastWindowClosed_
on applicationShouldTerminate_(sender)
set myFile to pathToRoot & "Country.plist"
if not allCities's writeToFile_atomically_(myFile, true) then
set messageText to "There was an error writing to Country.plist"
display dialog messageText buttons {"Ok"} default button 1
end if
set myFile to pathToRoot & "River.plist"
if not allRivers's writeToFile_atomically_(myFile, true) then
set messageText to "There was an error writing to River.plist"
display dialog messageText buttons {"Ok"} default button 1
end if
return 1
end applicationShouldTerminate_
end script
Thanks again.
Regarding auto-completion, if I write “C”, I get “Canada” but if I write “c”, I don’t get anything. I thought Shane’s earlier post was to fix this problem. Now, I understand that it was to auto-capitalize the first letter so that “kenya” gets stored as “Kenya”.