BathyScapheで画像のインラインプレビューを可能にするプラグイン
| Revision | 9ad10f9ecb1c550c2951ad9267e3d4439b61bd64 (tree) |
|---|---|
| Time | 2012-05-18 23:05:51 |
| Author | masakih <masakih@user...> |
| Commiter | masakih |
[New] BSIPEReplacerを新設。置換用
| @@ -19,6 +19,7 @@ | ||
| 19 | 19 | F4ADF9591565114F00F666EB /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F468C69C1220CA07009EFA3E /* WebKit.framework */; }; |
| 20 | 20 | F4ADF9671565124200F666EB /* BSInlinePreviewerEx.m in Sources */ = {isa = PBXBuildFile; fileRef = F4ADF9661565124200F666EB /* BSInlinePreviewerEx.m */; }; |
| 21 | 21 | F4ADF9711565150D00F666EB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = F4ADF96F156514F800F666EB /* InfoPlist.strings */; }; |
| 22 | + F4ADF98E1566774F00F666EB /* BSIPEReplacer.m in Sources */ = {isa = PBXBuildFile; fileRef = F4ADF98C15666FAD00F666EB /* BSIPEReplacer.m */; }; | |
| 22 | 23 | F4C8070C0E53CCB000BF4144 /* BSInlinePreviewer.m in Sources */ = {isa = PBXBuildFile; fileRef = F4C8070B0E53CCB000BF4144 /* BSInlinePreviewer.m */; }; |
| 23 | 24 | F4F1DC700E546EC800055177 /* notFound.png in Resources */ = {isa = PBXBuildFile; fileRef = F4F1DC6F0E546EC800055177 /* notFound.png */; }; |
| 24 | 25 | F4FE88A010F772DD0076B366 /* Panel.xib in Resources */ = {isa = PBXBuildFile; fileRef = F4FE889E10F772DD0076B366 /* Panel.xib */; }; |
| @@ -43,6 +44,8 @@ | ||
| 43 | 44 | F4ADF9661565124200F666EB /* BSInlinePreviewerEx.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSInlinePreviewerEx.m; sourceTree = "<group>"; }; |
| 44 | 45 | F4ADF96E156514F800F666EB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; |
| 45 | 46 | F4ADF970156514FE00F666EB /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; }; |
| 47 | + F4ADF98B15666FAD00F666EB /* BSIPEReplacer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BSIPEReplacer.h; path = Ex/BSIPEReplacer.h; sourceTree = "<group>"; }; | |
| 48 | + F4ADF98C15666FAD00F666EB /* BSIPEReplacer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BSIPEReplacer.m; path = Ex/BSIPEReplacer.m; sourceTree = "<group>"; }; | |
| 46 | 49 | F4C807090E53CBE500BF4144 /* BSImagePreviewerInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSImagePreviewerInterface.h; sourceTree = "<group>"; }; |
| 47 | 50 | F4C8070A0E53CCB000BF4144 /* BSInlinePreviewer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSInlinePreviewer.h; sourceTree = "<group>"; }; |
| 48 | 51 | F4C8070B0E53CCB000BF4144 /* BSInlinePreviewer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSInlinePreviewer.m; sourceTree = "<group>"; }; |
| @@ -159,6 +162,8 @@ | ||
| 159 | 162 | children = ( |
| 160 | 163 | F4ADF9651565124200F666EB /* BSInlinePreviewerEx.h */, |
| 161 | 164 | F4ADF9661565124200F666EB /* BSInlinePreviewerEx.m */, |
| 165 | + F4ADF98B15666FAD00F666EB /* BSIPEReplacer.h */, | |
| 166 | + F4ADF98C15666FAD00F666EB /* BSIPEReplacer.m */, | |
| 162 | 167 | F4ADF95F1565115000F666EB /* BSInlinePreviewerEx-Info.plist */, |
| 163 | 168 | F4ADF96F156514F800F666EB /* InfoPlist.strings */, |
| 164 | 169 | ); |
| @@ -306,6 +311,7 @@ | ||
| 306 | 311 | F4ADF9551565114F00F666EB /* BSInlinePreviewer.m in Sources */, |
| 307 | 312 | F4ADF9561565114F00F666EB /* BSILinkInfomation.m in Sources */, |
| 308 | 313 | F4ADF9671565124200F666EB /* BSInlinePreviewerEx.m in Sources */, |
| 314 | + F4ADF98E1566774F00F666EB /* BSIPEReplacer.m in Sources */, | |
| 309 | 315 | ); |
| 310 | 316 | runOnlyForDeploymentPostprocessing = 0; |
| 311 | 317 | }; |
| @@ -9,8 +9,7 @@ | ||
| 9 | 9 | #import "BSInlinePreviewerEx.h" |
| 10 | 10 | |
| 11 | 11 | NSString *const CMRThreadViewerDidChangeThreadNotification = @"CMRThreadViewerDidChangeThreadNotification"; |
| 12 | - | |
| 13 | -static NSString *const BSInlinePreviewerPreviewed = @"BSInlinePreviewerPreviewed"; | |
| 12 | +#import "BSIPEReplacer.h" | |
| 14 | 13 | |
| 15 | 14 | |
| 16 | 15 | @interface BSInlinePreviewer(Private) |
| @@ -42,99 +41,13 @@ static NSString *const BSInlinePreviewerPreviewed = @"BSInlinePreviewerPreviewed | ||
| 42 | 41 | return self; |
| 43 | 42 | } |
| 44 | 43 | |
| 45 | -- (id)keyOfObject:(id)obj | |
| 46 | -{ | |
| 47 | - return [NSString stringWithFormat:@"%p", obj]; | |
| 48 | -} | |
| 49 | -- (void)insertImage:(NSDictionary *)attr | |
| 50 | -{ | |
| 51 | - NSTextView *tv = [attr objectForKey:@"View"]; | |
| 52 | - NSRange range = NSRangeFromString([attr objectForKey:@"Range"]); | |
| 53 | - id newInsertion = [attr objectForKey:@"Image"]; | |
| 54 | - NSUInteger offset = [[attr objectForKey:@"Offset"] unsignedIntegerValue]; | |
| 55 | - range.location += offset; | |
| 56 | - | |
| 57 | - NSTextStorage *ts = [tv textStorage]; | |
| 58 | - [ts beginEditing]; | |
| 59 | - { | |
| 60 | - [ts addAttribute:BSInlinePreviewerPreviewed | |
| 61 | - value:[NSNumber numberWithBool:YES] | |
| 62 | - range:range]; | |
| 63 | - | |
| 64 | - [ts insertAttributedString:newInsertion atIndex:range.location]; | |
| 65 | - } | |
| 66 | - [ts endEditing]; | |
| 67 | -} | |
| 68 | 44 | - (void)viewerDidEndFinishing:(id)no |
| 69 | 45 | { |
| 70 | - NSLog(@"CMRThreadViewerDidChangeThreadNotification %@", no); | |
| 71 | 46 | id threadViewer = [no object]; |
| 72 | - NSTextView *tv = [[threadViewer textView] retain]; | |
| 73 | - @synchronized(tasking) { | |
| 74 | - id check = [tasking objectForKey:[self keyOfObject:tv]]; | |
| 75 | - if(check) return; | |
| 76 | - [tasking setObject:tv forKey:[self keyOfObject:tv]]; | |
| 77 | - } | |
| 78 | - | |
| 79 | - dispatch_async(dispatch_get_global_queue(0,0), ^{ | |
| 80 | - NSUInteger i = 0; | |
| 81 | - while([[tv textStorage] length] == 0) { | |
| 82 | - i++; | |
| 83 | - if(i > NSUIntegerMax - 5) { | |
| 84 | - NSLog(@"Abort"); | |
| 85 | - return; | |
| 86 | - } | |
| 87 | - } | |
| 88 | - sleep(1); | |
| 89 | - NSTextStorage *ts = [[tv textStorage] copy]; | |
| 90 | - | |
| 91 | - NSMutableArray *links = [NSMutableArray array]; | |
| 92 | - | |
| 93 | - [ts enumerateAttribute:NSLinkAttributeName | |
| 94 | - inRange:NSMakeRange(0, [ts length]) | |
| 95 | - options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired | |
| 96 | - usingBlock:^(id value, NSRange range, BOOL *stop) { | |
| 97 | - NSURL *url = [NSURL URLWithString:value]; | |
| 98 | - if([self validateLink:url]) { | |
| 99 | - [links addObject:[NSDictionary dictionaryWithObjectsAndKeys:url, @"Link", | |
| 100 | - NSStringFromRange(range), @"Range", nil]]; | |
| 101 | - } | |
| 102 | - }]; | |
| 103 | - | |
| 104 | - __block NSUInteger offset = 0; | |
| 105 | - NSUInteger count = [links count]; | |
| 106 | - dispatch_apply(count, dispatch_get_global_queue(0,0), ^(size_t index){ | |
| 107 | - id dict = [links objectAtIndex:index]; | |
| 108 | - NSRange range = NSRangeFromString([dict objectForKey:@"Range"]); | |
| 109 | - if([ts attribute:BSInlinePreviewerPreviewed atIndex:range.location longestEffectiveRange:NULL inRange:range]) { | |
| 110 | - return; | |
| 111 | - } | |
| 112 | - // download image. | |
| 113 | - self.totalDownloads = self.remainder = 1; | |
| 114 | - NSImage *image = [self downloadImageURL:[dict objectForKey:@"Link"]]; | |
| 115 | - self.remainder = 0; | |
| 116 | - if(!image) return; | |
| 117 | - | |
| 118 | - id newInsertion = [self attachmentAttributedStringWithImage:image]; | |
| 119 | - | |
| 120 | - | |
| 121 | - NSDictionary *attr = [[NSDictionary alloc] initWithObjectsAndKeys: | |
| 122 | - newInsertion, @"Image", | |
| 123 | - [dict objectForKey:@"Range"], @"Range", | |
| 124 | - tv, @"View", | |
| 125 | - [NSNumber numberWithUnsignedInteger:offset], @"Offset", | |
| 126 | - nil]; | |
| 127 | - [self performSelectorOnMainThread:@selector(insertImage:) withObject:attr waitUntilDone:NO]; | |
| 128 | - offset += [newInsertion length]; | |
| 129 | - [attr release]; | |
| 130 | - }); | |
| 131 | - [ts release]; | |
| 132 | - [tv release]; | |
| 133 | - @synchronized(tasking) { | |
| 134 | - [tasking removeObjectForKey:[self keyOfObject:tv]]; | |
| 135 | - } | |
| 136 | -// NSLog(@"ts \n%@", ts); | |
| 137 | - }); | |
| 47 | + NSTextView *tv = [threadViewer textView]; | |
| 48 | + BSIPEReplacer *replacer = [BSIPEReplacer replaserWithTextView:tv]; | |
| 49 | + replacer.owner = self; | |
| 50 | + replacer.textView = tv; | |
| 138 | 51 | } |
| 139 | 52 | |
| 140 | 53 |
| @@ -0,0 +1,26 @@ | ||
| 1 | +// | |
| 2 | +// BSIPEReplacer.h | |
| 3 | +// BSInlinePreviewer | |
| 4 | +// | |
| 5 | +// Created by 堀 昌樹 on 12/05/18. | |
| 6 | +// Copyright (c) 2012年 __MyCompanyName__. All rights reserved. | |
| 7 | +// | |
| 8 | + | |
| 9 | +#import <Foundation/Foundation.h> | |
| 10 | + | |
| 11 | +#import "BSInlinePreviewerEx.h" | |
| 12 | + | |
| 13 | +@interface BSIPEReplacer : NSObject | |
| 14 | +{ | |
| 15 | + NSLock *lock; | |
| 16 | + NSTextView *_textView; | |
| 17 | + BSInlinePreviewerEx *_owner; | |
| 18 | + | |
| 19 | + BOOL _newStorage; | |
| 20 | + NSUInteger _selfAwaking; | |
| 21 | +} | |
| 22 | +@property (retain) NSTextView *textView; | |
| 23 | +@property (assign) BSInlinePreviewerEx *owner; | |
| 24 | + | |
| 25 | ++ (id)replaserWithTextView:(NSTextView *)tv; | |
| 26 | +@end |
| @@ -0,0 +1,196 @@ | ||
| 1 | +// | |
| 2 | +// BSIPEReplacer.m | |
| 3 | +// BSInlinePreviewer | |
| 4 | +// | |
| 5 | +// Created by 堀 昌樹 on 12/05/18. | |
| 6 | +// Copyright (c) 2012年 __MyCompanyName__. All rights reserved. | |
| 7 | +// | |
| 8 | + | |
| 9 | +#import "BSIPEReplacer.h" | |
| 10 | + | |
| 11 | +@interface BSIPEReplacer () | |
| 12 | +@property BOOL newStorage; | |
| 13 | + | |
| 14 | +@property NSUInteger selfAwaking; | |
| 15 | + | |
| 16 | +@end | |
| 17 | + | |
| 18 | +@implementation BSIPEReplacer | |
| 19 | +@synthesize textView = _textView; | |
| 20 | +@synthesize owner = _owner; | |
| 21 | + | |
| 22 | +@synthesize newStorage = _newStorage; | |
| 23 | +@synthesize selfAwaking = _selfAwaking; | |
| 24 | + | |
| 25 | + | |
| 26 | +static NSMutableDictionary *instances = nil; | |
| 27 | +id keyForTextView(NSTextView *view) | |
| 28 | +{ | |
| 29 | + return [NSString stringWithFormat:@"%p", view]; | |
| 30 | +} | |
| 31 | ++ (id)replaserWithTextView:(NSTextView *)tv | |
| 32 | +{ | |
| 33 | + if(!instances) { | |
| 34 | + instances = [[NSMutableDictionary alloc] init]; | |
| 35 | + } | |
| 36 | + id result = nil; | |
| 37 | + @synchronized(self) { | |
| 38 | + result = [instances objectForKey:keyForTextView(tv)]; | |
| 39 | + [result retain]; | |
| 40 | + } | |
| 41 | + if(result) return [result autorelease]; | |
| 42 | + | |
| 43 | + result = [[[self alloc] init] autorelease]; | |
| 44 | + @synchronized(self) { | |
| 45 | + [instances setObject:result forKey:keyForTextView(tv)]; | |
| 46 | + } | |
| 47 | + return result; | |
| 48 | +} | |
| 49 | + | |
| 50 | +- (void)setTextView:(NSTextView *)textView | |
| 51 | +{ | |
| 52 | + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; | |
| 53 | + [nc removeObserver:self name:nil object:nil]; | |
| 54 | + | |
| 55 | + @synchronized(self) { | |
| 56 | + [_textView release]; | |
| 57 | + _textView = [textView retain]; | |
| 58 | + } | |
| 59 | + if(textView) { | |
| 60 | + [nc addObserver:self | |
| 61 | + selector:@selector(textDidChange:) | |
| 62 | + name:NSTextStorageDidProcessEditingNotification | |
| 63 | + object:[_textView textStorage]]; | |
| 64 | + [nc addObserver:self | |
| 65 | + selector:@selector(windowWillClose:) | |
| 66 | + name:NSWindowWillCloseNotification | |
| 67 | + object:[_textView window]]; | |
| 68 | + } | |
| 69 | + [self textDidChange:nil]; | |
| 70 | +} | |
| 71 | +- (NSTextView *)textView | |
| 72 | +{ | |
| 73 | + id result = nil; | |
| 74 | + @synchronized(self) { | |
| 75 | + result = [_textView retain]; | |
| 76 | + } | |
| 77 | + return [result autorelease]; | |
| 78 | +} | |
| 79 | +- (id)init | |
| 80 | +{ | |
| 81 | + self = [super init]; | |
| 82 | + if(self) { | |
| 83 | + lock = [[NSLock alloc] init]; | |
| 84 | + } | |
| 85 | + return self; | |
| 86 | +} | |
| 87 | +- (void)dealloc | |
| 88 | +{ | |
| 89 | + [_textView release]; | |
| 90 | + [lock release]; | |
| 91 | + | |
| 92 | + [super dealloc]; | |
| 93 | +} | |
| 94 | + | |
| 95 | +NSRange fixRange(NSRange range, NSTextStorage *ts) | |
| 96 | +{ | |
| 97 | + NSRange fixedRange = {0,0}; | |
| 98 | + NSRange searchRange = NSMakeRange(MAX(0, range.location - 20), range.length + 20); | |
| 99 | + searchRange = NSIntersectionRange(searchRange, NSMakeRange(0, [ts length])); | |
| 100 | + [ts attribute:NSLinkAttributeName atIndex:range.location longestEffectiveRange:&fixedRange inRange:searchRange]; | |
| 101 | + return fixedRange; | |
| 102 | +} | |
| 103 | +- (void)insertImage:(NSDictionary *)attr | |
| 104 | +{ | |
| 105 | + NSTextView *tv = [attr objectForKey:@"View"]; | |
| 106 | + NSRange range = NSRangeFromString([attr objectForKey:@"Range"]); | |
| 107 | + id newInsertion = [attr objectForKey:@"Image"]; | |
| 108 | + NSUInteger offset = [[attr objectForKey:@"Offset"] unsignedIntegerValue]; | |
| 109 | + range.location += offset; | |
| 110 | + | |
| 111 | + NSTextStorage *ts = [tv textStorage]; | |
| 112 | + range = fixRange(range, ts); | |
| 113 | + [ts beginEditing]; | |
| 114 | + { | |
| 115 | + [ts addAttribute:BSInlinePreviewerPreviewed | |
| 116 | + value:[NSNumber numberWithBool:YES] | |
| 117 | + range:range]; | |
| 118 | + | |
| 119 | + [ts insertAttributedString:newInsertion atIndex:range.location]; | |
| 120 | + self.selfAwaking++; | |
| 121 | + } | |
| 122 | + [ts endEditing]; | |
| 123 | +} | |
| 124 | +- (void)textDidChange:(NSNotification *)no | |
| 125 | +{ | |
| 126 | + NSUInteger length = [[self.textView textStorage] length]; | |
| 127 | + if(length == 0) { | |
| 128 | + self.newStorage = YES; | |
| 129 | + self.selfAwaking = 0; | |
| 130 | + return; | |
| 131 | + } | |
| 132 | + if(self.selfAwaking > 0) { | |
| 133 | + self.selfAwaking--; | |
| 134 | + return; | |
| 135 | + } | |
| 136 | + | |
| 137 | + NSTextStorage *ts = [[self.textView textStorage] copy]; | |
| 138 | + dispatch_async(dispatch_get_global_queue(0,0), ^{ | |
| 139 | + | |
| 140 | + [lock lock]; | |
| 141 | + | |
| 142 | + | |
| 143 | + NSMutableArray *links = [NSMutableArray array]; | |
| 144 | + | |
| 145 | + [ts enumerateAttribute:NSLinkAttributeName | |
| 146 | + inRange:NSMakeRange(0, [ts length]) | |
| 147 | + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired | |
| 148 | + usingBlock:^(id value, NSRange range, BOOL *stop) { | |
| 149 | + NSURL *url = [NSURL URLWithString:value]; | |
| 150 | + if([self.owner validateLink:url]) { | |
| 151 | + [links addObject:[NSDictionary dictionaryWithObjectsAndKeys:url, @"Link", | |
| 152 | + NSStringFromRange(range), @"Range", nil]]; | |
| 153 | + } | |
| 154 | + }]; | |
| 155 | + | |
| 156 | + __block NSUInteger offset = 0; | |
| 157 | + NSUInteger count = [links count]; | |
| 158 | + dispatch_apply(count, dispatch_get_global_queue(0,0), ^(size_t index){ | |
| 159 | + if(!self.textView) return; | |
| 160 | + | |
| 161 | + id dict = [links objectAtIndex:index]; | |
| 162 | + NSRange range = NSRangeFromString([dict objectForKey:@"Range"]); | |
| 163 | + if([ts attribute:BSInlinePreviewerPreviewed atIndex:range.location longestEffectiveRange:NULL inRange:range]) { | |
| 164 | + return; | |
| 165 | + } | |
| 166 | + // download image. | |
| 167 | + NSImage *image = [self.owner downloadImageURL:[dict objectForKey:@"Link"]]; | |
| 168 | + if(!image) return; | |
| 169 | + | |
| 170 | + id newInsertion = [self.owner attachmentAttributedStringWithImage:image]; | |
| 171 | + | |
| 172 | + | |
| 173 | + NSDictionary *attr = [[NSDictionary alloc] initWithObjectsAndKeys: | |
| 174 | + newInsertion, @"Image", | |
| 175 | + [dict objectForKey:@"Range"], @"Range", | |
| 176 | + self.textView, @"View", | |
| 177 | + [NSNumber numberWithUnsignedInteger:offset], @"Offset", | |
| 178 | + nil]; | |
| 179 | + [self performSelectorOnMainThread:@selector(insertImage:) withObject:attr waitUntilDone:NO]; | |
| 180 | + offset += [newInsertion length]; | |
| 181 | + [attr release]; | |
| 182 | + }); | |
| 183 | + [ts release]; | |
| 184 | + // NSLog(@"ts \n%@", ts); | |
| 185 | + | |
| 186 | + [lock unlock]; | |
| 187 | + }); | |
| 188 | +} | |
| 189 | + | |
| 190 | +- (void)windowWillClose:(NSNotification *)notification | |
| 191 | +{ | |
| 192 | + [[self retain] autorelease]; | |
| 193 | + [instances removeObjectForKey:keyForTextView(_textView)]; | |
| 194 | + self.textView = nil; | |
| 195 | +} | |
| 196 | +@end |