]> git.lyx.org Git - lyx.git/blob - src/support/linkback/LinkBack.m
* LinkBack support files, now in pure Objective-C (without any Objective-C++ which...
[lyx.git] / src / support / linkback / LinkBack.m
1 //
2 //  LinkBack.m
3 //  LinkBack
4 //
5 //  Created by Charles Jolley on Tue Jun 15 2004.
6 //  Copyright (c) 2004, Nisus Software, Inc.
7 //  All rights reserved.
8
9 //  Redistribution and use in source and binary forms, with or without 
10 //  modification, are permitted provided that the following conditions are met:
11 //
12 //  Redistributions of source code must retain the above copyright notice, 
13 //  this list of conditions and the following disclaimer.
14 //
15 //  Redistributions in binary form must reproduce the above copyright notice, 
16 //  this list of conditions and the following disclaimer in the documentation 
17 //  and/or other materials provided with the distribution.
18 //
19 //  Neither the name of the Nisus Software, Inc. nor the names of its 
20 //  contributors may be used to endorse or promote products derived from this 
21 //  software without specific prior written permission.
22 //
23 //  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
24 //  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
25 //  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
26 //  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
27 //  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
28 //  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
29 //  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
30 //  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
31 //  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
32 //  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
33 //  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 //
35
36 #import "LinkBack.h"
37 #import "LinkBackServer.h"
38
39 NSString* LinkBackPboardType = @"LinkBackData" ;
40
41 // LinkBack data keys.  These are used in a LinkBack object, which is currently a dictionary.  Do not depend on these values.  They are public for testing purposes only.
42 NSString* LinkBackServerActionKey = @"serverActionKey" ;
43 NSString* LinkBackServerApplicationNameKey = @"serverAppName" ;
44 NSString* LinkBackServerNameKey = @"serverName" ;
45 NSString* LinkBackServerBundleIdentifierKey = @"bundleId" ;
46 NSString* LinkBackVersionKey = @"version" ;
47 NSString* LinkBackApplicationDataKey = @"appData" ;
48 NSString* LinkBackSuggestedRefreshKey = @"refresh" ;
49 NSString* LinkBackApplicationURLKey = @"ApplicationURL" ;
50
51 NSString* LinkBackEditActionName = @"_Edit" ;
52 NSString* LinkBackRefreshActionName = @"_Refresh" ;
53
54 // ...........................................................................
55 // Support Functions
56 //
57
58 id MakeLinkBackData(NSString* serverName, id appData) 
59 {
60         return [NSDictionary linkBackDataWithServerName: serverName appData: appData] ;
61 }
62
63 id LinkBackGetAppData(id LinkBackData) 
64 {
65         return [LinkBackData linkBackAppData] ;
66 }
67
68 NSString* LinkBackUniqueItemKey() 
69 {
70     static int counter = 0 ;
71     
72     NSString* base = [[NSBundle mainBundle] bundleIdentifier] ;
73     unsigned long time = [NSDate timeIntervalSinceReferenceDate] ;
74     return [NSString stringWithFormat: @"%@%.8x.%.4x",base,time,counter++] ;
75 }
76
77 BOOL LinkBackDataBelongsToActiveApplication(id data) 
78 {
79         return [data linkBackDataBelongsToActiveApplication] ;
80 }
81
82 NSString* LinkBackEditMultipleMenuTitle() 
83 {
84         NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
85         NSString* ret = [bundle localizedStringForKey: @"_EditMultiple" value: @"Edit LinkBack Items" table: @"Localized"] ;
86         return ret ;
87 }
88
89 NSString* LinkBackEditNoneMenuTitle() 
90 {
91         NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
92         NSString* ret = [bundle localizedStringForKey: @"_EditNone" value: @"Edit LinkBack Item" table: @"Localized"] ;
93         return ret ;
94 }
95
96 // ...........................................................................
97 // LinkBack Data Category
98 //
99
100 // Use these methods to create and access linkback data objects.  You can also use the helper functions above.
101
102 @implementation NSDictionary (LinkBackData)
103
104 + (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData 
105 {
106         return [self linkBackDataWithServerName: serverName appData: appData actionName: nil suggestedRefreshRate: 0];
107 }
108
109 + (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData suggestedRefreshRate:(NSTimeInterval)rate 
110 {
111         return [self linkBackDataWithServerName: serverName appData: appData actionName: LinkBackRefreshActionName suggestedRefreshRate: rate] ;
112 }
113
114 + (NSDictionary*)linkBackDataWithServerName:(NSString*)serverName appData:(id)appData actionName:(NSString*)action suggestedRefreshRate:(NSTimeInterval)rate ;
115 {
116         NSDictionary* appInfo = [[NSBundle mainBundle] infoDictionary] ;
117
118     NSMutableDictionary* ret = [[NSMutableDictionary alloc] init] ;
119     NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier] ;
120         NSString* url = [appInfo objectForKey: @"LinkBackApplicationURL"] ;
121         NSString* appName = [[NSProcessInfo processInfo] processName] ;
122         id version = @"A" ;
123
124         if (nil==serverName) [NSException raise: NSInvalidArgumentException format: @"LinkBack Data cannot be created without a server name."] ;
125         
126         // callback information
127         [ret setObject: bundleId forKey: LinkBackServerBundleIdentifierKey]; 
128     [ret setObject: serverName forKey: LinkBackServerNameKey] ;
129     [ret setObject: version forKey: LinkBackVersionKey] ;
130         
131         // additional information
132         if (appName) [ret setObject: appName forKey: LinkBackServerApplicationNameKey] ;
133         if (action) [ret setObject: action forKey: LinkBackServerActionKey] ;
134     if (appData) [ret setObject: appData forKey: LinkBackApplicationDataKey] ;
135         if (url) [ret setObject: url forKey: LinkBackApplicationURLKey] ;
136         [ret setObject: [NSNumber numberWithFloat: rate] forKey: LinkBackSuggestedRefreshKey] ;
137         
138     return [ret autorelease] ;
139 }
140
141 - (BOOL)linkBackDataBelongsToActiveApplication 
142 {
143     NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier] ;
144     NSString* dataId = [self objectForKey: LinkBackServerBundleIdentifierKey] ;
145     return (dataId && [dataId isEqualToString: bundleId]) ;
146 }
147
148 - (id)linkBackAppData 
149 {
150         return [self objectForKey: LinkBackApplicationDataKey] ;
151 }
152
153 - (NSString*)linkBackSourceApplicationName 
154 {
155         return [self objectForKey: LinkBackServerApplicationNameKey] ;
156 }
157
158 - (NSString*)linkBackActionName 
159 {
160         NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
161         NSString* ret = [self objectForKey: LinkBackServerActionKey] ;
162         if (nil==ret) ret = LinkBackEditActionName ;
163         
164         ret = [bundle localizedStringForKey: ret value: ret table: @"Localized"] ;
165         return ret ;
166 }
167
168 - (NSString*)linkBackEditMenuTitle
169 {
170         NSBundle* bundle = [NSBundle bundleForClass: [LinkBack class]] ;
171         NSString* appName = [self linkBackSourceApplicationName] ;
172         NSString* action = [self linkBackActionName] ;
173         NSString* ret = [bundle localizedStringForKey: @"_EditPattern" value: @"%@ in %@" table: @"Localized"] ;
174         ret = [NSString stringWithFormat: ret, action, appName] ;
175         return ret ;
176 }
177
178 - (NSString*)linkBackVersion 
179 {
180         return [self objectForKey: LinkBackVersionKey] ;
181 }
182
183 - (NSTimeInterval)linkBackSuggestedRefreshRate 
184 {
185         id obj = [self objectForKey: LinkBackSuggestedRefreshKey] ;
186         return (obj) ? [obj floatValue] : 0 ;
187 }
188
189 - (NSURL*)linkBackApplicationURL 
190 {
191         id obj = [self objectForKey: LinkBackApplicationURLKey] ;
192         if (obj) obj = [NSURL URLWithString: obj] ;
193         return obj ;
194 }
195
196 @end
197
198 // ...........................................................................
199 // LinkBackServer 
200 //
201 // one of these exists for each registered server name.  This is the receiver of server requests.
202
203 // ...........................................................................
204 // LinkBack Class
205 //
206
207 NSMutableDictionary* keyedLinkBacks = nil ;
208
209 @implementation LinkBack
210
211 + (void)initialize
212 {
213     static BOOL inited = NO ;
214     if (inited) return ;
215     inited=YES; [super initialize] ;
216     keyedLinkBacks = [[NSMutableDictionary alloc] init] ;
217 }
218
219 + (LinkBack*)activeLinkBackForItemKey:(id)aKey 
220 {
221     return [keyedLinkBacks objectForKey: aKey] ;
222 }
223
224 - (id)initServerWithClient: (LinkBack*)aLinkBack delegate: (id<LinkBackServerDelegate>)aDel 
225 {
226     if (self = [super init]) {
227         peer = [aLinkBack retain] ;
228         sourceName = [[peer sourceName] copy] ;
229                 sourceApplicationName = [[peer sourceApplicationName] copy] ;
230         key = [[peer itemKey] copy] ;
231         isServer = YES ;
232         delegate = aDel ;
233         [keyedLinkBacks setObject: self forKey: key] ;
234     }
235     
236     return self ;
237 }
238
239 - (id)initClientWithSourceName:(NSString*)aName delegate:(id<LinkBackClientDelegate>)aDel itemKey:(NSString*)aKey ;
240 {
241     if (self = [super init]) {
242         isServer = NO ;
243         delegate = aDel ;
244         sourceName = [aName copy] ;
245                 sourceApplicationName = [[NSProcessInfo processInfo] processName] ;
246         pboard = [[NSPasteboard pasteboardWithUniqueName] retain] ;
247         key = [aKey copy] ;
248     }
249     
250     return self ;
251 }
252
253 - (void)dealloc
254 {
255     [repobj release] ;
256     [sourceName release] ;
257     
258     if (peer) [self closeLink] ;
259     [peer release] ;
260     
261     if (!isServer) [pboard releaseGlobally] ; // client owns the pboard.
262     [pboard release] ;
263     
264     [super dealloc] ;
265 }
266
267 // ...........................................................................
268 // General Use methods
269
270 - (NSPasteboard*)pasteboard 
271 {
272     return pboard ;
273 }
274
275 - (id)representedObject 
276 {
277     return repobj ;
278 }
279
280 - (void)setRepresentedObject:(id)obj 
281 {
282     [obj retain] ;
283     [repobj release] ;
284     repobj = obj ;
285 }
286
287 - (NSString*)sourceName
288 {
289     return sourceName ;
290 }
291
292 - (NSString*)sourceApplicationName 
293 {
294         return sourceApplicationName ;
295 }
296
297 - (NSString*)itemKey
298 {
299     return key ;
300 }
301
302 // this method is called to initial a link closure from this side.
303 - (void)closeLink 
304 {
305     // inform peer of closure
306     if (peer) {
307         [peer remoteCloseLink] ; 
308         [peer release] ;
309         peer = nil ;
310         [self release] ;
311         [keyedLinkBacks removeObjectForKey: [self itemKey]]; 
312     }
313 }
314
315 // this method is called whenever the link is about to be or has been closed by the other side.
316 - (void)remoteCloseLink 
317 {
318     if (peer) {
319         [peer release] ;
320         peer = nil ;
321         [self release] ;
322         [keyedLinkBacks removeObjectForKey: [self itemKey]]; 
323     }
324
325     if (delegate) [delegate linkBackDidClose: self] ;
326 }
327
328 // ...........................................................................
329 // Server-side methods
330 //
331 + (BOOL)publishServerWithName:(NSString*)name delegate:(id<LinkBackServerDelegate>)del 
332 {
333     return [LinkBackServer publishServerWithName: name delegate: del] ;
334 }
335
336 + (void)retractServerWithName:(NSString*)name 
337 {
338     LinkBackServer* server = [LinkBackServer LinkBackServerWithName: name] ;
339     if (server) [server retract] ;
340 }
341
342 - (void)sendEdit 
343 {
344     if (!peer) [NSException raise: NSGenericException format: @"tried to request edit from a live link not connect to a server."] ;
345     [peer refreshEditWithPasteboardName: [pboard name]] ;
346 }
347
348 // FROM CLIENT LinkBack
349 - (void)requestEditWithPasteboardName:(bycopy NSString*)pboardName
350 {
351     // get the new pasteboard, if needed
352     if ((!pboard) || ![pboardName isEqualToString: [pboard name]]) pboard = [[NSPasteboard pasteboardWithName: pboardName] retain] ;
353
354     // pass onto delegate
355         [delegate performSelectorOnMainThread: @selector(linkBackClientDidRequestEdit:) withObject: self waitUntilDone: NO] ;
356 }
357
358 // ...........................................................................
359 // Client-Side Methods
360 //
361 + (LinkBack*)editLinkBackData:(id)data sourceName:(NSString*)aName delegate:(id<LinkBackClientDelegate>)del itemKey:(NSString*)aKey
362 {
363     // if an active live link already exists, use that.  Otherwise, create a new one.
364     LinkBack* ret = [keyedLinkBacks objectForKey: aKey] ;
365     
366     if(nil==ret) {
367         BOOL ok ;
368         NSString* serverName ;
369         NSString* serverId ;
370         NSString* appName ;
371                 NSURL* url ;
372                 
373         // collect server contact information from data.
374         ok = [data isKindOfClass: [NSDictionary class]] ;
375         if (ok) {
376             serverName = [data objectForKey: LinkBackServerNameKey] ;
377             serverId = [data objectForKey: LinkBackServerBundleIdentifierKey];
378                         appName = [data linkBackSourceApplicationName] ;
379                         url = [data linkBackApplicationURL] ;
380         }
381         
382         if (!ok || !serverName || !serverId) [NSException raise: NSInvalidArgumentException format: @"LinkBackData is not of the correct format: %@", data] ;
383         
384         // create the live link object and try to connect to the server.
385         ret = [[LinkBack alloc] initClientWithSourceName: aName delegate: del itemKey: aKey] ;
386         
387         if (![ret connectToServerWithName: serverName inApplication: serverId fallbackURL: url appName: appName]) {
388             [ret release] ;
389             ret = nil ;
390         }
391     }
392     
393     // now with a live link in hand, request an edit
394     if (ret) {
395         // if connected to server, publish data and inform server.
396         NSPasteboard* my_pboard = [ret pasteboard] ;
397         [my_pboard declareTypes: [NSArray arrayWithObject: LinkBackPboardType] owner: ret] ;
398         [my_pboard setPropertyList: data forType: LinkBackPboardType] ;
399         
400         [ret requestEdit] ;
401         
402     // if connection to server failed, return nil.
403     } else {
404         [ret release] ;
405         ret = nil ;
406     }
407     
408     return ret ;
409 }
410
411 - (BOOL)connectToServerWithName:(NSString*)aName inApplication:(NSString*)bundleIdentifier fallbackURL:(NSURL*)url appName:(NSString*)appName 
412 {
413     // get the LinkBackServer.
414     LinkBackServer* server = [LinkBackServer LinkBackServerWithName: aName inApplication: bundleIdentifier launchIfNeeded: YES fallbackURL: url appName: appName] ;
415     if (!server) return NO ; // failed to get server
416     
417     peer = [[server initiateLinkBackFromClient: self] retain] ;
418     if (!peer) return NO ; // failed to initiate session
419     
420     // if we connected, then add to the list of active keys
421     [keyedLinkBacks setObject: self forKey: [self itemKey]] ;
422     
423     return YES ;
424 }
425
426 - (void)requestEdit 
427 {
428     if (!peer) [NSException raise: NSGenericException format: @"tried to request edit from a live link not connect to a server."] ;
429     [peer requestEditWithPasteboardName: [pboard name]] ;
430 }
431
432 // RECEIVED FROM SERVER
433 - (void)refreshEditWithPasteboardName:(bycopy NSString*)pboardName
434 {
435     // if pboard has changes, change to new pboard.
436     if (![pboardName isEqualToString: [pboard name]]) {
437         [pboard release] ;
438         pboard = [[NSPasteboard pasteboardWithName: pboardName] retain] ;
439     } 
440     
441     // inform delegate
442         [delegate performSelectorOnMainThread: @selector(linkBackServerDidSendEdit:) withObject: self waitUntilDone: NO] ;
443 }
444
445 @end