2005-02-23 16:05:09 +00:00
|
|
|
/**
|
|
|
|
* The GSRunLoopCtxt stores context information to handle polling for
|
|
|
|
* events. This information is associated with a particular runloop
|
|
|
|
* mode, and persists throughout the life of the runloop instance.
|
|
|
|
*
|
|
|
|
* NB. This class is private to NSRunLoop and must not be subclassed.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
#include "GNUstepBase/preface.h"
|
2005-10-30 10:42:42 +00:00
|
|
|
#include "../GSRunLoopCtxt.h"
|
|
|
|
#include "../GSRunLoopWatcher.h"
|
2005-02-23 16:05:09 +00:00
|
|
|
#include <Foundation/NSDebug.h>
|
|
|
|
#include <Foundation/NSNotificationQueue.h>
|
|
|
|
#include <Foundation/NSPort.h>
|
|
|
|
|
|
|
|
extern BOOL GSCheckTasks();
|
|
|
|
|
|
|
|
#if GS_WITH_GC == 0
|
|
|
|
SEL wRelSel;
|
|
|
|
SEL wRetSel;
|
|
|
|
IMP wRelImp;
|
|
|
|
IMP wRetImp;
|
|
|
|
|
|
|
|
static void
|
|
|
|
wRelease(NSMapTable* t, void* w)
|
|
|
|
{
|
|
|
|
(*wRelImp)((id)w, wRelSel);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
wRetain(NSMapTable* t, const void* w)
|
|
|
|
{
|
|
|
|
(*wRetImp)((id)w, wRetSel);
|
|
|
|
}
|
|
|
|
|
|
|
|
static const NSMapTableValueCallBacks WatcherMapValueCallBacks =
|
|
|
|
{
|
|
|
|
wRetain,
|
|
|
|
wRelease,
|
|
|
|
0
|
|
|
|
};
|
|
|
|
#else
|
|
|
|
#define WatcherMapValueCallBacks NSOwnedPointerMapValueCallBacks
|
|
|
|
#endif
|
|
|
|
|
|
|
|
@implementation GSRunLoopCtxt
|
|
|
|
- (void) dealloc
|
|
|
|
{
|
|
|
|
RELEASE(mode);
|
|
|
|
GSIArrayEmpty(performers);
|
|
|
|
NSZoneFree(performers->zone, (void*)performers);
|
|
|
|
GSIArrayEmpty(timers);
|
|
|
|
NSZoneFree(timers->zone, (void*)timers);
|
|
|
|
GSIArrayEmpty(watchers);
|
|
|
|
NSZoneFree(watchers->zone, (void*)watchers);
|
|
|
|
if (handleMap != 0)
|
|
|
|
{
|
|
|
|
NSFreeMapTable(handleMap);
|
|
|
|
}
|
2005-10-30 10:42:42 +00:00
|
|
|
if (winMsgMap != 0)
|
|
|
|
{
|
|
|
|
NSFreeMapTable(winMsgMap);
|
|
|
|
}
|
2005-02-23 16:05:09 +00:00
|
|
|
[super dealloc];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove any callback for the specified event which is set for an
|
|
|
|
* uncompleted poll operation.<br />
|
|
|
|
* This is called by nested event loops on contexts in outer loops
|
|
|
|
* when they handle an event ... removing the event from the outer
|
|
|
|
* loop ensures that it won't get handled twice, once by the inner
|
|
|
|
* loop and once by the outer one.
|
|
|
|
*/
|
|
|
|
- (void) endEvent: (void*)data
|
|
|
|
type: (RunLoopEventType)type
|
|
|
|
{
|
|
|
|
if (completed == NO)
|
|
|
|
{
|
|
|
|
switch (type)
|
|
|
|
{
|
|
|
|
case ET_HANDLE:
|
|
|
|
break;
|
2005-10-30 12:08:54 +00:00
|
|
|
case ET_WINMSG:
|
|
|
|
break;
|
2005-02-23 16:05:09 +00:00
|
|
|
default:
|
|
|
|
NSLog(@"Ending an event of unkown type (%d)", type);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mark this poll context as having completed, so that if we are
|
|
|
|
* executing a re-entrant poll, the enclosing poll operations
|
|
|
|
* know they can stop what they are doing because an inner
|
|
|
|
* operation has done the job.
|
|
|
|
*/
|
|
|
|
- (void) endPoll
|
|
|
|
{
|
|
|
|
completed = YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (id) init
|
|
|
|
{
|
|
|
|
[NSException raise: NSInternalInconsistencyException
|
|
|
|
format: @"-init may not be called for GSRunLoopCtxt"];
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (id) initWithMode: (NSString*)theMode extra: (void*)e
|
|
|
|
{
|
|
|
|
self = [super init];
|
|
|
|
if (self != nil)
|
|
|
|
{
|
|
|
|
NSZone *z = [self zone];
|
|
|
|
|
|
|
|
mode = [theMode copy];
|
|
|
|
extra = e;
|
|
|
|
performers = NSZoneMalloc(z, sizeof(GSIArray_t));
|
|
|
|
GSIArrayInitWithZoneAndCapacity(performers, z, 8);
|
|
|
|
timers = NSZoneMalloc(z, sizeof(GSIArray_t));
|
|
|
|
GSIArrayInitWithZoneAndCapacity(timers, z, 8);
|
|
|
|
watchers = NSZoneMalloc(z, sizeof(GSIArray_t));
|
|
|
|
GSIArrayInitWithZoneAndCapacity(watchers, z, 8);
|
|
|
|
|
|
|
|
handleMap = NSCreateMapTable(NSIntMapKeyCallBacks,
|
|
|
|
WatcherMapValueCallBacks, 0);
|
2005-10-30 10:42:42 +00:00
|
|
|
winMsgMap = NSCreateMapTable(NSIntMapKeyCallBacks,
|
|
|
|
WatcherMapValueCallBacks, 0);
|
2005-02-23 16:05:09 +00:00
|
|
|
|
|
|
|
msgTarget = nil;
|
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL) pollUntil: (int)milliseconds within: (NSArray*)contexts
|
|
|
|
{
|
|
|
|
NSMapEnumerator hEnum;
|
|
|
|
GSRunLoopWatcher *watcher;
|
2005-10-30 12:46:26 +00:00
|
|
|
HANDLE handleArray[MAXIMUM_WAIT_OBJECTS-1];
|
2005-02-23 16:05:09 +00:00
|
|
|
int num_handles;
|
2005-10-30 10:42:42 +00:00
|
|
|
int num_winMsgs;
|
2005-02-23 16:05:09 +00:00
|
|
|
unsigned i;
|
2005-10-04 11:54:03 +00:00
|
|
|
void *handle;
|
2005-02-23 16:05:09 +00:00
|
|
|
int wait_timeout;
|
|
|
|
DWORD wait_return;
|
|
|
|
BOOL do_wait;
|
|
|
|
|
|
|
|
// Set timeout how much time should wait
|
|
|
|
if (milliseconds >= 0)
|
|
|
|
{
|
|
|
|
wait_timeout = milliseconds;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
wait_timeout = INFINITE;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSResetMapTable(handleMap);
|
2005-10-30 10:42:42 +00:00
|
|
|
NSResetMapTable(winMsgMap);
|
2005-02-23 16:05:09 +00:00
|
|
|
|
|
|
|
i = GSIArrayCount(watchers);
|
|
|
|
num_handles = 0;
|
2005-10-30 10:42:42 +00:00
|
|
|
num_winMsgs = 0;
|
2005-02-23 16:05:09 +00:00
|
|
|
while (i-- > 0)
|
|
|
|
{
|
|
|
|
GSRunLoopWatcher *info;
|
|
|
|
HANDLE handle;
|
|
|
|
|
|
|
|
info = GSIArrayItemAtIndex(watchers, i).obj;
|
|
|
|
if (info->_invalidated == YES)
|
|
|
|
{
|
|
|
|
GSIArrayRemoveItemAtIndex(watchers, i);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
switch (info->type)
|
|
|
|
{
|
|
|
|
case ET_HANDLE:
|
|
|
|
handle = (HANDLE)(int)info->data;
|
|
|
|
NSMapInsert(handleMap, (void*)handle, info);
|
|
|
|
num_handles++;
|
|
|
|
break;
|
|
|
|
case ET_RPORT:
|
|
|
|
{
|
|
|
|
id port = info->receiver;
|
|
|
|
int port_handle_count = 128; // #define this constant
|
|
|
|
int port_handle_array[port_handle_count];
|
|
|
|
if ([port respondsToSelector: @selector(getFds:count:)])
|
|
|
|
{
|
|
|
|
[port getFds: port_handle_array count: &port_handle_count];
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
NSLog(@"pollUntil - Impossible get win32 Handles");
|
|
|
|
abort();
|
|
|
|
}
|
|
|
|
NSDebugMLLog(@"NSRunLoop", @"listening to %d port handles",
|
|
|
|
port_handle_count);
|
|
|
|
while (port_handle_count--)
|
|
|
|
{
|
|
|
|
NSMapInsert(handleMap,
|
|
|
|
(void*)port_handle_array[port_handle_count], info);
|
|
|
|
num_handles++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2005-10-30 10:42:42 +00:00
|
|
|
case ET_WINMSG:
|
2005-10-30 12:08:54 +00:00
|
|
|
handle = (HANDLE)(int)info->data;
|
2005-10-30 10:42:42 +00:00
|
|
|
NSMapInsert(winMsgMap, (void*)handle, info);
|
|
|
|
num_winMsgs++;
|
|
|
|
break;
|
2005-02-23 16:05:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If there are notifications in the 'idle' queue, we try an
|
|
|
|
* instantaneous select so that, if there is no input pending,
|
|
|
|
* we can service the queue. Similarly, if a task has completed,
|
|
|
|
* we need to deliver its notifications.
|
|
|
|
*/
|
|
|
|
if (GSCheckTasks() || GSNotifyMore())
|
|
|
|
{
|
|
|
|
wait_timeout = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
i = 0;
|
2005-10-30 10:42:42 +00:00
|
|
|
hEnum = NSEnumerateMapTable(handleMap);
|
2005-10-04 11:54:03 +00:00
|
|
|
while (NSNextMapEnumeratorPair(&hEnum, &handle, (void**)&watcher))
|
2005-02-23 16:05:09 +00:00
|
|
|
{
|
2005-10-30 12:46:26 +00:00
|
|
|
if (i < MAXIMUM_WAIT_OBJECTS-1)
|
|
|
|
{
|
|
|
|
handleArray[i++] = (HANDLE)handle;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
NSLog(@"Too many handles to wait for ... only using %d of %d",
|
|
|
|
i, num_handles);
|
|
|
|
}
|
2005-02-23 16:05:09 +00:00
|
|
|
}
|
2005-10-30 10:42:42 +00:00
|
|
|
NSEndMapTableEnumeration(&hEnum);
|
2005-10-30 12:08:54 +00:00
|
|
|
num_handles = i;
|
2005-02-23 16:05:09 +00:00
|
|
|
|
|
|
|
do_wait = YES;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
wait_return = MsgWaitForMultipleObjects(num_handles, handleArray,
|
2005-10-30 10:42:42 +00:00
|
|
|
NO, wait_timeout, QS_ALLINPUT);
|
2005-02-23 16:05:09 +00:00
|
|
|
NSDebugMLLog(@"NSRunLoop", @"wait returned %d", wait_return);
|
|
|
|
|
|
|
|
// if there are windows message
|
|
|
|
if (wait_return == WAIT_OBJECT_0 + num_handles)
|
|
|
|
{
|
2005-10-30 10:42:42 +00:00
|
|
|
MSG msg;
|
|
|
|
INT bRet;
|
|
|
|
|
2005-10-30 12:46:26 +00:00
|
|
|
if (msgTarget == nil)
|
2005-10-30 10:42:42 +00:00
|
|
|
{
|
2005-10-30 12:46:26 +00:00
|
|
|
bRet = PeekMessage(&msg, 0, 0, 0, PM_REMOVE);
|
|
|
|
if (bRet != 0)
|
2005-10-30 10:42:42 +00:00
|
|
|
{
|
2005-11-20 10:36:12 +00:00
|
|
|
if (msg.message == WM_QUIT)
|
|
|
|
{
|
|
|
|
Class c = NSClassFromString(@"NSApplication");
|
|
|
|
|
|
|
|
if (c == 0)
|
|
|
|
{
|
|
|
|
[NSException raise: NSGenericException
|
|
|
|
format: @"Received WM_QUIT"];
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
SEL s = NSSelectorFromString(@"sharedApplication");
|
|
|
|
id o = [c performSelector: s];
|
|
|
|
|
|
|
|
s = NSSelectorFromString(@"terminate:");
|
|
|
|
[o performSelector: s withObject: nil];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2005-10-30 12:46:26 +00:00
|
|
|
if (num_winMsgs > 0)
|
2005-10-30 10:42:42 +00:00
|
|
|
{
|
2005-10-30 12:46:26 +00:00
|
|
|
HANDLE handle;
|
|
|
|
|
|
|
|
handle = msg.hwnd;
|
|
|
|
watcher = (GSRunLoopWatcher*)NSMapGet(winMsgMap,
|
|
|
|
(void*)handle);
|
|
|
|
if (watcher == nil || watcher->_invalidated == YES)
|
2005-10-30 12:08:54 +00:00
|
|
|
{
|
2005-10-30 12:46:26 +00:00
|
|
|
handle = 0; // Generic
|
|
|
|
watcher = (GSRunLoopWatcher*)NSMapGet(winMsgMap,
|
|
|
|
(void*)handle);
|
2005-10-30 12:08:54 +00:00
|
|
|
}
|
2005-10-30 12:46:26 +00:00
|
|
|
if (watcher != nil && watcher->_invalidated == NO)
|
2005-10-30 10:42:42 +00:00
|
|
|
{
|
|
|
|
i = [contexts count];
|
|
|
|
while (i-- > 0)
|
|
|
|
{
|
|
|
|
GSRunLoopCtxt *c = [contexts objectAtIndex: i];
|
|
|
|
|
|
|
|
if (c != self)
|
|
|
|
{
|
|
|
|
[c endEvent: (void*)handle type: ET_WINMSG];
|
|
|
|
}
|
|
|
|
}
|
2005-10-30 12:08:54 +00:00
|
|
|
completed = YES;
|
2005-10-30 10:42:42 +00:00
|
|
|
/*
|
2005-10-30 12:08:54 +00:00
|
|
|
* The watcher is still valid - so call the
|
|
|
|
* receiver's event handling method.
|
2005-10-30 10:42:42 +00:00
|
|
|
*/
|
|
|
|
(*watcher->handleEvent)(watcher->receiver,
|
2005-10-30 12:46:26 +00:00
|
|
|
eventSel, watcher->data, watcher->type,
|
|
|
|
(void*)(gsaddr)&msg, mode);
|
2005-10-30 10:42:42 +00:00
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
2005-10-30 12:46:26 +00:00
|
|
|
DispatchMessage(&msg);
|
2005-10-30 10:42:42 +00:00
|
|
|
}
|
2005-10-30 12:46:26 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (num_winMsgs > 0)
|
2005-10-30 12:08:54 +00:00
|
|
|
{
|
2005-10-30 12:46:26 +00:00
|
|
|
GSRunLoopWatcher *generic = nil;
|
|
|
|
unsigned num = num_winMsgs;
|
|
|
|
|
|
|
|
hEnum = NSEnumerateMapTable(winMsgMap);
|
|
|
|
while (NSNextMapEnumeratorPair(&hEnum, &handle,
|
|
|
|
(void**)&watcher))
|
2005-10-30 12:08:54 +00:00
|
|
|
{
|
2005-10-30 12:46:26 +00:00
|
|
|
if (watcher->_invalidated == NO)
|
2005-10-30 12:08:54 +00:00
|
|
|
{
|
2005-10-30 12:46:26 +00:00
|
|
|
if (handle == 0 && num > 1)
|
|
|
|
{
|
|
|
|
// Let window specific watchers have first attempt.
|
|
|
|
generic = watcher;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
bRet = PeekMessage(&msg, handle, 0, 0, PM_REMOVE);
|
|
|
|
if (bRet != 0)
|
|
|
|
{
|
|
|
|
i = [contexts count];
|
|
|
|
while (i-- > 0)
|
|
|
|
{
|
|
|
|
GSRunLoopCtxt *c = [contexts objectAtIndex: i];
|
|
|
|
|
|
|
|
if (c != self)
|
|
|
|
{
|
|
|
|
[c endEvent: (void*)handle
|
|
|
|
type: ET_WINMSG];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
NSEndMapTableEnumeration(&hEnum);
|
|
|
|
completed = YES;
|
|
|
|
/*
|
|
|
|
* The watcher is still valid - so call the
|
|
|
|
* receiver's event handling method.
|
|
|
|
*/
|
|
|
|
(*watcher->handleEvent)(watcher->receiver,
|
|
|
|
eventSel, watcher->data, watcher->type,
|
|
|
|
(void*)(gsaddr)&msg, mode);
|
|
|
|
return NO;
|
2005-10-30 12:08:54 +00:00
|
|
|
}
|
|
|
|
}
|
2005-10-30 12:46:26 +00:00
|
|
|
num--;
|
2005-10-30 12:08:54 +00:00
|
|
|
}
|
2005-10-30 12:46:26 +00:00
|
|
|
NSEndMapTableEnumeration(&hEnum);
|
|
|
|
if (generic != nil && generic->_invalidated == NO)
|
|
|
|
{
|
|
|
|
bRet = PeekMessage(&msg, 0, 0, 0, PM_REMOVE);
|
|
|
|
if (bRet != 0)
|
|
|
|
{
|
|
|
|
i = [contexts count];
|
|
|
|
while (i-- > 0)
|
|
|
|
{
|
|
|
|
GSRunLoopCtxt *c = [contexts objectAtIndex: i];
|
2005-02-23 16:05:09 +00:00
|
|
|
|
2005-10-30 12:46:26 +00:00
|
|
|
if (c != self)
|
|
|
|
{
|
|
|
|
[c endEvent: (void*)0 type: ET_WINMSG];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
NSEndMapTableEnumeration(&hEnum);
|
|
|
|
completed = YES;
|
|
|
|
(*generic->handleEvent)(watcher->receiver,
|
|
|
|
eventSel, watcher->data, watcher->type,
|
|
|
|
(void*)(gsaddr)&msg, mode);
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
2005-10-30 10:42:42 +00:00
|
|
|
}
|
2005-10-30 12:46:26 +00:00
|
|
|
completed = YES;
|
|
|
|
[msgTarget performSelector: msgSelector withObject: nil];
|
|
|
|
return NO;
|
2005-10-30 10:42:42 +00:00
|
|
|
}
|
2005-02-23 16:05:09 +00:00
|
|
|
--wait_timeout;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
do_wait = NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
while (do_wait && (wait_timeout >= 0));
|
|
|
|
|
|
|
|
// check wait errors
|
|
|
|
if (wait_return == WAIT_FAILED)
|
|
|
|
{
|
2005-11-14 09:25:31 +00:00
|
|
|
int i;
|
|
|
|
BOOL found = NO;
|
|
|
|
|
|
|
|
NSDebugMLLog(@"NSRunLoop", @"WaitForMultipleObjects() error in "
|
|
|
|
@"-acceptInputForMode:beforeDate: %s",
|
2005-11-10 21:45:16 +00:00
|
|
|
GSLastErrorStr(GetLastError()));
|
2005-11-14 09:25:31 +00:00
|
|
|
/*
|
|
|
|
* Check each handle in turn until either we find one which has an
|
|
|
|
* event signalled, or we find the one which caused the original
|
|
|
|
* wait to fail ... so the callback routine for that handle can
|
|
|
|
* deal with the problem.
|
|
|
|
*/
|
|
|
|
for (i = 0; i < num_handles; i++)
|
|
|
|
{
|
|
|
|
handleArray[0] = handleArray[i];
|
|
|
|
wait_return = WaitForMultipleObjects(1, handleArray, NO, 0);
|
|
|
|
if (wait_return != WAIT_TIMEOUT)
|
|
|
|
{
|
|
|
|
wait_return = WAIT_OBJECT_0;
|
|
|
|
found = YES;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (found == NO)
|
|
|
|
{
|
|
|
|
NSLog(@"WaitForMultipleObjects() error in "
|
|
|
|
@"-acceptInputForMode:beforeDate: %s",
|
|
|
|
GSLastErrorStr(GetLastError()));
|
|
|
|
abort ();
|
|
|
|
}
|
2005-02-23 16:05:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if there arent events
|
|
|
|
if (wait_return == WAIT_TIMEOUT)
|
|
|
|
{
|
|
|
|
completed = YES;
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Look the event that WaitForMultipleObjects() says is ready;
|
|
|
|
* get the corresponding fd for that handle event and notify
|
|
|
|
* the corresponding object for the ready fd.
|
|
|
|
*/
|
|
|
|
i = wait_return - WAIT_OBJECT_0;
|
|
|
|
|
|
|
|
NSDebugMLLog(@"NSRunLoop", @"Event listen %d", i);
|
|
|
|
|
|
|
|
handle = handleArray[i];
|
|
|
|
|
|
|
|
watcher = (GSRunLoopWatcher*)NSMapGet(handleMap, (void*)handle);
|
|
|
|
if (watcher != nil && watcher->_invalidated == NO)
|
|
|
|
{
|
|
|
|
i = [contexts count];
|
|
|
|
while (i-- > 0)
|
|
|
|
{
|
|
|
|
GSRunLoopCtxt *c = [contexts objectAtIndex: i];
|
|
|
|
|
|
|
|
if (c != self)
|
|
|
|
{
|
|
|
|
[c endEvent: (void*)handle type: ET_HANDLE];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
|
|
* The watcher is still valid - so call its receivers
|
|
|
|
* event handling method.
|
|
|
|
*/
|
2005-11-11 12:09:19 +00:00
|
|
|
NSDebugMLLog(@"NSRunLoop", @"Event callback found");
|
2005-02-23 16:05:09 +00:00
|
|
|
(*watcher->handleEvent)(watcher->receiver,
|
|
|
|
eventSel, watcher->data, watcher->type,
|
|
|
|
(void*)(gsaddr)handle, mode);
|
|
|
|
}
|
|
|
|
|
|
|
|
GSNotifyASAP();
|
|
|
|
|
|
|
|
completed = YES;
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|