Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AppController.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
// Basic functionality
-(void) pollPB:(NSTimer *)timer;
-(void) addClipToPasteboard:(NSString*)pbFullText;
-(void) addClipToPasteboard:(NSString*)pbFullText withRichData:(NSDictionary *)richData;
-(void) setPBBlockCount:(NSNumber *)newPBBlockCount;
-(void) hideApp;
-(void) fakeCommandV;
Expand Down
47 changes: 38 additions & 9 deletions AppController.m
Original file line number Diff line number Diff line change
Expand Up @@ -883,10 +883,11 @@ - (void)restoreStashedStoreAndUpdate
- (void)pasteFromStack
{
NSLog(@"pasteFromStack called");
NSDictionary *richData = [flycutOperator getRichDataFromStackPosition];
NSString *content = [flycutOperator getPasteFromStackPosition];
if ( nil != content ) {
NSLog(@"Content found, adding to pasteboard and preparing to paste: %@", [content substringToIndex:MIN(content.length, 50)]);
[self addClipToPasteboard:content];
[self addClipToPasteboard:content withRichData:richData];
[self performSelector:@selector(hideApp) withObject:nil afterDelay:0.2];
[self performSelector:@selector(fakeCommandV) withObject:nil afterDelay:0.5];
} else {
Expand Down Expand Up @@ -915,10 +916,11 @@ - (void)pasteIndexAndUpdate:(int) position {
position = [mapping[position] intValue];
}

NSDictionary *richData = [flycutOperator getRichDataFromIndex: position];
NSString *content = [flycutOperator getPasteFromIndex: position];
if ( nil != content )
{
[self addClipToPasteboard:content];
[self addClipToPasteboard:content withRichData:richData];
[self updateMenu];
}
}
Expand Down Expand Up @@ -1097,12 +1099,30 @@ -(void)pollPB:(NSTimer *)timer
if ( contents == nil || [flycutOperator shouldSkip:contents ofType:[jcPasteboard availableTypeFromArray:[NSArray arrayWithObject:NSPasteboardTypeString]] fromAvailableTypes:[jcPasteboard types]] ) {
DLog(@"Contents: Empty or skipped");
} else {
// Capture rich representations of the text so pasting an older clipping
// can keep its formatting. Only done after the skip checks above so
// concealed/transient clippings never have their rich data read.
NSMutableDictionary *richData = nil;
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"preserveTextFormatting"] ) {
for ( NSString *richType in [NSArray arrayWithObjects:NSPasteboardTypeRTF, NSPasteboardTypeHTML, nil] ) {
if ( nil != [jcPasteboard availableTypeFromArray:[NSArray arrayWithObject:richType]] ) {
NSData *data = [jcPasteboard dataForType:richType];
// Cap the size so one giant rich clipping can't bloat the saved store.
if ( nil != data && [data length] > 0 && [data length] <= 1048576 ) {
if ( nil == richData )
richData = [NSMutableDictionary dictionaryWithCapacity:2];
[richData setObject:data forKey:richType];
}
}
}
}

// Dispatch back to main queue to safely modify the clipping store and update UI.
// jcList (NSMutableArray) is not thread-safe, and concurrent access from this
// background queue and the main thread (e.g. showing the bezel) causes crashes.
dispatch_async(dispatch_get_main_queue(), ^{
if ( ! [pbCount isEqualTo:pbBlockCount] ) {
[flycutOperator addClipping:contents ofType:type fromApp:[currRunningApp localizedName] withAppBundleURL:currRunningApp.bundleURL.path target:self clippingAddedSelector:@selector(updateMenu)];
[flycutOperator addClipping:contents ofType:type withRichData:richData fromApp:[currRunningApp localizedName] withAppBundleURL:currRunningApp.bundleURL.path target:self clippingAddedSelector:@selector(updateMenu)];
}
});
}
Expand Down Expand Up @@ -1554,12 +1574,20 @@ -(void) setPBBlockCount:(NSNumber *)newPBBlockCount

-(void)addClipToPasteboard:(NSString*)pbFullText
{
NSArray *pbTypes;
pbTypes = [NSArray arrayWithObjects:@"NSStringPboardType",NULL];

[self addClipToPasteboard:pbFullText withRichData:nil];
}

