mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
385 lines
13 KiB
Plaintext
385 lines
13 KiB
Plaintext
/*
|
|
TODO Filter duplicates
|
|
*/
|
|
|
|
#import "OakPasteboard.h"
|
|
#import "OakPasteboardSelector.h"
|
|
#import <oak/oak.h>
|
|
#import <oak/debug.h>
|
|
|
|
OAK_DEBUG_VAR(Pasteboard);
|
|
|
|
NSString* const NSReplacePboard = @"NSReplacePboard";
|
|
NSString* const OakPasteboardDidChangeNotification = @"OakClipboardDidChangeNotification";
|
|
NSString* const OakPasteboardOptionsPboardType = @"OakPasteboardOptionsPboardType";
|
|
|
|
NSString* const kUserDefaultsFindWrapAround = @"findWrapAround";
|
|
NSString* const kUserDefaultsFindIgnoreCase = @"findIgnoreCase";
|
|
|
|
NSString* const OakFindIgnoreWhitespaceOption = @"ignoreWhitespace";
|
|
NSString* const OakFindFullWordsOption = @"fullWordMatch";
|
|
NSString* const OakFindRegularExpressionOption = @"regularExpression";
|
|
|
|
NSString* const kUserDefaultsDisablePersistentClipboardHistory = @"disablePersistentClipboardHistory";
|
|
|
|
@interface OakPasteboardEntry ()
|
|
{
|
|
NSMutableDictionary* _options;
|
|
}
|
|
@end
|
|
|
|
@implementation OakPasteboardEntry
|
|
- (id)initWithString:(NSString*)aString andOptions:(NSDictionary*)someOptions
|
|
{
|
|
D(DBF_Pasteboard, bug("%s, %s\n", aString.UTF8String, someOptions.description.UTF8String););
|
|
ASSERT(aString != nil);
|
|
if(self = [super init])
|
|
{
|
|
self.string = aString;
|
|
self.options = [[NSMutableDictionary alloc] initWithDictionary:someOptions];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
+ (OakPasteboardEntry*)pasteboardEntryWithString:(NSString*)aString andOptions:(NSDictionary*)someOptions
|
|
{
|
|
return [[self alloc] initWithString:aString andOptions:someOptions];
|
|
}
|
|
|
|
+ (OakPasteboardEntry*)pasteboardEntryWithString:(NSString*)aString
|
|
{
|
|
return [self pasteboardEntryWithString:aString andOptions:nil];
|
|
}
|
|
|
|
- (void)setOptions:(NSDictionary*)aDictionary
|
|
{
|
|
if(_options == aDictionary)
|
|
return;
|
|
_options = [NSMutableDictionary dictionaryWithDictionary:aDictionary];
|
|
}
|
|
|
|
- (BOOL)isEqual:(OakPasteboardEntry*)otherEntry
|
|
{
|
|
return [otherEntry isKindOfClass:[self class]] && [self.string isEqual:otherEntry.string];
|
|
}
|
|
|
|
- (BOOL)fullWordMatch { return [_options[OakFindFullWordsOption] boolValue]; };
|
|
- (BOOL)ignoreWhitespace { return [_options[OakFindIgnoreWhitespaceOption] boolValue]; };
|
|
- (BOOL)regularExpression { return [_options[OakFindRegularExpressionOption] boolValue]; };
|
|
|
|
- (void)setOption:(NSString*)aKey toBoolean:(BOOL)flag
|
|
{
|
|
if(!flag)
|
|
{
|
|
[_options removeObjectForKey:aKey];
|
|
return;
|
|
}
|
|
|
|
if(!_options)
|
|
_options = [NSMutableDictionary new];
|
|
_options[aKey] = @YES;
|
|
}
|
|
|
|
- (void)setFullWordMatch:(BOOL)flag { return [self setOption:OakFindFullWordsOption toBoolean:flag]; };
|
|
- (void)setIgnoreWhitespace:(BOOL)flag { return [self setOption:OakFindIgnoreWhitespaceOption toBoolean:flag]; };
|
|
- (void)setRegularExpression:(BOOL)flag { return [self setOption:OakFindRegularExpressionOption toBoolean:flag]; };
|
|
|
|
- (find::options_t)findOptions
|
|
{
|
|
return find::options_t(
|
|
([self fullWordMatch] ? find::full_words : find::none) |
|
|
([[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsFindIgnoreCase] ? find::ignore_case : find::none) |
|
|
([[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsFindWrapAround] ? find::wrap_around : find::none) |
|
|
([self ignoreWhitespace] ? find::ignore_whitespace : find::none) |
|
|
([self regularExpression] ? find::regular_expression : find::none));
|
|
}
|
|
|
|
- (void)setFindOptions:(find::options_t)findOptions
|
|
{
|
|
self.fullWordMatch = (findOptions & find::full_words ) != 0;
|
|
self.ignoreWhitespace = (findOptions & find::ignore_whitespace ) != 0;
|
|
self.regularExpression = (findOptions & find::regular_expression) != 0;
|
|
}
|
|
|
|
- (NSDictionary*)asDictionary
|
|
{
|
|
NSMutableDictionary* res = [NSMutableDictionary dictionaryWithDictionary:_options];
|
|
res[@"string"] = self.string ?: @"";
|
|
return res;
|
|
}
|
|
@end
|
|
|
|
@interface OakPasteboard ()
|
|
{
|
|
NSMutableArray* _entries;
|
|
}
|
|
@property (nonatomic) NSString* pasteboardName;
|
|
@property (nonatomic) NSInteger changeCount;
|
|
@property (nonatomic) NSArray* entries;
|
|
@property (nonatomic) NSUInteger index;
|
|
- (void)checkForExternalPasteboardChanges;
|
|
@end
|
|
|
|
// ============================
|
|
// = Event Loop Idle Callback =
|
|
// ============================
|
|
|
|
namespace
|
|
{
|
|
struct event_loop_idle_callback_t;
|
|
static event_loop_idle_callback_t& idle_callback ();
|
|
|
|
struct event_loop_idle_callback_t
|
|
{
|
|
event_loop_idle_callback_t () : _running(false) { _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 100, &callback, NULL); start(); }
|
|
~event_loop_idle_callback_t () { stop(); CFRelease(_observer); }
|
|
|
|
void start ()
|
|
{
|
|
if(_running)
|
|
return;
|
|
_running = true;
|
|
CFRunLoopAddObserver(CFRunLoopGetCurrent(), _observer, kCFRunLoopCommonModes);
|
|
}
|
|
|
|
void stop ()
|
|
{
|
|
if(!_running)
|
|
return;
|
|
_running = false;
|
|
CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), _observer, kCFRunLoopCommonModes);
|
|
}
|
|
|
|
void add (OakPasteboard* aPasteboard) { _pasteboards.insert(aPasteboard); }
|
|
void remove (OakPasteboard* aPasteboard) { ASSERT(_pasteboards.find(aPasteboard) != _pasteboards.end()); _pasteboards.erase(aPasteboard); }
|
|
|
|
private:
|
|
static void callback (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void* info)
|
|
{
|
|
iterate(it, idle_callback()._pasteboards)
|
|
[*it checkForExternalPasteboardChanges];
|
|
}
|
|
|
|
bool _running;
|
|
CFRunLoopObserverRef _observer;
|
|
std::set<OakPasteboard*> _pasteboards;
|
|
};
|
|
|
|
static event_loop_idle_callback_t& idle_callback ()
|
|
{
|
|
static event_loop_idle_callback_t res;
|
|
return res;
|
|
}
|
|
}
|
|
|
|
@implementation OakPasteboard
|
|
- (NSPasteboard*)pasteboard
|
|
{
|
|
return [NSPasteboard pasteboardWithName:_pasteboardName];
|
|
}
|
|
|
|
- (void)saveToDefaults
|
|
{
|
|
if([[[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsDisablePersistentClipboardHistory] boolValue])
|
|
return [[NSUserDefaults standardUserDefaults] removeObjectForKey:_pasteboardName];
|
|
|
|
NSArray* tmp = [_entries count] < 50 ? _entries : [_entries subarrayWithRange:NSMakeRange([_entries count]-50, 50)];
|
|
[[NSUserDefaults standardUserDefaults] setObject:[tmp valueForKey:@"asDictionary"] forKey:_pasteboardName];
|
|
}
|
|
|
|
- (void)checkForExternalPasteboardChanges
|
|
{
|
|
D(DBF_Pasteboard, bug("new data: %s (%zd = %zd)\n", BSTR((_changeCount != [[self pasteboard] changeCount])), (ssize_t)_changeCount, (ssize_t)[[self pasteboard] changeCount]););
|
|
if(_changeCount != [[self pasteboard] changeCount])
|
|
{
|
|
NSString* onClipboard = [[self pasteboard] availableTypeFromArray:@[ NSStringPboardType ]] ? [[self pasteboard] stringForType:NSStringPboardType] : nil;
|
|
NSString* onStack = [([_entries count] == 0 ? nil : _entries[_index]) string];
|
|
_changeCount = [[self pasteboard] changeCount];
|
|
if((onClipboard && !onStack) || (onClipboard && onStack && ![onStack isEqualToString:onClipboard]))
|
|
{
|
|
id options = [[self pasteboard] availableTypeFromArray:@[ OakPasteboardOptionsPboardType ]] ? [[self pasteboard] propertyListForType:OakPasteboardOptionsPboardType] : nil;
|
|
[_entries addObject:[OakPasteboardEntry pasteboardEntryWithString:onClipboard andOptions:options]];
|
|
_index = [_entries count]-1;
|
|
_auxiliaryOptionsForCurrent = nil;
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:OakPasteboardDidChangeNotification object:self];
|
|
[self saveToDefaults];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)applicationDidBecomeActiveNotification:(id)sender
|
|
{
|
|
[self checkForExternalPasteboardChanges];
|
|
idle_callback().start();
|
|
}
|
|
|
|
- (void)applicationDidResignActiveNotification:(id)sender
|
|
{
|
|
idle_callback().stop();
|
|
}
|
|
|
|
- (void)setIndex:(NSUInteger)newIndex
|
|
{
|
|
D(DBF_Pasteboard, bug("%zu → %zu\n", (size_t)_index, (size_t)newIndex););
|
|
if(_index != newIndex)
|
|
{
|
|
_index = newIndex;
|
|
_auxiliaryOptionsForCurrent = nil;
|
|
if(OakPasteboardEntry* current = [_entries count] == 0 ? nil : _entries[_index])
|
|
{
|
|
[[self pasteboard] declareTypes:@[ NSStringPboardType, OakPasteboardOptionsPboardType ] owner:nil];
|
|
[[self pasteboard] setString:[current string] forType:NSStringPboardType];
|
|
[[self pasteboard] setPropertyList:[current options] forType:OakPasteboardOptionsPboardType];
|
|
_changeCount = [[self pasteboard] changeCount];
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:OakPasteboardDidChangeNotification object:self];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (id)initWithName:(NSString*)aName
|
|
{
|
|
if(self = [super init])
|
|
{
|
|
_pasteboardName = aName;
|
|
|
|
_entries = [NSMutableArray new];
|
|
if(![[[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsDisablePersistentClipboardHistory] boolValue])
|
|
{
|
|
if(NSArray* history = [[NSUserDefaults standardUserDefaults] arrayForKey:_pasteboardName])
|
|
{
|
|
for(NSDictionary* entry in history)
|
|
{
|
|
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithDictionary:entry];
|
|
[dict removeObjectForKey:@"string"];
|
|
if(NSString* str = entry[@"string"])
|
|
[_entries addObject:[OakPasteboardEntry pasteboardEntryWithString:str andOptions:dict]];
|
|
}
|
|
|
|
_index = [_entries count]-1;
|
|
}
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActiveNotification:) name:NSApplicationDidBecomeActiveNotification object:NSApp];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidResignActiveNotification:) name:NSApplicationDidResignActiveNotification object:NSApp];
|
|
[self checkForExternalPasteboardChanges];
|
|
idle_callback().add(self);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(NSNotification*)aNotification
|
|
{
|
|
[self saveToDefaults];
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
idle_callback().remove(self);
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidBecomeActiveNotification object:NSApp];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidResignActiveNotification object:NSApp];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationWillTerminateNotification object:NSApp];
|
|
}
|
|
|
|
+ (OakPasteboard*)pasteboardWithName:(NSString*)aName
|
|
{
|
|
static NSMutableDictionary* SharedInstances = [NSMutableDictionary new];
|
|
OakPasteboard* res = SharedInstances[aName];
|
|
if(!res)
|
|
{
|
|
SharedInstances[aName] = res = [[OakPasteboard alloc] initWithName:aName];
|
|
if(![aName isEqualToString:NSGeneralPboard])
|
|
res.avoidsDuplicates = YES;
|
|
}
|
|
|
|
[res checkForExternalPasteboardChanges];
|
|
return res;
|
|
}
|
|
|
|
- (void)addEntry:(OakPasteboardEntry*)anEntry
|
|
{
|
|
D(DBF_Pasteboard, bug("%s (currently at %zu / %zu)\n", [[anEntry string] UTF8String], (size_t)_index, (size_t)[_entries count]););
|
|
[self checkForExternalPasteboardChanges];
|
|
if(_avoidsDuplicates && [[_entries lastObject] isEqual:anEntry])
|
|
{
|
|
_index = [_entries count]-1;
|
|
_auxiliaryOptionsForCurrent = nil;
|
|
}
|
|
else if(!_avoidsDuplicates || ![[self current] isEqual:anEntry])
|
|
{
|
|
[_entries addObject:anEntry];
|
|
[self setIndex:[_entries count]-1];
|
|
}
|
|
[self saveToDefaults];
|
|
}
|
|
|
|
- (OakPasteboardEntry*)previous
|
|
{
|
|
D(DBF_Pasteboard, bug("%zu / %zu\n", (size_t)_index, (size_t)[_entries count]););
|
|
[self checkForExternalPasteboardChanges];
|
|
[self setIndex:_index == 0 ? _index : _index-1];
|
|
return [self current];
|
|
}
|
|
|
|
- (OakPasteboardEntry*)current
|
|
{
|
|
D(DBF_Pasteboard, bug("%zu / %zu\n", (size_t)_index, (size_t)[_entries count]););
|
|
[self checkForExternalPasteboardChanges];
|
|
return [_entries count] == 0 ? nil : _entries[_index];
|
|
}
|
|
|
|
- (OakPasteboardEntry*)next
|
|
{
|
|
D(DBF_Pasteboard, bug("%zu / %zu\n", (size_t)_index, (size_t)[_entries count]););
|
|
[self checkForExternalPasteboardChanges];
|
|
[self setIndex:_index+1 == [_entries count] ? _index : _index+1];
|
|
return [self current];
|
|
}
|
|
|
|
- (void)setEntries:(NSArray*)newEntries
|
|
{
|
|
if(![newEntries isEqual:_entries])
|
|
{
|
|
_entries = [newEntries mutableCopy];
|
|
_index = [_entries count]-1;
|
|
_auxiliaryOptionsForCurrent = nil;
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:OakPasteboardDidChangeNotification object:self];
|
|
[self saveToDefaults];
|
|
}
|
|
}
|
|
|
|
- (BOOL)selectItemAtPosition:(NSPoint)location withWidth:(CGFloat)width respondToSingleClick:(BOOL)singleClick
|
|
{
|
|
[self checkForExternalPasteboardChanges];
|
|
|
|
NSUInteger selectedRow = ([_entries count]-1) - _index;
|
|
OakPasteboardSelector* pasteboardSelector = [OakPasteboardSelector sharedInstance];
|
|
[pasteboardSelector setEntries:[[_entries reverseObjectEnumerator] allObjects]];
|
|
[pasteboardSelector setIndex:selectedRow];
|
|
if(width)
|
|
[pasteboardSelector setWidth:width];
|
|
if(singleClick)
|
|
[pasteboardSelector setPerformsActionOnSingleClick];
|
|
selectedRow = [pasteboardSelector showAtLocation:location];
|
|
|
|
[self setEntries:[[[pasteboardSelector entries] reverseObjectEnumerator] allObjects]];
|
|
[self setIndex:([_entries count]-1) - selectedRow];
|
|
|
|
return [pasteboardSelector shouldSendAction];
|
|
}
|
|
|
|
- (void)selectItemAtPosition:(NSPoint)aLocation andCall:(SEL)aSelector
|
|
{
|
|
if([self selectItemAtPosition:aLocation withWidth:0 respondToSingleClick:NO])
|
|
[NSApp sendAction:aSelector to:nil from:self];
|
|
}
|
|
|
|
- (void)selectItemForControl:(NSView*)controlView
|
|
{
|
|
NSPoint origin = [[controlView window] convertBaseToScreen:[controlView frame].origin];
|
|
[self selectItemAtPosition:origin withWidth:[controlView frame].size.width respondToSingleClick:YES];
|
|
}
|
|
@end
|