Leopard Calendar Store

Well… Thanks Craig. I do work on a PPC. (Are you saying that XCode 3.1 is intel only?)

Actually I’m sort of onto something on my own here. But lacking enough of a grasp of Obj-C I’m rather gasping at straws here.

Still, what I succeeded in doing is this:

  1. make a custom class to implement Calendar Store methods, and define that object as an AS variable:

set WBCCalStore to (call method "newWBCcalStore" of class "WBCcalStore")

  1. the methods of this custom class can be called with call methods, and certain things can be done with the returned values. for example:

set these_events to call method "getEventsinRangewithStart:withEnd:" of WBCCalStore with parameters {sd, ed}

  1. I can get events within a range (as example above)

What I can’t do is:

  1. DO ANYTHING with those events. Simply getting the title of these returned event objects I can’t yet do. I haven’t been successful in writing the “gettitle:” method referred to below:

set anEvent to item 1 of these_events
-- following does not work
--set anEventTitle to title of anEvent
-- following cannot make work so for now return notice string
set anEventTitle to call method "gettitle:" of WBCCalStore with parameter anEvent

(What I’d ultimately like to do is return a list or a record of all properties of the event that can of course be accessed in applescript)

  1. Be certain I’m dealing with memory and retaining and releasing objects properly.

  2. Figure out how to create and retain just one instance of the CalCalendarStore rather than creating a new instance for each method of this custom class.

  3. Know that I’m adding the Calendar Store framework to the project properly (as a reference to the /System/ version) without actually copying it into my builds

  4. Know that I can in fact add a framework that would be used on Leopard if available but that the project build would still launch on Pather/Leopard (and work as long as these methods aren’t called). In fact if that won’t be the case it is sort of a show stopper for me right there.

The sample project so far is here (alpha 0.1):

http://www.woodenbrain.com/sw/betas/WBCCalendarStore.zip

You seem to making progress. The problem seems to be with what is returned from the message call:

set these_events to call method "getEventsinRangewithStart:withEnd:" of WBCCalStore with parameters {sd, ed}

I inserted a routine to write the these_events list to desktop:

set a to (path to desktop as text) & "iCalEvents1.txt"
		set aa to open for access a with write permission
		write these_events to aa as list
		close access aa

and then opened up the list in SE, and this is what you get:

{item id 12, item id 13, item id 14, item id 15, item id 16, item id 17, item id 18, item id 19, item id 20, item id 21, item id 22}

The big problem now is trying to use data in that list inside of an iCal tell block is pretty much impossible, so it precludes using an AS routine to analyze these_events. It would seem that you are going to be stuck with working out everything in Obj-C for this. I am pretty short on free time this week, but this is truly interesting, so I will see what I can come up with playing around with your stuff.

I am pretty sure that the beta XCode is only functional on Intel machines, but I am not positive about that. I am often wrong, but seldom in doubt…

OK, I had a few minutes and got some good stuff. Here is the new method in WBCcalStore.m:

-(NSArray *)getEventsinRangewithStart:(NSDate *)theStartdate withEnd:(NSDate *)theEnddate {
	NSMutableArray *eventInformation = [NSMutableArray arrayWithCapacity:10];
	CalCalendarStore *calStore = [CalCalendarStore defaultCalendarStore];
  NSPredicate *predicate = [CalCalendarStore eventPredicateWithStartDate:theStartdate endDate:theEnddate calendars:[calStore calendars]];
	NSEnumerator *eventEnumerator = [[calStore eventsWithPredicate:predicate] objectEnumerator];
	id event;
	while (event = [eventEnumerator nextObject]) {
		[eventInformation addObject:[event title]];
	}
	return eventInformation;
}

And here is the revised code from the .applescript file:

if ce > 0 then
			set anEvent to item 1 of these_events
			my show_output("title of item 1 of " & ce & ": " & anEvent)
		end if

I think this is close to what you are looking for.

Thanks Craig, that’s promising. How would I modify that enumeration loop so that instead of returning a list of title strings, it returned a list of lists of all properties of the event, ie :

event class [CalEvent]:
isAllDay property
location property
recurrenceRule property
startDate property
endDate property
attendees property
isDetached property
occurrence property

superclass [calCalendarItem]:

Getting and Setting Properties
calendar property
notes property
url property
title property
uid property
dateStamp property
alarms property
Setting Alarms
“ hasAlarm
“ nextAlarmDate

of the above, I personally would need at least [calendar, notes, title, uid, startDate, endDate, location, and isAllDay]. But as long as we’re at it might as well make it general, right?

