2017-07-12 21:43:18 +08:00
//
// FlycutOperator . m
// Flycut
//
2017-07-12 21:43:44 +08:00
// Flycut by Gennadiy Potapov and contributors . Based on Jumpcut by Steve Cook .
// Copyright 2011 General Arcade . All rights reserved .
2017-07-12 21:43:18 +08:00
//
2017-07-12 21:43:44 +08:00
// This code is open - source software subject to the MIT License ; see the homepage
// at < https : // github . com / TermiT / Flycut > for details .
2017-07-12 21:43:18 +08:00
//
2017-07-12 21:43:44 +08:00
// FlycutOperator owns and interacts with the FlycutStores , providing
// manipulation of the stores .
2017-07-12 21:43:18 +08:00
# import < Foundation / Foundation . h >
2017-07-12 21:43:44 +08:00
# import "FlycutOperator.h"
2017-09-03 11:13:52 +08:00
# import "MJCloudKitUserDefaultsSync.h"
2017-07-12 21:43:44 +08:00
@ implementation FlycutOperator
- ( id ) init
{
[ [ NSUserDefaults standardUserDefaults ] registerDefaults : [ NSDictionary dictionaryWithObjectsAndKeys :
[ NSNumber numberWithInt : 40 ] ,
@ "rememberNum" ,
[ NSNumber numberWithInt : 40 ] ,
@ "favoritesRememberNum" ,
[ NSNumber numberWithInt : 1 ] ,
@ "savePreference" ,
[ NSDictionary dictionary ] ,
@ "store" ,
[ NSNumber numberWithBool : YES ] ,
@ "skipPasswordFields" ,
[ NSNumber numberWithBool : YES ] ,
@ "skipPboardTypes" ,
@ "PasswordPboardType" ,
@ "skipPboardTypesList" ,
[ NSNumber numberWithBool : NO ] ,
@ "skipPasswordLengths" ,
@ "12, 20, 32" ,
@ "skipPasswordLengthsList" ,
[ NSNumber numberWithBool : NO ] ,
@ "revealPasteboardTypes" ,
2017-08-22 04:55:00 +08:00
[ NSNumber numberWithBool : YES ] , // do not commit with YES . Use NO
2017-07-12 21:43:44 +08:00
@ "removeDuplicates" ,
2017-08-22 04:55:00 +08:00
[ NSNumber numberWithBool : YES ] , // do not commit with YES . Use NO
2017-07-12 21:43:44 +08:00
@ "pasteMovesToTop" ,
2017-09-03 11:13:52 +08:00
[ NSNumber numberWithBool : NO ] ,
@ "syncSettingsViaICloud" ,
[ NSNumber numberWithBool : NO ] ,
@ "syncClippingsViaICloud" ,
2017-07-12 21:43:44 +08:00
nil ] ] ;
2017-09-03 11:13:52 +08:00
settingsSyncList = @ [ @ "rememberNum" ,
@ "favoritesRememberNum" ,
@ "savePreference" ,
@ "skipPasswordFields" ,
@ "skipPboardTypes" ,
@ "skipPboardTypesList" ,
@ "skipPasswordLengths" ,
@ "skipPasswordLengthsList" ,
@ "removeDuplicates" ,
@ "pasteMovesToTop" ] ;
[ settingsSyncList retain ] ;
2017-07-12 21:43:44 +08:00
return self ;
}
2017-09-03 11:13:52 +08:00
- ( void ) awakeFromNibDisplaying : ( int ) dispNum withDisplayLength : ( int ) dispLength withSaveSelector : ( SEL ) selector forTarget : ( NSObject * ) target
2017-07-12 21:43:44 +08:00
{
2017-09-03 11:13:52 +08:00
displayNum = dispNum ;
displayLength = dispLength ;
2017-08-27 12:23:20 +08:00
saveSelector = selector ;
saveTarget = target ;
2017-07-12 21:43:44 +08:00
2017-09-03 11:13:52 +08:00
// Initialize the FlycutStore
[ self initializeStoresAndLoadContents ] ;
2017-07-12 21:43:44 +08:00
// Stack position starts @ 0 by default
stackPosition = favoritesStackPosition = stashedStackPosition = 0 ;
2017-09-03 11:13:52 +08:00
[ self registerOrDeregisterICloudSync ] ;
}
- ( FlycutStore * ) allocInitFlycutStoreRemembering : ( int ) remembering
{
return [ [ FlycutStore alloc ] initRemembering : remembering
displaying : displayNum
withDisplayLength : displayLength ] ;
}
- ( void ) initializeStores
{
// Fixme - These stores are not released anywhere .
if ( ! clippingStore )
2017-10-20 03:41:24 +08:00
{
2017-09-03 11:13:52 +08:00
clippingStore = [ self allocInitFlycutStoreRemembering : [ [ NSUserDefaults standardUserDefaults ] integerForKey : @ "rememberNum" ] ] ;
2017-10-20 03:41:24 +08:00
clippingStore . deleteDelegate = self ;
}
2017-09-03 11:13:52 +08:00
else
{
[ clippingStore setDisplayNum : displayNum ] ;
[ clippingStore setDisplayLen : displayLength ] ;
}
if ( ! favoritesStore )
2017-10-20 03:41:24 +08:00
{
2017-09-03 11:13:52 +08:00
favoritesStore = [ self allocInitFlycutStoreRemembering : [ [ NSUserDefaults standardUserDefaults ] integerForKey : @ "favoritesRememberNum" ] ] ;
2017-10-20 03:41:24 +08:00
favoritesStore . deleteDelegate = self ;
}
2017-09-03 11:13:52 +08:00
else
{
[ favoritesStore setDisplayNum : displayNum ] ;
[ favoritesStore setDisplayLen : displayLength ] ;
}
stashedStore = NULL ;
}
- ( void ) initializeStoresAndLoadContents
{
[ self initializeStores ] ;
// If our preferences indicate that we are saving , load the dictionary from the saved plist
// and use it to get everything set up .
if ( [ [ NSUserDefaults standardUserDefaults ] integerForKey : @ "savePreference" ] >= 1 ) {
[ self loadEngineFromPList ] ;
}
2017-07-12 21:43:44 +08:00
}
2017-10-31 21:58:34 +08:00
- ( void ) willShowPreferences
2017-07-12 21:43:44 +08:00
{
2017-10-31 21:58:34 +08:00
issuedRememberResizeWarning = NO ;
}
- ( int ) setRememberNum : ( int ) newRemember forPrimaryStore : ( BOOL ) isPrimaryStore
{
int oldRemeber = [ self rememberNum ] ;
// Ensure that we don ' t remember zero or fewer clippings .
if ( newRemember <= 0 )
{
newRemember = oldRemeber ;
if ( newRemember <= 0 )
newRemember = 40 ;
}
if ( newRemember < [ self jcListCount ] &&
! issuedRememberResizeWarning &&
! [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "stifleRememberResizeWarning" ]
) {
NSString * choice = [ self delegateAlertWithMessageText : @ "Resize Stack"
informationText : @ "Resizing the stack to a value below its present size will cause clippings to be lost."
buttonsTexts : @ [ @ "Resize" , @ "Cancel" , @ "Don't Warn Me Again" ] ] ;
if ( [ choice isEqualToString : @ "Cancel" ] ) {
// Cancel - Change to prior setting .
newRemember = oldRemeber ;
if ( isPrimaryStore ) {
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithInt : newRemember ]
forKey : @ "rememberNum" ] ;
} else {
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithInt : newRemember ]
forKey : @ "favoritesRememberNum" ] ;
}
} else if ( [ choice isEqualToString : @ "Don't Warn Me Again" ] ) {
// Don ' t Warn Me Again
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithBool : YES ]
forKey : @ "stifleRememberResizeWarning" ] ;
} else {
// Resize
issuedRememberResizeWarning = YES ;
}
}
// Set the value .
2017-07-12 21:43:44 +08:00
[ clippingStore setRememberNum : newRemember ] ;
2017-10-31 21:58:34 +08:00
return newRemember ;
2017-07-12 21:43:44 +08:00
}
- ( void ) toggleToFromFavoritesStore
{
if ( NULL ! = stashedStore )
[ self restoreStashedStore ] ;
else
[ self switchToFavoritesStore ] ;
}
- ( bool ) favoritesStoreIsSelected
{
return clippingStore = = favoritesStore ;
}
- ( void ) switchToFavoritesStore
{
stashedStore = clippingStore ;
clippingStore = favoritesStore ;
stashedStackPosition = stackPosition ;
stackPosition = favoritesStackPosition ;
}
- ( bool ) restoreStashedStore
{
if ( NULL ! = stashedStore )
{
clippingStore = stashedStore ;
stashedStore = NULL ;
favoritesStackPosition = stackPosition ;
stackPosition = stashedStackPosition ;
return YES ;
}
return NO ;
}
- ( NSString * ) getPasteFromStackPosition
{
if ( [ clippingStore jcListCount ] > stackPosition ) {
return [ self getPasteFromIndex : stackPosition ] ;
}
return nil ;
}
- ( bool ) saveFromStack
{
return [ self saveFromStackWithPrefix : @ "" ] ;
}
- ( bool ) saveFromStackWithPrefix : ( NSString * ) prefix
{
2017-10-20 03:41:24 +08:00
return [ self saveFromStore : clippingStore atIndex : stackPosition withPrefix : prefix ] ;
}
- ( bool ) saveFromStore : ( FlycutStore * ) store atIndex : ( int ) index withPrefix : ( NSString * ) prefix
{
if ( [ store jcListCount ] > index ) {
2017-07-12 21:43:44 +08:00
// Get text from clipping store .
2017-10-20 03:41:24 +08:00
NSString * pbFullText = [ self clippingStringWithCount : index inStore : store ] ;
2017-07-12 21:43:44 +08:00
pbFullText = [ pbFullText stringByReplacingOccurrencesOfString : @ "\r" withString : @ "\r\n" ] ;
// Get the Desktop directory :
NSArray * paths = NSSearchPathForDirectoriesInDomains
( NSDesktopDirectory , NSUserDomainMask , YES ) ;
NSString * desktopDirectory = [ paths objectAtIndex : 0 ] ;
// Get the timestamp string :
NSDate * currentDate = [ NSDate date ] ;
NSDateFormatter * dateFormatter = [ [ NSDateFormatter alloc ] init ] ;
[ dateFormatter setDateFormat : @ "YYYY-MM-dd 'at' HH.mm.ss" ] ;
NSString * dateString = [ dateFormatter stringFromDate : currentDate ] ;
// Make a file name to write the data to using the Desktop directory :
NSString * fileName = [ NSString stringWithFormat : @ "%@/%@%@Clipping %@.txt" ,
2017-10-20 03:41:24 +08:00
desktopDirectory , prefix , store = = favoritesStore ? @ "Favorite " : @ "" , dateString ] ;
2017-07-12 21:43:44 +08:00
// Save content to the file
[ pbFullText writeToFile : fileName
atomically : NO
encoding : NSNonLossyASCIIStringEncoding
error : nil ] ;
return YES ;
}
return NO ;
}
- ( bool ) saveFromStackToFavorites
{
if ( clippingStore ! = favoritesStore && [ clippingStore jcListCount ] > stackPosition ) {
// Get text from clipping store .
[ favoritesStore addClipping : [ clippingStore clippingAtPosition : stackPosition ] ] ;
2017-09-03 11:13:52 +08:00
[ self clearItemAtStackPosition ] ;
2017-07-12 21:43:44 +08:00
return YES ;
}
return NO ;
}
- ( NSString * ) getPasteFromIndex : ( int ) position {
NSString * clipping = [ self getClipFromCount : position ] ;
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "pasteMovesToTop" ] ) {
[ clippingStore clippingMoveToTop : position ] ;
stackPosition = 0 ;
2017-09-03 11:13:52 +08:00
[ self actionAfterListModification ] ;
2017-07-12 21:43:44 +08:00
}
return clipping ;
}
- ( NSString * ) getClipFromCount : ( int ) indexInt
{
NSString * pbFullText ;
NSArray * pbTypes ;
if ( ( indexInt + 1 ) > [ clippingStore jcListCount ] ) {
// We ' re asking for a clipping that isn ' t there yet
// This only tends to happen immediately on startup when not saving , as the entire list is empty .
DLog ( @ "Out of bounds request to jcList ignored." ) ;
return nil ;
}
return [ self clippingStringWithCount : indexInt ] ;
}
- ( BOOL ) shouldSkip : ( NSString * ) contents ofType : ( NSString * ) type
{
// Check to see if we are skipping passwords based on length and characters .
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "skipPasswordFields" ] )
{
// Check to see if they want a little help figuring out what types to enter .
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "revealPasteboardTypes" ] )
[ clippingStore addClipping : type ofType : type fromAppLocalizedName : @ "Flycut" fromAppBundleURL : nil atTimestamp : 0 ] ;
2017-09-03 11:13:52 +08:00
[ self actionAfterListModification ] ;
2017-07-12 21:43:44 +08:00
__block bool skipClipping = NO ;
// Check the array of types to skip .
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "skipPboardTypes" ] )
{
NSArray * typesArray = [ [ [ [ NSUserDefaults standardUserDefaults ] stringForKey : @ "skipPboardTypesList" ] stringByReplacingOccurrencesOfString : @ " " withString : @ "" ] componentsSeparatedByString : @ "," ] ;
[ typesArray enumerateObjectsUsingBlock : ^ ( id typeString , NSUInteger idx , BOOL * stop )
{
if ( [ type isEqualToString : typeString ] )
{
skipClipping = YES ;
* stop = YES ;
}
} ] ;
}
if ( skipClipping )
return YES ;
// Check the array of lengths to skip for suspicious strings .
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "skipPasswordLengths" ] )
{
int contentsLength = [ contents length ] ;
NSArray * lengthsArray = [ [ [ [ NSUserDefaults standardUserDefaults ] stringForKey : @ "skipPasswordLengthsList" ] stringByReplacingOccurrencesOfString : @ " " withString : @ "" ] componentsSeparatedByString : @ "," ] ;
[ lengthsArray enumerateObjectsUsingBlock : ^ ( id lengthString , NSUInteger idx , BOOL * stop )
{
if ( [ lengthString integerValue ] = = contentsLength )
{
NSRange uppercaseLetter = [ contents rangeOfCharacterFromSet : [ NSCharacterSet uppercaseLetterCharacterSet ] ] ;
NSRange lowercaseLetter = [ contents rangeOfCharacterFromSet : [ NSCharacterSet lowercaseLetterCharacterSet ] ] ;
NSRange decimalDigit = [ contents rangeOfCharacterFromSet : [ NSCharacterSet decimalDigitCharacterSet ] ] ;
NSRange punctuation = [ contents rangeOfCharacterFromSet : [ NSCharacterSet punctuationCharacterSet ] ] ;
NSRange symbol = [ contents rangeOfCharacterFromSet : [ NSCharacterSet symbolCharacterSet ] ] ;
NSRange control = [ contents rangeOfCharacterFromSet : [ NSCharacterSet controlCharacterSet ] ] ;
NSRange illegal = [ contents rangeOfCharacterFromSet : [ NSCharacterSet illegalCharacterSet ] ] ;
NSRange whitespaceAndNewline = [ contents rangeOfCharacterFromSet : [ NSCharacterSet whitespaceAndNewlineCharacterSet ] ] ;
if ( NSNotFound = = control . location
&& NSNotFound = = illegal . location
&& NSNotFound = = whitespaceAndNewline . location
&& NSNotFound ! = uppercaseLetter . location
&& NSNotFound ! = lowercaseLetter . location
&& NSNotFound ! = decimalDigit . location
&& ( NSNotFound ! = punctuation . location
|| NSNotFound ! = symbol . location ) )
{
skipClipping = YES ;
* stop = YES ;
}
}
} ] ;
if ( skipClipping )
return YES ;
}
}
return NO ;
}
- ( void ) setDisableStoreTo : ( bool ) value
{
disableStore = value ;
}
- ( bool ) storeDisabled
{
return disableStore ;
}
2017-09-03 11:13:52 +08:00
- ( void ) setClippingsStoreDelegate : ( id < FlycutStoreDelegate > ) delegate
{
if ( ! clippingStore )
[ self initializeStores ] ;
clippingStore . delegate = delegate ;
}
- ( void ) setFavoritesStoreDelegate : ( id < FlycutStoreDelegate > ) delegate
{
if ( ! favoritesStore )
[ self initializeStores ] ;
favoritesStore . delegate = delegate ;
}
2017-07-28 13:58:44 +08:00
- ( int ) indexOfClipping : ( NSString * ) contents ofType : ( NSString * ) type fromApp : ( NSString * ) appName withAppBundleURL : ( NSString * ) bundleURL
{
return [ clippingStore indexOfClipping : contents
ofType : type
fromAppLocalizedName : appName
fromAppBundleURL : bundleURL
atTimestamp : [ [ NSDate date ] timeIntervalSince1970 ] ] ;
}
2017-07-12 21:43:44 +08:00
- ( bool ) addClipping : ( NSString * ) contents ofType : ( NSString * ) type fromApp : ( NSString * ) appName withAppBundleURL : ( NSString * ) bundleURL target : ( id ) selectorTarget clippingAddedSelector : ( SEL ) clippingAddedSelector
{
if ( [ clippingStore jcListCount ] = = 0 || ! [ contents isEqualToString : [ clippingStore clippingContentsAtPosition : 0 ] ] ) {
2017-07-28 13:58:44 +08:00
bool success = [ clippingStore addClipping : contents
ofType : type
fromAppLocalizedName : appName
fromAppBundleURL : bundleURL
atTimestamp : [ [ NSDate date ] timeIntervalSince1970 ] ] ;
2017-09-03 11:13:52 +08:00
2017-07-12 21:43:44 +08:00
// The below tracks our position down down down . . . Maybe as an option ?
// if ( [ clippingStore jcListCount ] > 1 ) stackPosition + + ;
stackPosition = 0 ;
[ selectorTarget performSelector : clippingAddedSelector ] ;
2017-09-03 11:13:52 +08:00
[ self actionAfterListModification ] ;
2017-07-12 21:43:44 +08:00
2017-07-28 13:58:44 +08:00
return success ;
2017-07-12 21:43:44 +08:00
}
return NO ;
}
2017-10-20 03:41:24 +08:00
- ( void ) willDeleteClippingFromStore : ( id ) store AtIndex : ( int ) index {
if ( ( ! inhibitAutosaveClippings ) // Avoid saving things that the user explicitly deletes .
&& ( store = = favoritesStore
? [ [ [ NSUserDefaults standardUserDefaults ] valueForKey : @ "saveForgottenFavorites" ] boolValue ]
: [ [ [ NSUserDefaults standardUserDefaults ] valueForKey : @ "saveForgottenClippings" ] boolValue ] ) )
{
// clipping is being removed , so save it before it gets lost .
// Set to last item , save , and restore position .
[ self saveFromStore : store atIndex : index withPrefix : @ "Autosave " ] ;
}
}
2017-09-03 11:13:52 +08:00
- ( void ) actionAfterListModification
{
if ( ! inhibitSaveEngineAfterListModification
&& [ [ NSUserDefaults standardUserDefaults ] integerForKey : @ "savePreference" ] >= 2 )
[ self saveEngine ] ;
}
2017-07-12 21:43:44 +08:00
- ( int ) jcListCount
{
return [ clippingStore jcListCount ] ;
}
2017-09-03 11:13:52 +08:00
- ( int ) rememberNum
{
return [ clippingStore rememberNum ] ;
}
2017-07-12 21:43:44 +08:00
- ( int ) stackPosition
{
return stackPosition ;
}
- ( bool ) setStackPositionToFirstItem
{
if ( [ clippingStore jcListCount ] > 0 ) {
stackPosition = 0 ;
return YES ;
}
return NO ;
}
- ( bool ) setStackPositionToLastItem
{
if ( [ clippingStore jcListCount ] > 0 ) {
stackPosition = [ clippingStore jcListCount ] - 1 ;
return YES ;
}
return NO ;
}
- ( bool ) setStackPositionToTenMoreRecent
{
if ( [ clippingStore jcListCount ] > 0 ) {
stackPosition = stackPosition - 10 ; if ( stackPosition < 0 ) stackPosition = 0 ;
return YES ;
}
return NO ;
}
- ( bool ) setStackPositionToTenLessRecent
{
if ( [ clippingStore jcListCount ] > 0 ) {
stackPosition = stackPosition + 10 ; if ( stackPosition >= [ clippingStore jcListCount ] ) stackPosition = [ clippingStore jcListCount ] - 1 ;
return YES ;
}
return NO ;
}
- ( bool ) clearItemAtStackPosition
{
if ( [ clippingStore jcListCount ] = = 0 )
return NO ;
2017-10-20 03:41:24 +08:00
inhibitAutosaveClippings = YES ; // Avoid saving things that the user explicitly deletes .
2017-09-03 11:13:52 +08:00
[ clippingStore clearItem : stackPosition ] ;
2017-10-20 03:41:24 +08:00
inhibitAutosaveClippings = NO ;
2017-09-03 11:13:52 +08:00
[ self actionAfterListModification ] ;
2017-07-12 21:43:44 +08:00
return YES ;
}
- ( bool ) setStackPositionTo : ( int ) newStackPosition
{
if ( [ clippingStore jcListCount ] >= newStackPosition ) {
stackPosition = newStackPosition ;
return YES ;
}
return NO ;
}
// Would probably be good to just prevent this scenario where it originates and
// delete this check .
- ( void ) adjustStackPositionIfOutOfBounds
{
if ( stackPosition >= [ clippingStore jcListCount ] && stackPosition ! = 0 ) { // deleted last item
stackPosition = [ clippingStore jcListCount ] - 1 ;
}
}
- ( bool ) stackPositionIsInBounds
{
return ( [ clippingStore jcListCount ] > 0 && [ clippingStore jcListCount ] > stackPosition ) ;
}
- ( void ) clearList
{
[ clippingStore clearList ] ;
2017-09-03 11:13:52 +08:00
[ self actionAfterListModification ] ;
2017-07-12 21:43:44 +08:00
}
- ( void ) mergeList
{
2017-09-03 11:13:52 +08:00
[ clippingStore mergeList ] ;
2017-07-12 21:43:44 +08:00
}
- ( BOOL ) isValidClippingNumber : ( NSNumber * ) number {
2017-10-20 03:41:24 +08:00
return [ self isValidClippingNumber : number inStore : clippingStore ] ;
}
- ( BOOL ) isValidClippingNumber : ( NSNumber * ) number inStore : ( FlycutStore * ) store {
return ( ( [ number intValue ] + 1 ) <= [ store jcListCount ] ) ;
2017-07-12 21:43:44 +08:00
}
- ( NSString * ) clippingStringWithCount : ( int ) count {
2017-10-20 03:41:24 +08:00
return [ self clippingStringWithCount : count inStore : clippingStore ] ;
}
- ( NSString * ) clippingStringWithCount : ( int ) count inStore : ( FlycutStore * ) store {
if ( [ self isValidClippingNumber : [ NSNumber numberWithInt : count ] inStore : store ] ) {
return [ store clippingContentsAtPosition : count ] ;
2017-07-12 21:43:44 +08:00
} else { // It fails - - we shouldn ' t be passed this , but . . .
return @ "" ;
}
}
2017-09-03 11:13:52 +08:00
- ( void ) registerOrDeregisterICloudSync
{
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "syncSettingsViaICloud" ] ) {
[ MJCloudKitUserDefaultsSync startWithKeyMatchList : settingsSyncList
withContainerIdentifier : @ "iCloud.com.mark-a-jerde.Flycut" ] ;
}
else {
[ MJCloudKitUserDefaultsSync stopForKeyMatchList : settingsSyncList ] ;
}
2017-10-20 04:01:44 +08:00
BOOL syncClippings = [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "syncClippingsViaICloud" ] ;
BOOL changedSyncClippings = ( ! [ [ NSUserDefaults standardUserDefaults ] objectForKey : @ "previousSyncClippingsViaICloud" ]
|| syncClippings ! = [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "previousSyncClippingsViaICloud" ] ) ;
2017-10-31 21:58:34 +08:00
if ( changedSyncClippings )
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithBool : syncClippings ]
forKey : @ "previousSyncClippingsViaICloud" ] ;
2017-10-20 04:01:44 +08:00
// We will enable / disable regardless of changedSyncClippings because this gets called at app launch , where the feature was previously enabled but needs to be registered .
if ( syncClippings ) {
if ( changedSyncClippings )
firstClippingsSyncAfterEnabling = YES ;
2017-09-03 11:13:52 +08:00
[ MJCloudKitUserDefaultsSync removeNotificationsFor : MJSyncNotificationChanges forTarget : self ] ;
[ MJCloudKitUserDefaultsSync addNotificationFor : MJSyncNotificationChanges withSelector : @ selector ( checkPreferencesChanges : ) withTarget : self ] ;
[ MJCloudKitUserDefaultsSync removeNotificationsFor : MJSyncNotificationConflicts forTarget : self ] ;
[ MJCloudKitUserDefaultsSync addNotificationFor : MJSyncNotificationConflicts withSelector : @ selector ( checkPreferencesConflicts : ) withTarget : self ] ;
2017-10-20 04:01:44 +08:00
[ MJCloudKitUserDefaultsSync removeNotificationsFor : MJSyncNotificationSaveSuccess forTarget : self ] ;
[ MJCloudKitUserDefaultsSync addNotificationFor : MJSyncNotificationSaveSuccess withSelector : @ selector ( checkPreferencesSaveSuccess : ) withTarget : self ] ;
2017-09-03 11:13:52 +08:00
[ MJCloudKitUserDefaultsSync startWithKeyMatchList : @ [ @ "store" ]
withContainerIdentifier : @ "iCloud.com.mark-a-jerde.Flycut" ] ;
}
else {
[ MJCloudKitUserDefaultsSync removeNotificationsFor : MJSyncNotificationChanges forTarget : self ] ;
[ MJCloudKitUserDefaultsSync stopForKeyMatchList : @ [ @ "store" ] ] ;
}
}
- ( NSDictionary * ) checkPreferencesChanges : ( NSDictionary * ) changes
{
if ( [ changes valueForKey : @ "store" ] )
{
2017-10-20 04:01:44 +08:00
[ self integrateAllStores ] ;
2017-09-03 11:13:52 +08:00
}
return nil ;
}
2017-10-20 04:01:44 +08:00
- ( void ) integrateAllStores
{
DLog ( @ "integrating stores" ) ;
inhibitSaveEngineAfterListModification = YES ;
[ self integrateStoreAtKey : @ "jcList" into : clippingStore descriptiveName : @ "" ] ;
// It is possible that the user would disable sync rather than merge the main clippings store . They would have a poor user experience if they were then asked the same question about the favorites store after believing that they had disabled sync , so check setting before integrating .
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "syncClippingsViaICloud" ] )
[ self integrateStoreAtKey : @ "favoritesList" into : favoritesStore descriptiveName : @ "favorites " ] ;
DLog ( @ "integrating stores complete" ) ;
inhibitSaveEngineAfterListModification = NO ;
firstClippingsSyncAfterEnabling = NO ;
[ self actionAfterListModification ] ;
}
- ( void ) integrateStoreAtKey : ( NSString * ) listKey into : ( FlycutStore * ) store descriptiveName : ( NSString * ) name
2017-09-03 11:13:52 +08:00
{
FlycutStore * newContent = [ self allocInitFlycutStoreRemembering : [ clippingStore rememberNum ] ] ;
NSDictionary * loadDict = [ [ [ NSUserDefaults standardUserDefaults ] dictionaryForKey : @ "store" ] copy ] ;
if ( loadDict && [ self loadEngineFrom : loadDict key : listKey into : newContent ] )
{
2017-10-20 04:01:44 +08:00
BOOL mergeLists = NO ;
if ( firstClippingsSyncAfterEnabling )
{
if ( 0 = = [ store jcListCount ] )
{
// Just accept whatever iCloud has .
[ store clearInsertionJournalCount : [ [ store insertionJournal ] count ] ] ;
[ store clearDeletionJournalCount : [ [ store deletionJournal ] count ] ] ;
}
else if ( 0 = = [ newContent jcListCount ] )
{
// We have something . iCloud has nothing . Ignore iCloud this time .
[ newContent release ] ;
[ self actionAfterListModification ] ; // To overwrite what sync put in the defaults .
return ;
}
else
{
int newCount = [ newContent jcListCount ] ;
int ourCount = [ store jcListCount ] ;
int newDistinct = 0 ;
int ourDistinct = 0 ;
for ( int i = 0 ; i < newCount ; i + + )
{
if ( 0 > [ store indexOfClipping : [ newContent clippingAtPosition : i ] ] )
{
newDistinct + + ;
}
}
for ( int i = 0 ; i < ourCount ; i + + )
{
if ( 0 > [ newContent indexOfClipping : [ store clippingAtPosition : i ] ] )
{
ourDistinct + + ;
}
}
BOOL promptUser = NO ;
if ( 0 = = ourDistinct )
{
// Just accept whatever iCloud has .
[ store clearInsertionJournalCount : [ [ store insertionJournal ] count ] ] ;
[ store clearDeletionJournalCount : [ [ store deletionJournal ] count ] ] ;
}
else if ( 0 = = newDistinct )
{
// We have something . iCloud has nothing . Ignore iCloud this time .
[ newContent release ] ;
[ self actionAfterListModification ] ; // To overwrite what sync put in the defaults .
return ;
}
else
{
// Policy : For sake of user experience , the user will not be asked to merge or overwrite if one is a superset of the other and they have just said that they want sync . Assume they meant , "I want sync and I want it to include all content."
promptUser = YES ;
}
while ( promptUser )
{
promptUser = NO ;
NSString * choice = [ self delegateAlertWithMessageText : @ "First Sync"
informationText : [ NSString stringWithFormat : @ "Flycut found %i %@clipping%@ shared by both iCloud and this device, %i only in iCloud, and \%i only on this device. How can I handle these for you?" ,
( ourCount - ourDistinct ) ,
name ,
( ( ourCount - ourDistinct ) ! = 1 ? @ "s" : @ "" ) ,
newDistinct , ourDistinct ]
buttonsTexts : @ [ @ "Merge Lists" ,
@ "Overwrite Device List" ,
@ "Overwrite iCloud List" ,
@ "Disable Sync" ] ] ;
if ( ! choice )
{
// This most likely means the UI wasn ' t implemented , so cover it with merge .
choice = @ "Merge Lists" ;
}
if ( [ choice isEqualToString : @ "Merge Lists" ] )
{
mergeLists = YES ;
}
else if ( [ choice isEqualToString : @ "Disable Sync" ] )
{
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithBool : NO ]
forKey : @ "syncClippingsViaICloud" ] ;
[ newContent release ] ;
[ self registerOrDeregisterICloudSync ] ;
[ self actionAfterListModification ] ; // To overwrite what sync put in the defaults .
return ;
}
else
{
NSString * okCancel = [ self delegateAlertWithMessageText : @ "Warning"
informationText : [ NSString stringWithFormat : @ "%@ will cause clippings to be lost!" , choice ]
buttonsTexts : @ [ @ "Ok" , @ "Cancel" ] ] ;
if ( [ okCancel isEqualToString : @ "Ok" ] )
{
if ( [ choice isEqualToString : @ "Overwrite Device List" ] )
{
// Just accept whatever iCloud has .
[ store clearInsertionJournalCount : [ [ store insertionJournal ] count ] ] ;
[ store clearDeletionJournalCount : [ [ store deletionJournal ] count ] ] ;
}
else if ( [ choice isEqualToString : @ "Overwrite iCloud List" ] )
{
// Ignore iCloud this time .
[ newContent release ] ;
[ self actionAfterListModification ] ; // To overwrite what sync put in the defaults .
return ;
}
else
{
// This should be impossible , so cover it with disabling sync .
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithBool : NO ] forKey : @ "syncClippingsViaICloud" ] ;
[ newContent release ] ;
[ self registerOrDeregisterICloudSync ] ;
[ self actionAfterListModification ] ; // To overwrite what sync put in the defaults .
return ;
}
}
else
{
promptUser = YES ;
}
}
}
}
}
2017-09-03 11:13:52 +08:00
int newCount = [ newContent jcListCount ] ;
2017-10-20 04:01:44 +08:00
int offsetForMerge = 0 ;
2017-09-03 11:13:52 +08:00
for ( int i = 0 ; i < newCount ; i + + )
{
FlycutClipping * newClipping = [ newContent clippingAtPosition : i ] ;
if ( i >= [ store jcListCount ] )
{
// Clipping was beyond the end of the store , so just add it .
2017-10-20 04:01:44 +08:00
[ self integrateInsertClipping : newClipping toStore : store atIndex : ( i + offsetForMerge ) withMerge : mergeLists ] ;
2017-09-03 11:13:52 +08:00
}
2017-10-20 04:01:44 +08:00
else if ( ! [ newClipping isEqual : [ store clippingAtPosition : ( i + offsetForMerge ) ] ] )
2017-09-03 11:13:52 +08:00
{
2017-10-20 04:01:44 +08:00
BOOL contentAtThisStorePositionNotInNewContent = ( i + offsetForMerge ) < [ store jcListCount ] && [ newContent indexOfClipping : [ store clippingAtPosition : ( i + offsetForMerge ) ] ] < 0 ;
2017-09-03 11:13:52 +08:00
int firstIndex = [ store indexOfClipping : newClipping ] ;
2017-10-20 04:01:44 +08:00
if ( firstIndex < 0 && [ [ store deletionJournal ] containsObject : newClipping ] )
{
// Clipping was deleted locally , so delete from the new content and move on .
[ newContent clearItem : i ] ;
i - - ;
newCount - - ;
}
else if ( firstIndex < 0 )
2017-09-03 11:13:52 +08:00
{
// Clipping wasn ' t previously in the store , so just add it .
2017-10-20 04:01:44 +08:00
if ( mergeLists && contentAtThisStorePositionNotInNewContent )
{
// Give priority to local items so they end up at the top of the list .
// Look to the next store item .
offsetForMerge + + ;
// While checking this newContent item again .
i - - ;
}
else
{
[ self integrateInsertClipping : newClipping toStore : store atIndex : ( i + offsetForMerge ) withMerge : mergeLists ] ;
}
2017-09-03 11:13:52 +08:00
}
2017-10-20 04:01:44 +08:00
else if ( contentAtThisStorePositionNotInNewContent )
2017-09-03 11:13:52 +08:00
{
// Contents in the store at this position didn ' t exist in the newContent . Handle deletion .
2017-10-20 04:01:44 +08:00
if ( mergeLists
|| [ [ store insertionJournal ] containsObject : [ store clippingAtPosition : ( i + offsetForMerge ) ] ] )
{
// Look to the next store item .
offsetForMerge + + ;
// While checking this newContent item again .
i - - ;
}
else
{
[ store clearItem : ( i + offsetForMerge ) ] ;
i - - ;
}
2017-09-03 11:13:52 +08:00
}
else if ( [ store removeDuplicates ] )
{
if ( i < firstIndex )
2017-10-20 04:01:44 +08:00
[ store clippingMoveFrom : firstIndex To : ( i + offsetForMerge ) ] ;
2017-09-03 11:13:52 +08:00
else
{
// This can only happen if the remote store allowed duplicates and we do not . Just delete from the new content and move on .
[ newContent clearItem : i ] ;
i - - ;
newCount - - ;
}
}
else
{
2017-10-20 04:01:44 +08:00
[ self integrateInsertClipping : newClipping toStore : store atIndex : ( i + offsetForMerge ) withMerge : mergeLists ] ;
2017-09-03 11:13:52 +08:00
}
}
}
2017-10-20 04:01:44 +08:00
while ( [ store jcListCount ] > newCount + offsetForMerge )
[ store clearItem : ( newCount + offsetForMerge ) ] ;
2017-09-03 11:13:52 +08:00
# ifdef DEBUG
2017-10-20 04:01:44 +08:00
if ( ! mergeLists )
2017-09-03 11:13:52 +08:00
{
2017-10-20 04:01:44 +08:00
[ newContent release ] ;
newContent = [ self allocInitFlycutStoreRemembering : [ clippingStore rememberNum ] ] ;
[ self loadEngineFrom : loadDict key : listKey into : newContent ] ;
newCount = [ newContent jcListCount ] ;
if ( newCount ! = [ store jcListCount ] )
NSLog ( @ "Error in integrateStoreAtKey with mismatching after counts!" ) ;
else
2017-09-03 11:13:52 +08:00
{
2017-10-20 04:01:44 +08:00
for ( int i = 0 ; i < newCount ; i + + )
{
if ( ! [ [ store clippingAtPosition : i ] isEqual : [ newContent clippingAtPosition : i ] ] )
NSLog ( @ "Error in integrateStoreAtKey with mismatching clippings at index %i!" , i ) ;
}
2017-09-03 11:13:52 +08:00
}
}
# endif
}
[ newContent release ] ;
}
2017-10-20 04:01:44 +08:00
- ( void ) integrateInsertClipping : ( FlycutClipping * ) clipping toStore : ( FlycutStore * ) store atIndex : ( int ) index withMerge : ( BOOL ) mergeLists
{
[ clipping setDisplayLength : displayLength ] ;
if ( mergeLists && [ store jcListCount ] = = [ store rememberNum ] )
{
// Grow the rememberNum if needed in merge .
int newRememberNum = [ store rememberNum ] + 1 ;
[ store setRememberNum : newRememberNum ] ;
if ( store = = favoritesStore )
{
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithInt : newRememberNum ]
forKey : @ "favoritesRememberNum" ] ;
}
else
{
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSNumber numberWithInt : newRememberNum ]
forKey : @ "rememberNum" ] ;
}
}
[ store insertClipping : clipping atIndex : index ] ;
}
2017-09-03 11:13:52 +08:00
- ( NSDictionary * ) checkPreferencesConflicts : ( NSDictionary * ) changes
{
NSMutableDictionary * corrections = nil ;
if ( [ changes valueForKey : @ "store" ] )
{
2017-10-20 04:01:44 +08:00
// Load the version that the other party pushed .
[ [ NSUserDefaults standardUserDefaults ] setObject : [ changes valueForKey : @ "store" ] [ 2 ] forKey : @ "store" ] ;
// Integrate stores to apply journaled changes to conflict resolution .
[ self integrateAllStores ] ;
// Load the resolution into corrections .
2017-09-03 11:13:52 +08:00
if ( ! corrections )
corrections = [ [ NSMutableDictionary alloc ] init ] ;
2017-10-20 04:01:44 +08:00
corrections [ @ "store" ] = [ [ NSUserDefaults standardUserDefaults ] objectForKey : @ "store" ] ;
2017-09-03 11:13:52 +08:00
}
return corrections ;
}
2017-10-20 04:01:44 +08:00
- ( NSDictionary * ) checkPreferencesSaveSuccess : ( NSDictionary * ) changes
{
if ( [ changes valueForKey : @ "store" ] )
{
[ clippingStore pruneJournals ] ;
[ favoritesStore pruneJournals ] ;
}
return nil ;
}
2017-09-03 11:13:52 +08:00
- ( void ) checkCloudKitUpdates
{
[ MJCloudKitUserDefaultsSync checkCloudKitUpdates ] ;
}
- ( bool ) loadEngineFrom : ( NSDictionary * ) loadDict key : ( NSString * ) listKey into : ( FlycutStore * ) store
{
NSArray * savedJCList = [ loadDict objectForKey : listKey ] ;
if ( [ savedJCList isKindOfClass : [ NSArray class ] ] ) {
// There ' s probably a nicer way to prevent the range from going out of bounds , but this works .
int rangeCap = [ savedJCList count ] < [ store rememberNum ] ? [ savedJCList count ] : [ store rememberNum ] ;
NSRange loadRange = NSMakeRange ( 0 , rangeCap ) ;
NSArray * toBeRestoredClips = [ [ [ savedJCList subarrayWithRange : loadRange ] reverseObjectEnumerator ] allObjects ] ;
for ( NSDictionary * aSavedClipping in toBeRestoredClips )
[ store addClipping : [ aSavedClipping objectForKey : @ "Contents" ]
ofType : [ aSavedClipping objectForKey : @ "Type" ]
fromAppLocalizedName : [ aSavedClipping objectForKey : @ "AppLocalizedName" ]
fromAppBundleURL : [ aSavedClipping objectForKey : @ "AppBundleURL" ]
atTimestamp : [ [ aSavedClipping objectForKey : @ "Timestamp" ] integerValue ] ] ;
return YES ;
} else DLog ( @ "Not array" ) ;
return NO ;
}
2017-07-12 21:43:44 +08:00
- ( bool ) loadEngineFromPList
{
2017-09-03 11:13:52 +08:00
NSDictionary * loadDict = [ [ [ NSUserDefaults standardUserDefaults ] dictionaryForKey : @ "store" ] copy ] ;
if ( loadDict ! = nil ) {
bool success = NO ;
success | = [ self loadEngineFrom : loadDict key : @ "jcList" into : clippingStore ] ;
success | = [ self loadEngineFrom : loadDict key : @ "favoritesList" into : favoritesStore ] ;
[ loadDict release ] ;
return success ;
}
return NO ;
2017-07-12 21:43:44 +08:00
}
- ( bool ) setStackPositionToOneLessRecent
{
stackPosition + + ;
if ( [ clippingStore jcListCount ] > stackPosition ) {
return YES ;
} else {
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "wraparoundBezel" ] ) {
stackPosition = 0 ;
return YES ;
} else {
stackPosition - - ;
}
}
return NO ;
}
- ( bool ) setStackPositionToOneMoreRecent
{
stackPosition - - ;
if ( stackPosition < 0 ) {
if ( [ [ NSUserDefaults standardUserDefaults ] boolForKey : @ "wraparoundBezel" ] ) {
stackPosition = [ clippingStore jcListCount ] - 1 ;
return YES ;
} else {
stackPosition = 0 ;
return NO ;
}
}
if ( [ clippingStore jcListCount ] > stackPosition ) {
return YES ;
}
return NO ;
}
- ( FlycutClipping * ) clippingAtStackPosition
{
return [ clippingStore clippingAtPosition : stackPosition ] ;
}
- ( void ) saveStore : ( FlycutStore * ) store toKey : ( NSString * ) key onDict : ( NSMutableDictionary * ) saveDict {
NSMutableArray * jcListArray = [ NSMutableArray array ] ;
for ( int i = 0 ; i < [ store jcListCount ] ; i + + )
{
FlycutClipping * clipping = [ store clippingAtPosition : i ] ;
NSMutableDictionary * dict = [ NSMutableDictionary dictionaryWithObjectsAndKeys :
[ clipping contents ] , @ "Contents" ,
[ clipping type ] , @ "Type" ,
[ NSNumber numberWithInt : i ] , @ "Position" , nil ] ;
NSString * val = [ clipping appLocalizedName ] ;
if ( nil ! = val )
[ dict setObject : val forKey : @ "AppLocalizedName" ] ;
val = [ clipping appBundleURL ] ;
if ( nil ! = val )
[ dict setObject : val forKey : @ "AppBundleURL" ] ;
int timestamp = [ clipping timestamp ] ;
if ( timestamp > 0 )
[ dict setObject : [ NSNumber numberWithInt : timestamp ] forKey : @ "Timestamp" ] ;
[ jcListArray addObject : dict ] ;
}
[ saveDict setObject : jcListArray forKey : key ] ;
2017-10-20 03:20:34 +08:00
[ store clearModifiedSinceLastSaveStore ] ;
2017-07-12 21:43:44 +08:00
}
- ( void ) saveEngine {
2017-10-20 03:20:34 +08:00
// saveEngine saves to NSUserDefaults . If there have been no modifications , just skip this to avoid busy activity for any observers .
if ( ! ( [ clippingStore modifiedSinceLastSaveStore ]
|| [ favoritesStore modifiedSinceLastSaveStore ] ) )
return ;
2017-07-12 21:43:44 +08:00
NSMutableDictionary * saveDict ;
saveDict = [ NSMutableDictionary dictionaryWithCapacity : 3 ] ;
[ saveDict setObject : @ "0.7" forKey : @ "version" ] ;
[ saveDict setObject : [ NSNumber numberWithInt : [ [ NSUserDefaults standardUserDefaults ] integerForKey : @ "rememberNum" ] ]
forKey : @ "rememberNum" ] ;
[ saveDict setObject : [ NSNumber numberWithInt : [ [ NSUserDefaults standardUserDefaults ] integerForKey : @ "favoritesRememberNum" ] ]
forKey : @ "favoritesRememberNum" ] ;
2017-08-27 12:23:20 +08:00
[ saveTarget performSelector : saveSelector withObject : saveDict ] ;
2017-07-12 21:43:44 +08:00
[ self saveStore : clippingStore toKey : @ "jcList" onDict : saveDict ] ;
[ self saveStore : favoritesStore toKey : @ "favoritesList" onDict : saveDict ] ;
[ [ NSUserDefaults standardUserDefaults ] setObject : saveDict forKey : @ "store" ] ;
[ [ NSUserDefaults standardUserDefaults ] synchronize ] ;
}
- ( void ) applicationWillTerminate {
if ( [ [ NSUserDefaults standardUserDefaults ] integerForKey : @ "savePreference" ] >= 1 ) {
DLog ( @ "Saving on exit" ) ;
[ self saveEngine ] ;
} else {
// Remove clips from store
[ [ NSUserDefaults standardUserDefaults ] setValue : [ NSDictionary dictionary ] forKey : @ "store" ] ;
DLog ( @ "Saving preferences on exit" ) ;
[ [ NSUserDefaults standardUserDefaults ] synchronize ] ;
}
}
- ( NSArray * ) previousIndexes : ( int ) howMany containing : ( NSString * ) search // This method is in newest - first order .
{
return [ clippingStore previousIndexes : howMany containing : search ] ;
}
- ( NSArray * ) previousDisplayStrings : ( int ) howMany containing : ( NSString * ) search
{
return [ clippingStore previousDisplayStrings : howMany containing : search ] ;
}
2017-10-20 04:01:44 +08:00
- ( NSString * ) delegateAlertWithMessageText : ( NSString * ) message informationText : ( NSString * ) information buttonsTexts : ( NSArray * ) buttons
{
if ( self . delegate && [ self . delegate respondsToSelector : @ selector ( alertWithMessageText : informationText : buttonsTexts : ) ] )
return [ self . delegate alertWithMessageText : message informationText : information buttonsTexts : buttons ] ;
return nil ;
}
2017-07-12 21:43:44 +08:00
@ end