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 |