Then if I had that, the one other thing I would need is a way to pass a list of strings of names of calendars to the Obj-C method, so that this line:

NSPredicate *predicate = [CalCalendarStore eventPredicateWithStartDate:theStartdate endDate:theEnddate calendars:[calStore calendars]];

would fetch only from the appropriate calendars?

Then I think I’d be truly good to go.

Thanks for your time, I’ll continue to plug at it, too.

I am not sure about choosing the individual calendar (I will try to work on that), but you can re-configure the enumerator loop by adding an internal mutable array, and then adding that array to the final array:

while (event = [eventEnumerator nextObject]) {
		NSMutableArray *eachEvent = [NSMutableArray arrayWithCapacity:10];
		[eachEvent addObject:[event title]];
		[eachEvent addObject:[event calendar]];
		[eachEvent addObject:[event startDate]];
		[eachEvent addObject:[event endDate]];
		[eventInformation addObject:eachEvent];
	}

This particular routine returns a list like this:

{{"Dance Practice", item id 9, date "Tuesday, June 10, 2008 7:00:00 PM", date "Tuesday, June 10, 2008 8:30:00 PM"}, {"Piano Recital", item id 10, date "Friday, June 13, 2008 7:00:00 PM", date "Friday, June 13, 2008 9:00:00 PM"}, {"Piano Recital", item id 9, date "Friday, June 13, 2008 7:00:00 PM", date "Friday, June 13, 2008 9:00:00 PM"}, {"Dance Practice", item id 9, date "Saturday, June 14, 2008 8:00:00 AM", date "Saturday, June 14, 2008 10:00:00 AM"}, {"Work", item id 11, date "Saturday, June 14, 2008 9:00:00 AM", date "Saturday, June 14, 2008 12:00:00 PM"}, {"Kimbo/Derek Haynes BBQ", item id 11, date "Saturday, June 14, 2008 1:00:00 PM", date "Saturday, June 14, 2008 6:00:00 PM"}}

As you can see, the [eachEvent addObject:[event calendar]]; is not working right, but the dates are in there and accurate.

Thanks again… getting somewhere! but a long road to go :wink:

As for your last return in the list from “Calendar” that is understandable because it is returning a Calendar object, what we need is the name of the calendar object.

Similarly, if we were to able able to pass a list of calendar names to the handler, it would have to loop through them and create an array with the matching calendar objects. I don’t think I can pull that one off!

Here’s the rest of my report thus far:

These properties of the event class and the superclass can all be added:

[eachEvent addObject:[event uid]];
[eachEvent addObject:[event calendar]];
[eachEvent addObject:[event title]];
[eachEvent addObject:[event startDate]];
[eachEvent addObject:[event endDate]];
[eachEvent addObject:[event notes]];
[eachEvent addObject:[event occurrence]];
[eachEvent addObject:[event dateStamp]];

These 3 give yellow compile warnings:

[eachEvent addObject:[event isAllDay]];
[eachEvent addObject:[event isDetached]];
[eachEvent addObject:[event hasAlarm]];

warning: passing argument 1 of ‘addObject:’ makes pointer from integer without a cat
(note that all 3 of them are boolean)

This 1 gives red compile error:
[eachEvent addObject:[event location]];

error: incompatible type for argument 1 of ‘addObject:’
(this one makes no sense, should just be a string)

Then of the rest, several compile but cause handler to fail:

[eachEvent addObject:[event url]];
[eachEvent addObject:[event attendees]];
[eachEvent addObject:[event recurrenceRule]];
[eachEvent addObject:[event nextAlarmDate]];
[eachEvent addObject:[event alarms]];

Question:

When we define this:
NSMutableArray *eventInformation = [NSMutableArray arrayWithCapacity:10];

what if capacity is unknown?

Not a problem, since the array is mutable. You just need to plug in a number for the initialization of the object. You could use 1 if you want, I like 10.

I will see about the others over the next few days.

Fantastic. Nice team work.

I’ve rebuilt the demo project, this time with table output and placeholders for all the properties that don’t work in the method (numbered strings). So please work with this, same link as before:

http://www.woodenbrain.com/sw/betas/WBCCalendarStore.zip

First bummer to report is it crashes at launch at Tiger. This is even with checks for Leopard built in and no Cal Store code executed on Tiger. So it’s the added framework. So first thing needed is a way to programmatically load the framework. Again, I thought that the “#include” should have been sufficient? I haven’t made sense out of this thread yet but it may help:

http://lists.apple.com/archives/xcode-users/2007/Dec/msg00132.html

Second, in addition to what I reported above:

   // following works IN MOST CASES BUT SEEMS TO BREAK WITH RECURRING EVENTS, or possibly if NOTES IS BLANK
   //[eachEvent addObject:[event notes]];

So clearly there needs to be some error trapping for each (or most) properties added to the array.

Third, I do think there is some memory allocation / release issue, this from my system log:

Jun 11 01:01:04 G5-Dual Xcode[78679]: Xcode(78679,0xf0103000) malloc: free_garbage: garbage ptr = 0x30dc010, has non-zero refcount = 1
Jun 11 01:01:04 G5-Dual Xcode[78679]: Xcode(78679,0xf0103000) malloc: free_garbage: garbage ptr = 0x316a4d0, has non-zero refcount = 1
Jun 11 01:01:04 G5-Dual [0x0-0x3f13f1].com.apple.Xcode[78679]: Xcode(78679,0xf0103000) malloc: free_garbage: garbage ptr = 0x30dc010, has non-zero refcount = 1
Jun 11 01:01:04 G5-Dual [0x0-0x3f13f1].com.apple.Xcode[78679]: Xcode(78679,0xf0103000) malloc: free_garbage: garbage ptr = 0x316a4d0, has non-zero refcount = 1

This may also be due to Xcode itself though. (I have filed a bug report on Xcode 3.0 crashing on PPC with bugreporter before this.) Still it would seem prudent to do what is necessary.

Edit:

As of 7 AM EDT June 11, I rebuilt the project again, this time fixing a couple of issues with the table and sort of getting the calendar name. I say sort of because it works but there is a warning. Must have syntax off?


// following works, but we need calendar title instead
// [eachEvent addObject:[event calendar]];
// following works, but puts up warning during compile "NSCalendar may not respond to -title"
[eachEvent addObject:[[event calendar] title]];

It is available at the same URL.

It occurred to me also that:

  1. it will be necessary for my purposes and probably useful to others to have:
    A. use all calendars (current default)
    B. (EXCLUDE calendars whose names are) parameter
    C. (INCLUDE calendars whose names are) parameter.

  2. Perhaps the do/while loop to make the NSArray of event properties should be a subroutine, so that other methods can be added later and the same ordered event list could be returned.

Thanks!

At 10:30 EDT June 11, I uploaded another revision to the project with same URL.

This time it has a second table for calendars with their properties, and a check box on the left. The idea is to implement passing a list of calendar uid’s to the handler, and then in the handler using one of the methods, calendarWithUID:, to create an array of calendars we actually want to include. Possibly if an empty list is passed then use the default “calendars”

As with the events properties, there are 2 that don’t seem to work: notes (same reason?) and isEditable (same reason, boolean)…

At 18:25 EDT June 11, I uploaded another revision to the project with same URL.

This is about as far as I can take it for now without some more Obj-C help.

Added:

– search uncompleted todos that are due before given end date [obj-c & applescript]
– get todos and their properties, mapping them as closely as possible to the events’ properties [obj-c & applescript]
– output todos on same table as events
– AS routine for checking which of the calendars’ check boxes on the tables are enabled and reporting and producing a list of UIDs to pass to the handler.

(* now to search, you must get calendars first, though it actually has no effect yet)

Modified
– obj-c methods to include receivng list of calendar uids.

Added, but not successfully:
– obj-c conditional statement and build array loop to try to deal with passed list of uids.
– obj-c subroutine to get make the eachEvent array, for less code clutter.

Notes:

To-Dos, as with tasks, seem to have certain properties that cause the handler to fail. It seems that booleans are tricky, and anything with null or missing value causes errors.

So…
priorities for completion of this demo project as I see it are:

  1. load Calendar Store framework programmatically so it can run on Tiger.
  2. correctly implement passing a list of calendar uids to the fetch events & fetech todos handlers.
  3. include necessary coercion and error checking for each property for events and tasks
  4. address any possible memory release issues

EDIT:

with regards to priority #3, I figured out the boolean problem, by specifying like this:
[eachEvent addObject:[NSNumber numberWithBool:[event isAllDay]]]; //#9

as for the others, they all relate to nil values. have not figured out yet how to fix those.

debugger console shows:
[NSCFArray insertObject:atIndex:]: attempt to insert nil

if, following above pattern, I try this:
[eachEvent addObject:[NSString stringWithFormat:[event notes]]]; //#6

then debugger console shows:
-[NSPlaceholderString initWithFormat:locale:arguments:]: nil argument

