// // P4Spec.m // P4ObjectLayer // // Created by Michael Bishop on 4/25/11. // Copyright 2011 Numerical Garden, LLC. All rights reserved. // #import "P4Spec.h" #import "P4Port.h" #import "P4Response.h" #import "P4SpecManager.h" #import "P4SpecManager_p.h" #import "P4SpecDescription.h" #import "P4SpecEntityDescriptionAdditions.h" #import "NGAUtilities.h" NSString * const kP4DateFormatString = @"yyyy/MM/dd HH:mm:ss"; NSString * const kP4IdentifierKey = @"identifier"; NSString * const kP4PeekKeySuffix = @"Peek"; static const NSTimeInterval kRefreshRate = 30.0 * 60.0; // 30 minutes @interface P4Spec () -(NSString*)identifierPropertyName; -(NSDictionary*)changedKeysAndValuesFromData:(NSDictionary*)data; -(void)updateWithPropertyData:(NSDictionary*)data; -(void)updateWithChangedPropertyData:(NSDictionary*)changedValuesAndKey; -(void)invalidateFetchedPropertiesForEntity:(NSEntityDescription*)entity property:(NSString*)key; -(NSSet*)keysForConflictingValues; -(BOOL)keyIsReadOnly:(NSString*)key; -(P4SpecFieldDescription*)specFieldDescriptionForKey:(NSString*)key; //@property (readwrite, copy) NSString * type; @property (readwrite, retain) NSDictionary * lastRefreshDatesByProperty; @property (readwrite, retain) NSSet * summarizedPropertyKeys; @property (readwrite, retain) NSDictionary * baseFields; @property (readwrite, retain) NSDictionary * theirsFields; @end @implementation P4Spec // These are made by the NSEntityDescription @dynamic lastRefreshDatesByProperty, summarizedPropertyKeys, baseFields, theirsFields; @synthesize type = _type ; +(NSDateFormatter*)sharedPerforceDateFormatter { static dispatch_once_t pred; static NSDateFormatter *p4DateFormatter = nil; dispatch_once(&pred, ^{ p4DateFormatter = [[NSDateFormatter alloc] init]; [p4DateFormatter setDateFormat:kP4DateFormatString]; }); return p4DateFormatter; } +(NSDateFormatter*)sharedTimeZoneCompliantPerforceDateFormatter { static dispatch_once_t pred; static NSDateFormatter *p4DateFormatter = nil; dispatch_once(&pred, ^{ p4DateFormatter = [[NSDateFormatter alloc] init]; [p4DateFormatter setDateFormat:[kP4DateFormatString stringByAppendingFormat:@" ZZZ"]]; }); return p4DateFormatter; } -(void)awakeFromFetch { [super awakeFromFetch]; _completionBlocksForRefresh = [NSMutableArray new]; _completionBlocksForSave = [NSMutableArray new]; _fetchedPropertyCache = [NSMutableDictionary new]; } -(void)awakeFromInsert { [super awakeFromInsert]; _completionBlocksForRefresh = [NSMutableArray new]; _completionBlocksForSave = [NSMutableArray new]; _fetchedPropertyCache = [NSMutableDictionary new]; } -(void)didTurnIntoFault { RELEASE(_completionBlocksForRefresh); RELEASE(_completionBlocksForSave); RELEASE(_mappedSpecKeysCache); RELEASE(_fetchedPropertyCache); } -(void)dealloc { RELEASE(_type); RELEASE(_completionBlocksForRefresh); RELEASE(_completionBlocksForSave); RELEASE(_mappedSpecKeysCache); RELEASE(_fetchedPropertyCache); [super dealloc]; } -(NSString*)description { return [self form]; } -(P4SpecManager*)manager { return [[self entity] manager]; } -(P4Connection*)connection { return [[[self entity] manager] connection]; } -(NSString*)identifierPropertyName { return [[self entity] identifierPropertyName]; } -(NSString*)lastAccessedPropertyName { return [[self entity] lastAccessedPropertyName]; } -(NSArray*)inverseFetchedProperties { return [[self entity] inverseFetchedProperties]; } -(NSDictionary*)fetchedPropertyDescriptions { return [[self entity] fetchedPropertyDescriptions]; } -(NSString*)type { if (!_type) _type = [[[self entity] type] retain]; return _type; } -(NSString*)identifier { NSString * identifier = [self identifierPropertyName]; return [self primitiveValueForKey:identifier]; } -(NSString*)specTagForKey:(NSString*)key { return [[self entity] specTagForPropertyName:key]; } -(P4SpecFieldDescription*)specFieldDescriptionForKey:(NSString*)key { return [[[self entity] specDescription] fieldForTag:[self specTagForKey:key]]; } -(BOOL)keyIsReadOnly:(NSString*)key { // We need to add many to this based on the spec description return [[self specFieldDescriptionForKey:key] isReadOnly]; } -(NSString*)form { NSSet * specKeys = [[self entity] allMappedSpecKeys]; NSMutableDictionary * contents = [NSMutableDictionary dictionaryWithCapacity:[specKeys count]]; for (NSString * key in specKeys) { if ( [self keyIsReadOnly:key] && ![key isEqualToString:[self identifierPropertyName]] ) continue; id value = [self valueForKey:key]; NSString * specTag = [[self entity] specTagForPropertyName:key]; if ( value ) { if ( [value isKindOfClass:[NSDate class]] ) value = [[[self class] sharedTimeZoneCompliantPerforceDateFormatter] stringFromDate:value]; [contents setObject:value forKey:specTag]; } } return [[[self entity] specDescription] formFromSpecProperties:contents]; } -(NSDate*)lastAccessedDate { id identifier = [self lastAccessedPropertyName]; return [self primitiveValueForKey:identifier]; } -(NSSet*)allMappedSpecKeys { if (_mappedSpecKeysCache) return _mappedSpecKeysCache; _mappedSpecKeysCache = [[[self entity] allMappedSpecKeys] retain]; return _mappedSpecKeysCache; } -(NSString*)propertyKeyForSpecKey:(NSString*)key { NSDictionary * mapping = [[self entity] specKeyToMOKeyMapping]; id mappedKey = [mapping objectForKey:key]; if ( !mappedKey ) mappedKey = key; return mappedKey; } // converts all the keys from the server data returned into keys // for this entity -(NSDictionary*)propertyDataFromRawData:(NSDictionary*)transformedData { NSMutableDictionary * propertyData = [NSMutableDictionary dictionaryWithCapacity:[transformedData count]]; for (NSString * specKey in transformedData) { NSString * key = [self propertyKeyForSpecKey:specKey]; [propertyData setObject:[transformedData objectForKey:specKey] forKey:key]; } return propertyData; } -(NSDictionary*)changedKeysAndValuesFromData:(NSDictionary*)propertyData { NSEntityDescription * entityDescription = [self entity]; NSDictionary * attributes = [entityDescription attributesByName]; // First find all the attributes that aren't different NSMutableDictionary * changedAttributes = [NSMutableDictionary dictionaryWithCapacity:[propertyData count]]; NSMutableDictionary * theirsFields = nil; for (NSString * dataKey in propertyData) { NSAttributeDescription * attributeDescription = [attributes objectForKey:dataKey]; if ( !attributeDescription ) continue; id value = [propertyData objectForKey:dataKey]; if ( value == [NSNull null] ) value = nil; if ( value && [attributeDescription attributeType] == NSDateAttributeType ) { // 2010/07/06 12:55:23 this comes in the -o versions of the spec // First, try to process it as a textual date, appending // the server's time-zone to make the time absolute NSString * fullDateString = [value stringByAppendingFormat:@" %@", [[[self manager] p4Port] GMTOffsetString]]; NSDate * date = [[[self class] sharedTimeZoneCompliantPerforceDateFormatter] dateFromString:fullDateString]; // The date was probably formatted in seconds (ex: "1542534") if (!date) date = [NSDate dateWithTimeIntervalSince1970:[value intValue]]; value = date; } // if there is an base key, compare against that. If they are not // equal, save it as a "theirs" key. Do not overwrite the local value. // // If we do a merge, it will look like this // base // / \ // modified theirs id editedValue = nil; if ((editedValue = [self.baseFields objectForKey:dataKey])) { if ( ![editedValue isEqualTo:value] ) { if ( !theirsFields ) theirsFields = [NSMutableDictionary dictionaryWithDictionary:self.theirsFields]; [theirsFields setObject:value forKey:dataKey]; } continue; } if ( ![value isEqualTo:[self primitiveValueForKey:dataKey]] ) [changedAttributes setObject:value forKey:dataKey]; } if ( theirsFields ) self.theirsFields = theirsFields; return changedAttributes; } -(void)willUpdateWithData:(NSDictionary*)data { // LOG_DEBUG( @"Updating '%@' with data:%@", self.identifier, data ); _isUpdatingFromRetrievedServerData = YES; } -(void)didUpdateFromData:(NSDictionary*)data { _isUpdatingFromRetrievedServerData = NO; [self.manager requestSave]; } -(void)setRefreshDateToNowForKeys:(id)keys { NSMutableDictionary * refreshDates = [NSMutableDictionary dictionaryWithDictionary:self.lastRefreshDatesByProperty]; NSDate * now = [NSDate date]; for ( NSString * key in keys ) [refreshDates setObject:now forKey:key]; self.lastRefreshDatesByProperty = refreshDates; [self.manager requestSave]; } -(NSDictionary*)summarizedDataFromRawData:(NSDictionary*)rawData { NSDictionary * summaryPropertyDescriptions = [[self entity] summaryPropertyDescriptions]; if ( !summaryPropertyDescriptions ) return nil; NSMutableDictionary * summarizedData = nil; for (NSString * summarizingPropertyName in [summaryPropertyDescriptions allKeys]) { // first see if the raw data contains summarizing properties id propertyData = [rawData objectForKey:summarizingPropertyName]; if ( !propertyData ) continue; // Get the associated info NSDictionary * summarizingPropertyInfo = [summaryPropertyDescriptions objectForKey:summarizingPropertyName]; if (!summarizedData) summarizedData = [NSMutableDictionary dictionary]; // The key in the info is the *spec* key, so we have // to translate it into our property key NSString * summarizedPropertyName = [self propertyKeyForSpecKey:[summarizingPropertyInfo objectForKey:@"summarizes"]]; [summarizedData setObject:propertyData forKey:summarizedPropertyName]; } return summarizedData; } -(void)updateWithRawData:(NSDictionary*)rawData { // LOG_DEBUG(@"Received the data: %@", rawData); // update summarized properties, but only if this entity supports them NSDictionary * data = [self propertyDataFromRawData:rawData]; if ( [[self entity] summaryPropertyDescriptions] ) { NSDictionary * summarizedData = [self summarizedDataFromRawData:rawData]; if ( summarizedData ) { NSMutableDictionary * summarizedMutableData = [[summarizedData mutableCopy] autorelease]; for (NSString * key in [summarizedMutableData allKeys]) { // If there is already non-summarized data, we ignore the summarized data if ( ![self.summarizedPropertyKeys containsObject:key] && [self primitiveValueForKey:key] ) [summarizedMutableData removeObjectForKey:key]; } NSMutableDictionary * mutableData = [[data mutableCopy] autorelease]; [mutableData addEntriesFromDictionary:summarizedMutableData]; data = mutableData; // update the summarized properties NSMutableSet * mutableSummarizedKeys = [NSMutableSet setWithSet:self.summarizedPropertyKeys]; [mutableSummarizedKeys addObjectsFromArray:[summarizedMutableData allKeys]]; self.summarizedPropertyKeys = mutableSummarizedKeys; } else { NSMutableSet * mutableSummarizedKeys = [self.summarizedPropertyKeys mutableCopy]; [mutableSummarizedKeys minusSet:[NSSet setWithArray:[data allKeys]]]; self.summarizedPropertyKeys = mutableSummarizedKeys; [mutableSummarizedKeys release]; } } [self updateWithPropertyData:data]; } -(void)updateWithPropertyData:(NSDictionary*)data { // It's important that we update the refresh dates of all the keys we // received even if they didn't change. [self setRefreshDateToNowForKeys:[data allKeys]]; [self updateWithChangedPropertyData:[self changedKeysAndValuesFromData:data]]; } -(void)updateWithChangedPropertyData:(NSDictionary*)changedValuesAndKey { // LOG_DEBUG(@"Setting the data: %@", data); if ( [changedValuesAndKey count] == 0 ) return; // Dependencies on the basic keys. We have two: NSMutableSet * allChangedKeys = [NSMutableSet setWithArray:[changedValuesAndKey allKeys]]; // First, all the keys with "Peek" added to the end for (NSString * key in [changedValuesAndKey allKeys]) [allChangedKeys addObject:[key stringByAppendingString:kP4PeekKeySuffix]]; // Second, the "identifier" key if ([[changedValuesAndKey allKeys] containsObject:[self identifierPropertyName]]) { [allChangedKeys addObject:kP4IdentifierKey]; [allChangedKeys addObject:[kP4IdentifierKey stringByAppendingString:kP4PeekKeySuffix]]; } [self willUpdateWithData:changedValuesAndKey]; [self notifyChangesToKeyValues:allChangedKeys occuringInBlock:^(id unretainedSelf){ // Change the values for (NSString * attributeKey in [changedValuesAndKey allKeys]) [unretainedSelf setPrimitiveValue:[changedValuesAndKey objectForKey:attributeKey] forKey:attributeKey]; }]; [self didUpdateFromData:changedValuesAndKey]; // Check to see if we invalidated a fetched property for (NSDictionary * inverseFetchedProperty in [self inverseFetchedProperties]) { NSString * inverseFetchedPropertyRelationshipName = [inverseFetchedProperty objectForKey:kP4InverseFetchedPropertyRelationshipName]; NSString * inverseFetchedPropertyRelationshipSpecType = [inverseFetchedProperty objectForKey:kP4InverseFetchedPropertyRelationshipSpecType]; NSString * identifierToBeUpdated = [changedValuesAndKey objectForKey:inverseFetchedPropertyRelationshipName]; if (identifierToBeUpdated) { P4Spec * dirtySpec = [[self manager] specOfType:inverseFetchedPropertyRelationshipSpecType identifier:identifierToBeUpdated createIfNotFound:NO]; [dirtySpec invalidateFetchedPropertiesForEntity:[self entity] property:inverseFetchedPropertyRelationshipName]; } } } -(void)invalidateFetchedPropertiesForEntity:(NSEntityDescription*)entity property:(NSString*)key { // find the property that depends on the property NSString * typeName = [[entity userInfo] objectForKey:kP4SpecTypeKey]; NSArray * auxData = [[[[[self manager] class] auxiliarySpecMetadata] objectForKey:[self type]] objectForKey:kP4FetchedRelationships]; for (NSDictionary * fetchedProperty in auxData) { NSString * fetchedSpecType = [fetchedProperty objectForKey:kP4FetchedRelationshipDestinationSpecType]; NSString * inverseRelationshipName = [fetchedProperty objectForKey:kP4FetchedRelationshipDestinationInverseName]; if ( ![fetchedSpecType isEqualToString:typeName] ) continue; if ( ![inverseRelationshipName isEqualToString:key] ) continue; NSString * fetchedRelationshipName = [fetchedProperty objectForKey:kP4FetchedRelationshipName]; NSMutableDictionary * fetchedPropertyData = [_fetchedPropertyCache objectForKey:fetchedRelationshipName]; if ( !fetchedPropertyData ) continue; [fetchedPropertyData removeObjectForKey:kNGAFetchedPropertyFetchDate]; [_fetchedPropertyCache setObject:fetchedPropertyData forKey:fetchedRelationshipName]; } } -(void)refresh:(id)sender { _isRefreshingFromServer++; NSArray * arguments = [NSArray arrayWithObjects:self.type, @"-o", self.identifier, nil]; [self.manager runArguments:arguments updateBlock:nil completionBlock:^(P4Response * response) { _isRefreshingFromServer--; if ( !response.error ) { // LOG_DEBUG( @"Refreshing with Data: %@", response.result); [self updateWithRawData:response.result]; // Because we did a <typename> -o, we are going to update // every property in the spec, even if we didn't get it back // This is because the server doesn't send back a property // if it has a null value. That's different from a property // simply non-existing. We can't tell the difference. // An additional way to handle this would be to supplement // the results with [NSNull null] where there was a missing // key. That WON'T work because we can't actually set Null // values in CodeData unless the property data type is Null [self setRefreshDateToNowForKeys:[self allMappedSpecKeys]]; } else { LOG_ERROR( @"Error when refreshing %@ error: %@", arguments, response.error ); // [propertiesBeingLoaded removeAllObjects]; } for (SpecRefreshCompletionBlock completion in _completionBlocksForRefresh) completion( response.error ); [_completionBlocksForRefresh removeAllObjects]; }]; } -(void)refreshWithCompletion:(SpecRefreshCompletionBlock)completion { [_completionBlocksForRefresh addObject:[[completion copy] autorelease]]; // No one else has refreshed this yet if ( [_completionBlocksForRefresh count] == 1 ) [self refresh:self]; } -(void)save:(id)sender { // 1 - refresh to get most recent values // 2 - Merge any necessary changes // 3 - send to server // 4 - refresh again [self refreshWithCompletion:^(NSError * error) { if ( error ) { [NSApp presentError:error]; return; } NSSet * conflictingKeys = [self keysForConflictingValues]; if ( conflictingKeys ) { LOG_ERROR( @"These keys are changed by you and others. Saving the form will overwrite their changes. KEYS: %@", conflictingKeys ); } [self.manager saveSpec:self completionBlock:^(P4Response * response ) { if (response.error) { [NSApp presentError:response.error]; return; } self.baseFields = nil; self.theirsFields = nil; for (SpecRefreshCompletionBlock completion in _completionBlocksForSave) completion( response.error ); [_completionBlocksForSave removeAllObjects]; [self refresh:self]; }]; }]; } -(void)saveWithCompletion:(SpecRefreshCompletionBlock)completion { [_completionBlocksForSave addObject:[[completion copy] autorelease]]; // No one else has saved this yet if ( [_completionBlocksForSave count] == 1 ) [self save:self]; } -(NSDate*)lastRefreshOfKey:(NSString*)key { NSDate * date = [self.lastRefreshDatesByProperty objectForKey:key]; if ( !date ) date = [NSDate dateWithTimeIntervalSince1970:0]; return date; } -(NSTimeInterval)refreshInterval { // For older things, we have slower refresh rates. They are less likely // to have changed and you can always force a refresh with them NSDate * lastAccessedDate = [self lastAccessedDate]; NSDate * now = [NSDate date]; NSTimeInterval timeIntervalSinceLastAccess = [now timeIntervalSinceDate:lastAccessedDate]; if ( timeIntervalSinceLastAccess <= (60.0 * 60.0 * 24.0 * 7.0) ) // 1 week return kRefreshRate; if ( timeIntervalSinceLastAccess <= 60.0 * 60.0 * 24.0 * 7.0 * 4 ) // 1 month return kRefreshRate * 8.0; if ( timeIntervalSinceLastAccess <= 60.0 * 60.0 * 24.0 * 7.0 * 4 ) // 3 months return kRefreshRate * 16.0; if ( timeIntervalSinceLastAccess <= 60.0 * 60.0 * 24.0 * 7.0 * 4 ) // 6 months return kRefreshRate * 32.0; return kRefreshRate * 2.0 * 48.0; } #pragma mark MAGIC +(NSSet*)keyPathsForValuesAffectingValueForDirty { return [NSSet setWithObject:kSpecPropertyNameBaseFieldValues]; } -(BOOL)isDirty { return (self.baseFields != nil); } -(NSSet*)keysForConflictingValues { NSDictionary * theirsFields = self.theirsFields; if ( !theirsFields ) return nil; NSMutableSet * conflictingKeys = [NSMutableSet setWithCapacity:[theirsFields count]]; for (NSString * key in theirsFields) { if ( ![[theirsFields objectForKey:key] isEqualTo:[self valueForKey:key]] ) [conflictingKeys addObject:key]; } return conflictingKeys; } -(void)revert:(id)sender { if ( !self.isDirty ) return; NSMutableDictionary * revertedFields = [NSMutableDictionary dictionaryWithDictionary:self.baseFields]; // Create a dictionary with the values to which we should revert. // prefer the "Theirs" values for (NSString * key in [revertedFields allKeys]) { id value = [self.theirsFields objectForKey:key]; if (value) [revertedFields setValue:value forKey:key]; } self.baseFields = nil; self.theirsFields = nil; // revert to the values [self updateWithChangedPropertyData:revertedFields]; [self refresh:self]; } -(NSString*)actualKeyFromPeekKey:(NSString*)peekKey { // This code allows you to "peek" at spec values without triggering a // reload from the server. Just specify the property and append "Peek" to // the name. You'll receive the current value in the store without // retriggering a fetch. You can also observe "Peek" versions of the // properties without triggering server fetches. NSRange peekRange = [peekKey rangeOfString:kP4PeekKeySuffix options:NSBackwardsSearch|NSAnchoredSearch]; if ( peekRange.location == NSNotFound ) return nil; if ( peekRange.location + peekRange.length != [peekKey length] ) return nil; NSString * actualKey = [peekKey substringToIndex:peekRange.location]; if ( [actualKey isEqualToString:kSpecPropertyNameIdentifier]) return [self identifierPropertyName]; return actualKey; } - (void)setValue:(id)value forUndefinedKey:(NSString *)key { NSString * actualKey = nil; if ( (actualKey = [self actualKeyFromPeekKey:key]) == nil ) { [super setValue:value forUndefinedKey:key]; return; } // Doesn't go through willAccessValue for Key and doesn't trigger a refresh [self willChangeValueForKey:actualKey]; [super setPrimitiveValue:value forKey:actualKey]; [self didChangeValueForKey:actualKey]; } -(id)valueForUndefinedKey:(NSString *)key { // LOG_DEBUG(@"accessing undefined key: %@", key); NSFetchedPropertyDescription * fetchedPropertyDescription = [[self fetchedPropertyDescriptions] objectForKey:key]; if ( fetchedPropertyDescription ) { NSMutableDictionary * fetchedProperty = [_fetchedPropertyCache objectForKey:key]; if ( !fetchedProperty ) fetchedProperty = [NSMutableDictionary dictionaryWithCapacity:2]; NSDate * lastRefreshDate = [fetchedProperty objectForKey:kNGAFetchedPropertyFetchDate]; if ( !lastRefreshDate || [lastRefreshDate timeIntervalSinceNow] > [[self managedObjectContext] stalenessInterval] ) { NSFetchRequest * fetchRequest = [[[fetchedPropertyDescription fetchRequest] copy] autorelease]; NSPredicate * predicate = [[fetchRequest predicate] predicateWithSubstitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:self, @"FETCH_SOURCE", fetchedPropertyDescription, @"FETCHED_PROPERTY", nil]]; [fetchRequest setPredicate:predicate]; NSError * error; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; [fetchedProperty setObject:fetchResults forKey:kNGAFetchedPropertyFetchData]; [fetchedProperty setObject:[NSDate date] forKey:kNGAFetchedPropertyFetchDate]; [_fetchedPropertyCache setObject:fetchedProperty forKey:key]; } return [fetchedProperty objectForKey:kNGAFetchedPropertyFetchData]; } NSString * actualKey = nil; // Doesn't go through willAccessValue for Key and doesn't trigger a refresh if ( (actualKey = [self actualKeyFromPeekKey:key]) ) return [super primitiveValueForKey:actualKey]; return [super valueForUndefinedKey:key]; } -(void)willChangeValueForKey:(NSString*)key { [self.manager requestSave]; if ( _isUpdatingFromRetrievedServerData || ![[self allMappedSpecKeys] containsObject:key] || [key isEqualToString:kSpecPropertyNameBaseFieldValues] ) { [super willChangeValueForKey:key]; return; } // At this point, we know we are changing a key for the spec AND we are // doing it NOT because we are updating our data from the server id baseFields = [self baseFields]; id originalValue = [baseFields objectForKey:key]; if ( originalValue ) { [super willChangeValueForKey:key]; return; } NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithDictionary:baseFields]; originalValue = [self primitiveValueForKey:key]; if ( originalValue ) [dict setObject:originalValue forKey:key]; [self setBaseFields:dict]; } -(void)willAccessValueForKey:(NSString*)key { [super willAccessValueForKey:key]; // LOG_DEBUG(@"accessing key: %@", key); // We check this because if we are being accessed because we are updating // ourself because of data we received from the server, we definitely // don't want to go back out to the server again see: updateWithRawData: if ( _isUpdatingFromRetrievedServerData ) return; if ( _isRefreshingFromServer > 0 ) return; // Ignore non spec keys. We can't get them from the server anyway if ( [key isEqualToString:kSpecPropertyNameType] || [key isEqualToString:kSpecPropertyNameIdentifier] || [key isEqualToString:kSpecPropertyNameDateRefreshed] || [key isEqualToString:[self identifierPropertyName]] ) return; if ( ![[self allMappedSpecKeys] containsObject:key] ) return; if ( [[NSDate date] timeIntervalSinceDate:[self lastRefreshOfKey:key]] > [self refreshInterval] ) { // LOG_DEBUG( @"Loading key '%@' of %@(%@) because %f > %f ", key, self.identifier, self.type, [[NSDate date] timeIntervalSinceDate:[self lastRefreshOfKey:key]], [self refreshInterval] ); [self refresh:self]; } if ( [[self entity] summaryPropertyDescriptions] ) { if ( [self.summarizedPropertyKeys containsObject:key] ) { // LOG_DEBUG( @"Loading key '%@' of %@(%@) because we only have summary data ", key, self.identifier, self.type ); [self refresh:self]; } } } @end
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 20722 | jdputsch | initial branch, prep for -Zapp= support | ||
//guest/michael_bishop/MacMenu/src/P4ObjectLayer/P4Spec.m | |||||
#1 | 8331 | Matt Attaway |
Adding initial version of MacMenu for Perforce MacMenu is a helpful Perforce client that sits in your toolbar. It allows you to run standard Perforce operations on the document that is open the currently active editor/viewer. |