-(void)addClipToPasteboard:(NSString*)pbFullText withRichData:(NSDictionary *)richData
{
NSMutableArray *pbTypes = [NSMutableArray arrayWithObject:NSPasteboardTypeString];
if ( nil != richData )
[pbTypes addObjectsFromArray:[richData allKeys]];

[jcPasteboard declareTypes:pbTypes owner:NULL];

[jcPasteboard setString:pbFullText forType:@"NSStringPboardType"];

[jcPasteboard setString:pbFullText forType:NSPasteboardTypeString];
for ( NSString *richType in richData )
[jcPasteboard setData:[richData objectForKey:richType] forType:richType];
[self setPBBlockCount:[NSNumber numberWithInt:[jcPasteboard changeCount]]];
}

Expand Down Expand Up @@ -1910,9 +1938,10 @@ - (IBAction)searchWindowItemSelected:(id)sender
position = [mapping[selectedRow] intValue];
}

NSDictionary *richData = [flycutOperator getRichDataFromIndex:position];
NSString *content = [flycutOperator getPasteFromIndex:position];
if (content) {
[self addClipToPasteboard:content];
[self addClipToPasteboard:content withRichData:richData];
[self updateMenu]; // Update menu like bezel does
[self hideSearchWindow];

Expand Down
4 changes: 4 additions & 0 deletions FlycutEngine/FlycutClipping.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
NSString * appBundleURL;
// The time
NSInteger clipTimestamp;
// Optional rich representations of the text (pasteboard type -> NSData), e.g. RTF or HTML
NSDictionary * clipRichData;
}

-(id) initWithContents:(NSString *)contents withType:(NSString *)type withDisplayLength:(int)displayLength withAppLocalizedName:(NSString *)localizedName withAppBundleURL:(NSString *)bundleURL withTimestamp:(NSInteger)timestamp;
Expand All @@ -42,6 +44,7 @@
-(void) setType:(NSString *)newType;
-(void) setDisplayLength:(int)newDisplayLength;
-(void) setHasName:(BOOL)newHasName;
-(void) setRichData:(NSDictionary *)newRichData;

// Retrieve values
-(FlycutClipping *) clipping;
Expand All @@ -53,6 +56,7 @@
-(NSString *) appBundleURL;
-(NSInteger) timestamp;
-(BOOL) hasName;
-(NSDictionary *) richData;

// Additional functions
-(void) resetDisplayString;
Expand Down
14 changes: 14 additions & 0 deletions FlycutEngine/FlycutClipping.m
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ -(void) setHasName:(BOOL)newHasName
clipHasName = newHasName;
}

-(void) setRichData:(NSDictionary *)newRichData
{
id old = clipRichData;
[newRichData retain];
clipRichData = newRichData;
[old release];
}

-(NSDictionary *) richData
{
return clipRichData;
}

-(void) resetDisplayString
{
NSString *newDisplayString, *firstLineOfClipping, *trimmedString;
Expand Down Expand Up @@ -234,6 +247,7 @@ -(void) dealloc
[clipType release];
[appLocalizedName release];
[appBundleURL release];
[clipRichData release];
clipDisplayLength = 0;
[clipDisplayString release];
clipHasName = 0;
Expand Down
2 changes: 2 additions & 0 deletions FlycutEngine/FlycutStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
-(NSString *) clippingContentsAtPosition:(int)index;
-(NSString *) clippingDisplayStringAtPosition:(int)index;
-(NSString *) clippingTypeAtPosition:(int)index;
-(NSDictionary *) clippingRichDataAtPosition:(int)index;
-(NSArray *) previousContents:(int)howMany;
-(NSArray *) previousDisplayStrings:(int)howMany;
-(NSArray *) previousDisplayStrings:(int)howMany containing:(NSString*)search;
Expand All @@ -87,6 +88,7 @@

// Add a clipping
-(bool) addClipping:(NSString *)clipping ofType:(NSString *)type fromAppLocalizedName:(NSString *)appLocalizedName fromAppBundleURL:(NSString *)bundleURL atTimestamp:(NSInteger) timestamp;
-(bool) addClipping:(NSString *)clipping ofType:(NSString *)type fromAppLocalizedName:(NSString *)appLocalizedName fromAppBundleURL:(NSString *)bundleURL atTimestamp:(NSInteger) timestamp withRichData:(NSDictionary *)richData;
-(void) addClipping:(FlycutClipping*) clipping;
-(void) insertClipping:(FlycutClipping*) clipping atIndex:(int) index;

Expand Down
18 changes: 16 additions & 2 deletions FlycutEngine/FlycutStore.m
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ -(int) indexOfClipping:(FlycutClipping*) clipping afterIndex:(int) after{

// Add a clipping
-(bool) addClipping:(NSString *)clipping ofType:(NSString *)type fromAppLocalizedName:(NSString *)appLocalizedName fromAppBundleURL:(NSString *)bundleURL atTimestamp:(int) timestamp{
return [self addClipping:clipping ofType:type fromAppLocalizedName:appLocalizedName fromAppBundleURL:bundleURL atTimestamp:timestamp withRichData:nil];
}

-(bool) addClipping:(NSString *)clipping ofType:(NSString *)type fromAppLocalizedName:(NSString *)appLocalizedName fromAppBundleURL:(NSString *)bundleURL atTimestamp:(int) timestamp withRichData:(NSDictionary *)richData{
if ([clipping length] == 0) {
return NO;
}
Expand All @@ -88,9 +92,10 @@ -(bool) addClipping:(NSString *)clipping ofType:(NSString *)type fromAppLocalize
withAppLocalizedName:appLocalizedName
withAppBundleURL:bundleURL
withTimestamp:timestamp];

[newClipping setRichData:richData];

[self addClipping:newClipping];

[newClipping release];
return YES;
}
Expand Down Expand Up @@ -311,6 +316,15 @@ -(NSString *) clippingContentsAtPosition:(int)index
}
}