so how do we force a nil or 0 or missing value or anything in there to the NSMutableArray?

(have not updated the project online yet)

OK, MAJOR update here… I checked all “nil” values with conditional statements and return “” for strings and 0 for dates. certainly not the most efficient way, but it works.

So now the only properties that DON’T work are:

  1. event location. no idea what’s wrong there (would like to fix that one)
  2. some arrays of other objects (such as alarms, recurrenceRules, attendees) [can live without these for now]

Now, “priorities for completion” are:

  1. load Calendar Store framework programmatically so it can run on Tiger.
  2. correctly implement passing a list of calendar uids to the fetch events & fetech todos handlers.
  3. address any possible memory release issues

Please help with these, thanks!

New version uploaded with same URL.

You are just chugging along!! Sorry that I have been out of touch. I have been keeping up with your posts, but have been short on coding time this week. Tonight looks OK, though, we shall see.

Cool,

After much reading of developer docs and cocoa lists etc that’s way over my head, I managed to, I believe, figure out priority #1. Despite what all those sources said, since I don’t need this code at all on Tiger and don’t execute it if it is present, all I had to do was the following:

  1. add the CalendarStore.framework to the project & build at least once
  2. add this line to the Other Linker Flags on the Builds section of Targets:
    –weak_framework CalendarStore
  3. build again for good measure
  4. delete CalendarStore.framework (I had to both where Xcode stuck it in “Scripts” and in “Link Binary with Libraries” under Target.
  5. build again!
  6. of course you have to make sure you never create or call the custom obj-c class that in turn imports CalendarStore.framework if Leopard is not running

http://developer.apple.com/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html

so now we’re down to priorities 2 & 3 + figuring out why “location” won’t work!

Made a little progress tonight. You can now return ONLY the events associated with the selected calendar(s). Here is the code to add to the -(NSArray *)getEventsinRangewithStart:(NSDate *)theStartdate withEnd:(NSDate *)theEnddate withCals:(NSMutableArray *)theuids method:

This will provide an array to hold the calendar objects for the predicate:

NSMutableArray *selectedCalendars = [NSMutableArray arrayWithCapacity:10]; //This will hold the array of Calendar objects so that only events from the checked calendar(s) are returned

Then, after the calStore initialization, add this Enumeration to fill the array:

NSEnumerator *calUIDEnumerator = [theuids objectEnumerator];
	id calUID;
	while (calUID = [calUIDEnumerator nextObject]) {
		[selectedCalendars addObject:[calStore calendarWithUID:calUID]];
	}

It works perfectly, since you already set up the method to receive the list of calendar UIDs from the AS portion. There are some other cleanup details for your other methods, but I will leave that you, since you know where to put the stuff.

I have been puzzling over the problem of Tiger and Leopard issues, and also tried reading the documentation, but I am totally confused about the whole picture. I will continue pondering…

Hi Craig,

Thanks a lot. Sorry got a bit side tracked here on implementing something related to Growl too… obj-c is a fussy beast.

First with regards to the Tiger / Leopard stuff – I think I have that sussed out as I said in last post. I really can’t believe how simple it was given how complicated people suggest it is.

Second, aside from this whole issue still wondering about that “location” property and memory problems.

Now then…

What you just posted is as far as I can tell the same thing I had in the commented out portion of the current version of the app online (with a couple different variable names).

I think you overlooked the part where I wanted to conditionally use “calendars” for all calendars if the content of the passed array/list was {“*”} rather than a list of uid strings.

Edit:

was having problems and still having some but jumped the gun here… will post later.

OK… I thought I had solved it by explicitly delcaring *prediate but I was wrong. Compiler continues to object to unused variable ‘predicate’.

See Screen shot:

http://www.woodenbrain.com/Images/WBCcalStore.m.jpg

And here is the currently “Broken” version of the project:

http://www.woodenbrain.com/sw/betas/WBCCalendarStore-broken.zip
(edit: before had the url of the last semi-functional version, not the really broken version, posted.)


-(NSArray *)getEventsinRangewithStart:(NSDate *)theStartdate withEnd:(NSDate *)theEnddate withCals:(NSMutableArray *)theuids {
   NSMutableArray *eventInformation = [NSMutableArray arrayWithCapacity:10];
   CalCalendarStore *calStore = [CalCalendarStore defaultCalendarStore];
   // here we define *predicate (the search function) in advance because it appears in two variations in the conditional if / else; not sure if this is necessary
 
   // we check whether we are passed the wildcard array {"*"}, in which case we use the default calendar list
   // not sure if this is the best conditional check
   
   // must explicitly define *predicate EVEN THOUGH we are going to do so in the following conditional statements.  WTF.
   
NSPredicate *predicate;
if ([theuids objectAtIndex:0] == [NSString stringWithString:@"*"]) {
//if (YES) {
NSLOG(@"met wildcard condition, using all calendars");
 NSPredicate *predicate = [CalCalendarStore eventPredicateWithStartDate:theStartdate endDate:theEnddate calendars:[calStore calendars]];
}
else
// loop through the passed uid's list (theuids) and get the corresponding calendar object from the C Store and add each to theCalendars array
{
 NSLOG(@"did not meet wildcard condition, using specific calendars");
  NSMutableArray *selectedCalendars = [NSMutableArray arrayWithCapacity:10];
   NSEnumerator *calUIDEnumerator = [theuids objectEnumerator];
   id calUID;
   while (calUID = [calUIDEnumerator nextObject]) {
       [selectedCalendars addObject:[calStore calendarWithUID:calUID]];
	   }
	   NSPredicate *predicate = [CalCalendarStore eventPredicateWithStartDate:theStartdate endDate:theEnddate calendars:selectedCalendars];
}

// the following line would be done in the condition above if the array passed = {"*"} in theory; but it doesn't work so here it is without the condition
//NSPredicate *predicate = [CalCalendarStore eventPredicateWithStartDate:theStartdate endDate:theEnddate calendars:[calStore calendars]];

// now do a search on the predicate (search function) and then loop through results, creating an array of a list of available properties for each event to return to AS
   
   NSEnumerator *eventEnumerator = [[calStore eventsWithPredicate:predicate] objectEnumerator];

Nota Bene: I am not really an Objective-C programmer, but I think I spotted the problem you are having with predicate (it is really at the level of C, with which I have more direct experience).

It looks like you have a variable scoping problem. Here is a skeleton version of the code you posted:

There are three distinct variables named predicate here. The one in the function scope of getEventsinRangeWithStart:withCals: and one in each block at the branches of the conditional. A variable declared inside a block (curly braces) is only visible inside that block or other nested scopes. This is called block scope. If this block scoped variable happens to have the same name as a variable in an outer scope, then the inner variable ˜shadows’ the outer variable from the point the variable is declared to the end of the immediately enclosing block (this varies by language, JavaScript does not really have block scoping–even though one can declare new variables in a block, they are still scoped at the function level, not the level of the immediately enclosing block).

To my eye, it looks like the two inner predicate variables are never used (they are initialized, but values they hold are never referenced). The outer predicate is referenced, but never initialized.

To solve this scope problem, I think you want to write the code like this:

In short: Declare it once in the outer scope, and assign a value to it in the branches of the conditional, but do not redeclare it.

chris,

that’s a fantastic explanation, thanks. and it doesn’t hurt that it worked.

so with your help and craig’s of course, I believe I have a fully functional demo now!

it is at least good enough for my needs with a few limitations, and i’m going to start folding it in to what i really need these methods for, OmniGrowl.

Here is the project URL once more:

http://www.woodenbrain.com/sw/betas/WBCCalendarStore.zip

and these are remaining to-dos. help from experts appreciated, and source is well commented related to these issues.

  1. check if there really are any memory leakage problems
  2. figure out why the event location property can’t be retreived, and/or file bug report with apple
  3. (somehow) get rid of the compile warnings like for [[event calendar] title]
  4. possilby iron out some of the other non-supported properties
  5. eventually optimize the code

WBC, Craig and others,

Fantastic work on the CalendarStore. I started looking into this earlier this year and quickly got overwhelmed and gave up. My goal is to be able to access the Calendar Store from within Applescript, even if it is through some sort of helper app. What I’d like to do is be able to trigger a text to speech reading of events for the day from Applescript. I need to trigger from Applescript because the home automation software I use supports it and this is how I plan to trigger it (i.e. press a button in the bathroom and "Good morning the weather today is… Today is Garbage day, you have a meeting at 10am. " etc. Is it possible to trigger an Applescript Studio app from Applescript? Any thoughts on the feasibility of this?

It is possible to trigger an AS Studio app from applescript if:

  1. that studio app includes an applescript dictionary

or

  1. you use the simpler but more “hacky” method that is actually being used by the demo app of scripting the interface buttons.

with that method, you can tell the studio app to ‘perform click’ on a button with a normal applescript.

if you look at OmniGrowl, it installs a bunch of “immediate actions” which use method #2.