-(NSDictionary *) clippingRichDataAtPosition:(int)index
{
if ( index >= [jcList count] ) {
return nil;
} else {
return [[jcList objectAtIndex:index] richData];
}
}

-(NSString *) clippingDisplayStringAtPosition:(int)index
{
return [[jcList objectAtIndex:index] displayString];
Expand Down
3 changes: 3 additions & 0 deletions FlycutOperator.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@
// Basic functionality
-(int)indexOfClipping:(NSString*)contents ofType:(NSString*)type fromApp:(NSString *)appName withAppBundleURL:(NSString *)bundleURL;
-(bool)addClipping:(NSString*)contents ofType:(NSString*)type fromApp:(NSString *)appName withAppBundleURL:(NSString *)bundleURL target:(id)selectorTarget clippingAddedSelector:(SEL)clippingAddedSelectorclippingAddedSelector;
-(bool)addClipping:(NSString*)contents ofType:(NSString*)type withRichData:(NSDictionary *)richData fromApp:(NSString *)appName withAppBundleURL:(NSString *)bundleURL target:(id)selectorTarget clippingAddedSelector:(SEL)clippingAddedSelector;
-(int)stackPosition;
-(NSString*)getPasteFromStackPosition;
-(NSString*)getPasteFromIndex:(int) position;
-(NSDictionary*)getRichDataFromStackPosition;
-(NSDictionary*)getRichDataFromIndex:(int) position;
-(bool) saveFromStack;
-(bool)clearItemAtStackPosition;
-(void)clearList;
Expand Down
50 changes: 46 additions & 4 deletions FlycutOperator.m
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ - (id)init
@"removeDuplicates",
[NSNumber numberWithBool:NO],
@"pasteMovesToTop",
[NSNumber numberWithBool:YES],
@"preserveTextFormatting",
[NSNumber numberWithBool:NO],
@"syncSettingsViaICloud",
[NSNumber numberWithBool:NO],
Expand All @@ -66,7 +68,8 @@ - (id)init
@"skipPasswordLengths",
@"skipPasswordLengthsList",
@"removeDuplicates",
@"pasteMovesToTop"];
@"pasteMovesToTop",
@"preserveTextFormatting"];
[settingsSyncList retain];

return self;
Expand Down Expand Up @@ -227,6 +230,21 @@ - (NSString*)getPasteFromStackPosition
return nil;
}

// Must be called before getPasteFromIndex:/getPasteFromStackPosition, which may
// reorder the list when pasteMovesToTop is enabled.
- (NSDictionary*)getRichDataFromStackPosition
{
if ( [clippingStore jcListCount] > stackPosition ) {
return [self getRichDataFromIndex: stackPosition];
}
return nil;
}

- (NSDictionary*)getRichDataFromIndex:(int) position
{
return [clippingStore clippingRichDataAtPosition:position];
}

- (bool)saveFromStack
{
return [self saveFromStackWithPrefix:@""];
Expand Down Expand Up @@ -460,13 +478,19 @@ -(int)indexOfClipping:(NSString*)contents ofType:(NSString*)type fromApp:(NSStri
}

-(bool)addClipping:(NSString*)contents ofType:(NSString*)type fromApp:(NSString *)appName withAppBundleURL:(NSString *)bundleURL target:(id)selectorTarget clippingAddedSelector:(SEL)clippingAddedSelector
{
return [self addClipping:contents ofType:type withRichData:nil fromApp:appName withAppBundleURL:bundleURL target:selectorTarget clippingAddedSelector:clippingAddedSelector];
}

-(bool)addClipping:(NSString*)contents ofType:(NSString*)type withRichData:(NSDictionary *)richData fromApp:(NSString *)appName withAppBundleURL:(NSString *)bundleURL target:(id)selectorTarget clippingAddedSelector:(SEL)clippingAddedSelector
{
if ( [clippingStore jcListCount] == 0 || ! [contents isEqualToString:[clippingStore clippingContentsAtPosition:0]]) {
bool success = [clippingStore addClipping:contents
ofType:type
fromAppLocalizedName:appName
fromAppBundleURL:bundleURL
atTimestamp:[[NSDate date] timeIntervalSince1970]];
atTimestamp:[[NSDate date] timeIntervalSince1970]
withRichData:richData];

// The below tracks our position down down down... Maybe as an option?
// if ( [clippingStore jcListCount] > 1 ) stackPosition++;
Expand Down Expand Up @@ -985,12 +1009,26 @@ -(bool) loadEngineFrom:(NSDictionary*)loadDict key:(NSString*)listKey into:(Flyc
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)
for( NSDictionary *aSavedClipping in toBeRestoredClips) {
// Only restore rich data that has the expected shape (type string -> data),
// since the plist could have been written by other versions or edited.
NSDictionary *richData = [aSavedClipping objectForKey:@"RichData"];
if ( [richData isKindOfClass:[NSDictionary class]] ) {
for ( id key in richData )
if ( ! [key isKindOfClass:[NSString class]] || ! [[richData objectForKey:key] isKindOfClass:[NSData class]] ) {
richData = nil;
break;
}
} else
richData = nil;

[store addClipping:[aSavedClipping objectForKey:@"Contents"]
ofType:[aSavedClipping objectForKey:@"Type"]
fromAppLocalizedName:[aSavedClipping objectForKey:@"AppLocalizedName"]
fromAppBundleURL:[aSavedClipping objectForKey:@"AppBundleURL"]
atTimestamp:[[aSavedClipping objectForKey:@"Timestamp"] integerValue]];
atTimestamp:[[aSavedClipping objectForKey:@"Timestamp"] integerValue]
withRichData:richData];
}
return YES;
} else DLog(@"Not array");
return NO;
Expand Down Expand Up @@ -1071,6 +1109,10 @@ - (void)saveStore:(FlycutStore *)store toKey:(NSString *)key onDict:(NSMutableDi
if ( timestamp > 0 )
[dict setObject:[NSNumber numberWithInt:timestamp] forKey:@"Timestamp"];

NSDictionary *richData = [clipping richData];
if ( nil != richData && [richData count] > 0 )
[dict setObject:richData forKey:@"RichData"];

[jcListArray addObject:dict];
}
[saveDict setObject:jcListArray forKey:key];
Expand Down
8 changes: 8 additions & 0 deletions help.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ Here's a complete list of keys you can use in the Bezel.

Press the Search Hotkey (Shift-Command-B, by default) to open the search window. Type to filter your clipboard history. Use the arrow keys to navigate results and press Return to paste the selected item.

**Text Formatting**

Flycut preserves the formatting (RTF and HTML) of copied text, so pasting an older clipping into a rich text editor keeps its bold, fonts, links, and colors. Plain-text-only targets are unaffected — they always receive the plain text. Rich data larger than 1 MB per clipping is not kept.

To disable this and store plain text only, run:

defaults write com.generalarcade.flycut preserveTextFormatting -bool NO

**Menu Features**

The Flycut menu allows you to select from the most recent items in the main clipboard history store, clear all of them, merge them all into one entry, or access the preferences panel. You can also Option-Click the Flycut menu icon to disable or reenable clipboard tracking, in case you are copying sensitive information such as passwords.
Expand Down