Commit | Line | Data |
---|---|---|
a09e091a JB |
1 | /* x-selection.m -- proxies between NSPasteboard and X11 selections |
2 | * | |
3 | * Copyright (c) 2002-2012 Apple Inc. All rights reserved. | |
4 | * | |
5 | * Permission is hereby granted, free of charge, to any person | |
6 | * obtaining a copy of this software and associated documentation files | |
7 | * (the "Software"), to deal in the Software without restriction, | |
8 | * including without limitation the rights to use, copy, modify, merge, | |
9 | * publish, distribute, sublicense, and/or sell copies of the Software, | |
10 | * and to permit persons to whom the Software is furnished to do so, | |
11 | * subject to the following conditions: | |
12 | * | |
13 | * The above copyright notice and this permission notice shall be | |
14 | * included in all copies or substantial portions of the Software. | |
15 | * | |
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT | |
20 | * HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
23 | * DEALINGS IN THE SOFTWARE. | |
24 | * | |
25 | * Except as contained in this notice, the name(s) of the above | |
26 | * copyright holders shall not be used in advertising or otherwise to | |
27 | * promote the sale, use or other dealings in this Software without | |
28 | * prior written authorization. | |
29 | */ | |
30 | ||
31 | #import "x-selection.h" | |
32 | ||
33 | #include <stdio.h> | |
34 | #include <stdlib.h> | |
35 | #include <X11/Xatom.h> | |
36 | #include <X11/Xutil.h> | |
37 | #import <AppKit/NSGraphics.h> | |
38 | #import <AppKit/NSImage.h> | |
39 | #import <AppKit/NSBitmapImageRep.h> | |
40 | ||
41 | /* | |
42 | * The basic design of the pbproxy code is as follows. | |
43 | * | |
44 | * When a client selects text, say from an xterm - we only copy it when the | |
45 | * X11 Edit->Copy menu item is pressed or the shortcut activated. In this | |
46 | * case we take the PRIMARY selection, and set it as the NSPasteboard data. | |
47 | * | |
48 | * When an X11 client copies something to the CLIPBOARD, pbproxy greedily grabs | |
49 | * the data, sets it as the NSPasteboard data, and finally sets itself as | |
50 | * owner of the CLIPBOARD. | |
51 | * | |
52 | * When an X11 window is activated we check to see if the NSPasteboard has | |
53 | * changed. If the NSPasteboard has changed, then we set pbproxy as owner | |
54 | * of the PRIMARY and CLIPBOARD and respond to requests for text and images. | |
55 | * | |
56 | * The behavior is now dynamic since the information above was written. | |
57 | * The behavior is now dependent on the pbproxy_prefs below. | |
58 | */ | |
59 | ||
60 | /* | |
61 | * TODO: | |
62 | * 1. handle MULTIPLE - I need to study the ICCCM further, and find a test app. | |
63 | * 2. Handle NSPasteboard updates immediately, not on active/inactive | |
64 | * - Open xterm, run 'cat readme.txt | pbcopy' | |
65 | */ | |
66 | ||
67 | static struct { | |
68 | BOOL active; | |
69 | BOOL primary_on_grab; /* This is provided as an option for people who | |
70 | * want it and has issues that won't ever be | |
71 | * addressed to make it *always* work. | |
72 | */ | |
73 | BOOL clipboard_to_pasteboard; | |
74 | BOOL pasteboard_to_primary; | |
75 | BOOL pasteboard_to_clipboard; | |
76 | } pbproxy_prefs = { YES, NO, YES, YES, YES }; | |
77 | ||
78 | @implementation x_selection | |
79 | ||
80 | static struct propdata null_propdata = { | |
81 | NULL, 0, 0 | |
82 | }; | |
83 | ||
84 | #ifdef DEBUG | |
85 | static void | |
86 | dump_prefs() | |
87 | { | |
88 | ErrorF("pbproxy preferences:\n" | |
89 | "\tactive %u\n" | |
90 | "\tprimary_on_grab %u\n" | |
91 | "\tclipboard_to_pasteboard %u\n" | |
92 | "\tpasteboard_to_primary %u\n" | |
93 | "\tpasteboard_to_clipboard %u\n", | |
94 | pbproxy_prefs.active, | |
95 | pbproxy_prefs.primary_on_grab, | |
96 | pbproxy_prefs.clipboard_to_pasteboard, | |
97 | pbproxy_prefs.pasteboard_to_primary, | |
98 | pbproxy_prefs.pasteboard_to_clipboard); | |
99 | } | |
100 | #endif | |
101 | ||
102 | extern CFStringRef app_prefs_domain_cfstr; | |
103 | ||
104 | static BOOL | |
105 | prefs_get_bool(CFStringRef key, BOOL defaultValue) | |
106 | { | |
107 | Boolean value, ok; | |
108 | ||
109 | value = CFPreferencesGetAppBooleanValue(key, app_prefs_domain_cfstr, &ok); | |
110 | ||
111 | return ok ? (BOOL)value : defaultValue; | |
112 | } | |
113 | ||
114 | static void | |
115 | init_propdata(struct propdata *pdata) | |
116 | { | |
117 | *pdata = null_propdata; | |
118 | } | |
119 | ||
120 | static void | |
121 | free_propdata(struct propdata *pdata) | |
122 | { | |
123 | free(pdata->data); | |
124 | *pdata = null_propdata; | |
125 | } | |
126 | ||
127 | /* | |
128 | * Return True if an error occurs. Return False if pdata has data | |
129 | * and we finished. | |
130 | * The property is only deleted when bytesleft is 0 if delete is True. | |
131 | */ | |
132 | static Bool | |
133 | get_property(Window win, Atom property, struct propdata *pdata, Bool delete, | |
134 | Atom *type) | |
135 | { | |
136 | long offset = 0; | |
137 | unsigned long numitems, bytesleft = 0; | |
138 | #ifdef TEST | |
139 | /* This is used to test the growth handling. */ | |
140 | unsigned long length = 4UL; | |
141 | #else | |
142 | unsigned long length = (100000UL + 3) / 4; | |
143 | #endif | |
144 | unsigned char *buf = NULL, *chunk = NULL; | |
145 | size_t buflen = 0, chunkbytesize = 0; | |
146 | int format; | |
147 | ||
148 | TRACE(); | |
149 | ||
150 | if (None == property) | |
151 | return True; | |
152 | ||
153 | do { | |
154 | unsigned long newbuflen = 0; | |
155 | unsigned char *newbuf = NULL; | |
156 | ||
157 | #ifdef TEST | |
158 | ErrorF("bytesleft %lu\n", bytesleft); | |
159 | #endif | |
160 | ||
161 | if (Success != XGetWindowProperty(xpbproxy_dpy, win, property, | |
162 | offset, length, delete, | |
163 | AnyPropertyType, | |
164 | type, &format, &numitems, | |
165 | &bytesleft, &chunk)) { | |
166 | DebugF("Error while getting window property.\n"); | |
167 | *pdata = null_propdata; | |
168 | free(buf); | |
169 | return True; | |
170 | } | |
171 | ||
172 | #ifdef TEST | |
173 | ErrorF("format %d numitems %lu bytesleft %lu\n", | |
174 | format, numitems, bytesleft); | |
175 | ||
176 | ErrorF("type %s\n", XGetAtomName(xpbproxy_dpy, *type)); | |
177 | #endif | |
178 | ||
179 | /* Format is the number of bits. */ | |
180 | if (format == 8) | |
181 | chunkbytesize = numitems; | |
182 | else if (format == 16) | |
183 | chunkbytesize = numitems * sizeof(short); | |
184 | else if (format == 32) | |
185 | chunkbytesize = numitems * sizeof(long); | |
186 | ||
187 | #ifdef TEST | |
188 | ErrorF("chunkbytesize %zu\n", chunkbytesize); | |
189 | #endif | |
190 | newbuflen = buflen + chunkbytesize; | |
191 | if (newbuflen > 0) { | |
192 | newbuf = realloc(buf, newbuflen); | |
193 | ||
194 | if (NULL == newbuf) { | |
195 | XFree(chunk); | |
196 | free(buf); | |
197 | return True; | |
198 | } | |
199 | ||
200 | memcpy(newbuf + buflen, chunk, chunkbytesize); | |
201 | XFree(chunk); | |
202 | buf = newbuf; | |
203 | buflen = newbuflen; | |
204 | /* offset is a multiple of 32 bits*/ | |
205 | offset += chunkbytesize / 4; | |
206 | } | |
207 | else { | |
208 | if (chunk) | |
209 | XFree(chunk); | |
210 | } | |
211 | ||
212 | #ifdef TEST | |
213 | ErrorF("bytesleft %lu\n", bytesleft); | |
214 | #endif | |
215 | } while (bytesleft > 0); | |
216 | ||
217 | pdata->data = buf; | |
218 | pdata->length = buflen; | |
219 | pdata->format = format; | |
220 | ||
221 | return /*success*/ False; | |
222 | } | |
223 | ||
224 | /* Implementation methods */ | |
225 | ||
226 | /* This finds the preferred type from a TARGETS list.*/ | |
227 | - (Atom) find_preferred:(struct propdata *)pdata | |
228 | { | |
229 | Atom a = None; | |
230 | size_t i, step; | |
231 | Bool png = False, jpeg = False, utf8 = False, string = False; | |
232 | ||
233 | TRACE(); | |
234 | ||
235 | if (pdata->format != 32) { | |
236 | ErrorF( | |
237 | "Atom list is expected to be formatted as an array of 32bit values.\n"); | |
238 | return None; | |
239 | } | |
240 | ||
241 | for (i = 0, step = sizeof(long); i < pdata->length; i += step) { | |
242 | a = (Atom) * (long *)(pdata->data + i); | |
243 | ||
244 | if (a == atoms->image_png) { | |
245 | png = True; | |
246 | } | |
247 | else if (a == atoms->image_jpeg) { | |
248 | jpeg = True; | |
249 | } | |
250 | else if (a == atoms->utf8_string) { | |
251 | utf8 = True; | |
252 | } | |
253 | else if (a == atoms->string) { | |
254 | string = True; | |
255 | } | |
256 | else { | |
257 | char *type = XGetAtomName(xpbproxy_dpy, a); | |
258 | if (type) { | |
259 | DebugF("Unhandled X11 mime type: %s", type); | |
260 | XFree(type); | |
261 | } | |
262 | } | |
263 | } | |
264 | ||
265 | /*We prefer PNG over strings, and UTF8 over a Latin-1 string.*/ | |
266 | if (png) | |
267 | return atoms->image_png; | |
268 | ||
269 | if (jpeg) | |
270 | return atoms->image_jpeg; | |
271 | ||
272 | if (utf8) | |
273 | return atoms->utf8_string; | |
274 | ||
275 | if (string) | |
276 | return atoms->string; | |
277 | ||
278 | /* This is evidently something we don't know how to handle.*/ | |
279 | return None; | |
280 | } | |
281 | ||
282 | /* Return True if this is an INCR-style transfer. */ | |
283 | - (Bool) is_incr_type:(XSelectionEvent *)e | |
284 | { | |
285 | Atom seltype; | |
286 | int format; | |
287 | unsigned long numitems = 0UL, bytesleft = 0UL; | |
288 | unsigned char *chunk; | |
289 | ||
290 | TRACE(); | |
291 | ||
292 | if (Success != XGetWindowProperty(xpbproxy_dpy, e->requestor, e->property, | |
293 | /*offset*/ 0L, /*length*/ 4UL, | |
294 | /*Delete*/ False, | |
295 | AnyPropertyType, &seltype, &format, | |
296 | &numitems, &bytesleft, &chunk)) { | |
297 | return False; | |
298 | } | |
299 | ||
300 | if (chunk) | |
301 | XFree(chunk); | |
302 | ||
303 | return (seltype == atoms->incr) ? True : False; | |
304 | } | |
305 | ||
306 | /* | |
307 | * This should be called after a selection has been copied, | |
308 | * or when the selection is unfinished before a transfer completes. | |
309 | */ | |
310 | - (void) release_pending | |
311 | { | |
312 | TRACE(); | |
313 | ||
314 | free_propdata(&pending.propdata); | |
315 | pending.requestor = None; | |
316 | pending.selection = None; | |
317 | } | |
318 | ||
319 | /* Return True if an error occurs during an append.*/ | |
320 | /* Return False if the append succeeds. */ | |
321 | - (Bool) append_to_pending:(struct propdata *)pdata requestor:(Window) | |
322 | requestor | |
323 | { | |
324 | unsigned char *newdata; | |
325 | size_t newlength; | |
326 | ||
327 | TRACE(); | |
328 | ||
329 | if (requestor != pending.requestor) { | |
330 | [self release_pending]; | |
331 | pending.requestor = requestor; | |
332 | } | |
333 | ||
334 | newlength = pending.propdata.length + pdata->length; | |
335 | newdata = realloc(pending.propdata.data, newlength); | |
336 | ||
337 | if (NULL == newdata) { | |
338 | perror("realloc propdata"); | |
339 | [self release_pending]; | |
340 | return True; | |
341 | } | |
342 | ||
343 | memcpy(newdata + pending.propdata.length, pdata->data, pdata->length); | |
344 | pending.propdata.data = newdata; | |
345 | pending.propdata.length = newlength; | |
346 | ||
347 | return False; | |
348 | } | |
349 | ||
350 | /* Called when X11 becomes active (i.e. has key focus) */ | |
351 | - (void) x_active:(Time)timestamp | |
352 | { | |
353 | static NSInteger changeCount; | |
354 | NSInteger countNow; | |
355 | NSPasteboard *pb; | |
356 | ||
357 | TRACE(); | |
358 | ||
359 | pb = [NSPasteboard generalPasteboard]; | |
360 | ||
361 | if (nil == pb) | |
362 | return; | |
363 | ||
364 | countNow = [pb changeCount]; | |
365 | ||
366 | if (countNow != changeCount) { | |
367 | DebugF("changed pasteboard!\n"); | |
368 | changeCount = countNow; | |
369 | ||
370 | if (pbproxy_prefs.pasteboard_to_primary) { | |
371 | XSetSelectionOwner(xpbproxy_dpy, atoms->primary, | |
372 | _selection_window, | |
373 | CurrentTime); | |
374 | } | |
375 | ||
376 | if (pbproxy_prefs.pasteboard_to_clipboard) { | |
377 | [self own_clipboard]; | |
378 | } | |
379 | } | |
380 | ||
381 | #if 0 | |
382 | /*gstaplin: we should perhaps investigate something like this branch above...*/ | |
383 | if ([_pasteboard availableTypeFromArray: _known_types] != nil) { | |
384 | /* Pasteboard has data we should proxy; I think it makes | |
385 | sense to put it on both CLIPBOARD and PRIMARY */ | |
386 | ||
387 | XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard, | |
388 | _selection_window, timestamp); | |
389 | XSetSelectionOwner(xpbproxy_dpy, atoms->primary, | |
390 | _selection_window, timestamp); | |
391 | } | |
392 | #endif | |
393 | } | |
394 | ||
395 | /* Called when X11 loses key focus */ | |
396 | - (void) x_inactive:(Time)timestamp | |
397 | { | |
398 | TRACE(); | |
399 | } | |
400 | ||
401 | /* This requests the TARGETS list from the PRIMARY selection owner. */ | |
402 | - (void) x_copy_request_targets | |
403 | { | |
404 | TRACE(); | |
405 | ||
406 | request_atom = atoms->targets; | |
407 | XConvertSelection(xpbproxy_dpy, atoms->primary, atoms->targets, | |
408 | atoms->primary, _selection_window, CurrentTime); | |
409 | } | |
410 | ||
411 | /* Called when the Edit/Copy item on the main X11 menubar is selected | |
412 | * and no appkit window claims it. */ | |
413 | - (void) x_copy:(Time)timestamp | |
414 | { | |
415 | Window w; | |
416 | ||
417 | TRACE(); | |
418 | ||
419 | w = XGetSelectionOwner(xpbproxy_dpy, atoms->primary); | |
420 | ||
421 | if (None != w) { | |
422 | ++pending_copy; | |
423 | ||
424 | if (1 == pending_copy) { | |
425 | /* | |
426 | * There are no other copy operations in progress, so we | |
427 | * can proceed safely. Otherwise the copy_completed method | |
428 | * will see that the pending_copy is > 1, and do another copy. | |
429 | */ | |
430 | [self x_copy_request_targets]; | |
431 | } | |
432 | } | |
433 | } | |
434 | ||
435 | /* Set pbproxy as owner of the SELECTION_MANAGER selection. | |
436 | * This prevents tools like xclipboard from causing havoc. | |
437 | * Returns TRUE on success | |
438 | */ | |
439 | - (BOOL) set_clipboard_manager_status:(BOOL)value | |
440 | { | |
441 | TRACE(); | |
442 | ||
443 | Window owner = XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager); | |
444 | ||
445 | if (value) { | |
446 | if (owner == _selection_window) | |
447 | return TRUE; | |
448 | ||
449 | if (owner != None) { | |
450 | ErrorF( | |
451 | "A clipboard manager using window 0x%lx already owns the clipboard selection. " | |
452 | "pbproxy will not sync clipboard to pasteboard.\n", owner); | |
453 | return FALSE; | |
454 | } | |
455 | ||
456 | XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager, | |
457 | _selection_window, | |
458 | CurrentTime); | |
459 | return (_selection_window == | |
460 | XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager)); | |
461 | } | |
462 | else { | |
463 | if (owner != _selection_window) | |
464 | return TRUE; | |
465 | ||
466 | XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager, None, | |
467 | CurrentTime); | |
468 | return (None == | |
469 | XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager)); | |
470 | } | |
471 | ||
472 | return FALSE; | |
473 | } | |
474 | ||
475 | /* | |
476 | * This occurs when we previously owned a selection, | |
477 | * and then lost it from another client. | |
478 | */ | |
479 | - (void) clear_event:(XSelectionClearEvent *)e | |
480 | { | |
481 | ||
482 | TRACE(); | |
483 | ||
484 | DebugF("e->selection %s\n", XGetAtomName(xpbproxy_dpy, e->selection)); | |
485 | ||
486 | if (e->selection == atoms->clipboard) { | |
487 | /* | |
488 | * We lost ownership of the CLIPBOARD. | |
489 | */ | |
490 | ++pending_clipboard; | |
491 | ||
492 | if (1 == pending_clipboard) { | |
493 | /* Claim the clipboard contents from the new owner. */ | |
494 | [self claim_clipboard]; | |
495 | } | |
496 | } | |
497 | else if (e->selection == atoms->clipboard_manager) { | |
498 | if (pbproxy_prefs.clipboard_to_pasteboard) { | |
499 | /* Another CLIPBOARD_MANAGER has set itself as owner. Disable syncing | |
500 | * to avoid a race. | |
501 | */ | |
502 | ErrorF("Another clipboard manager was started! " | |
503 | "xpbproxy is disabling syncing with clipboard.\n"); | |
504 | pbproxy_prefs.clipboard_to_pasteboard = NO; | |
505 | } | |
506 | } | |
507 | } | |
508 | ||
509 | /* | |
510 | * We greedily acquire the clipboard after it changes, and on startup. | |
511 | */ | |
512 | - (void) claim_clipboard | |
513 | { | |
514 | Window owner; | |
515 | ||
516 | TRACE(); | |
517 | ||
518 | if (!pbproxy_prefs.clipboard_to_pasteboard) | |
519 | return; | |
520 | ||
521 | owner = XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard); | |
522 | if (None == owner) { | |
523 | /* | |
524 | * The owner probably died or we are just starting up pbproxy. | |
525 | * Set pbproxy's _selection_window as the owner, and continue. | |
526 | */ | |
527 | DebugF("No clipboard owner.\n"); | |
528 | [self copy_completed:atoms->clipboard]; | |
529 | return; | |
530 | } | |
531 | else if (owner == _selection_window) { | |
532 | [self copy_completed:atoms->clipboard]; | |
533 | return; | |
534 | } | |
535 | ||
536 | DebugF("requesting targets\n"); | |
537 | ||
538 | request_atom = atoms->targets; | |
539 | XConvertSelection(xpbproxy_dpy, atoms->clipboard, atoms->targets, | |
540 | atoms->clipboard, _selection_window, CurrentTime); | |
541 | XFlush(xpbproxy_dpy); | |
542 | /* Now we will get a SelectionNotify event in the future. */ | |
543 | } | |
544 | ||
545 | /* Greedily acquire the clipboard. */ | |
546 | - (void) own_clipboard | |
547 | { | |
548 | ||
549 | TRACE(); | |
550 | ||
551 | /* We should perhaps have a boundary limit on the number of iterations... */ | |
552 | do { | |
553 | XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard, _selection_window, | |
554 | CurrentTime); | |
555 | } while (_selection_window != XGetSelectionOwner(xpbproxy_dpy, | |
556 | atoms->clipboard)); | |
557 | } | |
558 | ||
559 | - (void) init_reply:(XEvent *)reply request:(XSelectionRequestEvent *)e | |
560 | { | |
561 | reply->xselection.type = SelectionNotify; | |
562 | reply->xselection.selection = e->selection; | |
563 | reply->xselection.target = e->target; | |
564 | reply->xselection.requestor = e->requestor; | |
565 | reply->xselection.time = e->time; | |
566 | reply->xselection.property = None; | |
567 | } | |
568 | ||
569 | - (void) send_reply:(XEvent *)reply | |
570 | { | |
571 | /* | |
572 | * We are supposed to use an empty event mask, and not propagate | |
573 | * the event, according to the ICCCM. | |
574 | */ | |
575 | DebugF("reply->xselection.requestor 0x%lx\n", reply->xselection.requestor); | |
576 | ||
577 | XSendEvent(xpbproxy_dpy, reply->xselection.requestor, False, 0, reply); | |
578 | XFlush(xpbproxy_dpy); | |
579 | } | |
580 | ||
581 | /* | |
582 | * This responds to a TARGETS request. | |
583 | * The result is a list of a ATOMs that correspond to the types available | |
584 | * for a selection. | |
585 | * For instance an application might provide a UTF8_STRING and a STRING | |
586 | * (in Latin-1 encoding). The requestor can then make the choice based on | |
587 | * the list. | |
588 | */ | |
589 | - (void) send_targets:(XSelectionRequestEvent *)e pasteboard:(NSPasteboard *) | |
590 | pb | |
591 | { | |
592 | XEvent reply; | |
593 | NSArray *pbtypes; | |
594 | ||
595 | [self init_reply:&reply request:e]; | |
596 | ||
597 | pbtypes = [pb types]; | |
598 | if (pbtypes) { | |
599 | long list[7]; /* Don't forget to increase this if we handle more types! */ | |
600 | long count = 0; | |
601 | ||
602 | /* | |
603 | * I'm not sure if this is needed, but some toolkits/clients list | |
604 | * TARGETS in response to targets. | |
605 | */ | |
606 | list[count] = atoms->targets; | |
607 | ++count; | |
608 | ||
609 | if ([pbtypes containsObject:NSStringPboardType]) { | |
610 | /* We have a string type that we can convert to UTF8, or Latin-1... */ | |
611 | DebugF("NSStringPboardType\n"); | |
612 | list[count] = atoms->utf8_string; | |
613 | ++count; | |
614 | list[count] = atoms->string; | |
615 | ++count; | |
616 | list[count] = atoms->compound_text; | |
617 | ++count; | |
618 | } | |
619 | ||
620 | /* TODO add the NSPICTPboardType back again, once we have conversion | |
621 | * functionality in send_image. | |
622 | */ | |
623 | #ifdef __clang__ | |
624 | #pragma clang diagnostic push | |
625 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" // NSPICTPboardType | |
626 | #endif | |
627 | ||
628 | if ([pbtypes containsObject:NSPICTPboardType] | |
629 | || [pbtypes containsObject:NSTIFFPboardType]) { | |
630 | /* We can convert a TIFF to a PNG or JPEG. */ | |
631 | DebugF("NSTIFFPboardType\n"); | |
632 | list[count] = atoms->image_png; | |
633 | ++count; | |
634 | list[count] = atoms->image_jpeg; | |
635 | ++count; | |
636 | } | |
637 | ||
638 | #ifdef __clang__ | |
639 | #pragma clang diagnostic pop | |
640 | #endif | |
641 | ||
642 | if (count) { | |
643 | /* We have a list of ATOMs to send. */ | |
644 | XChangeProperty(xpbproxy_dpy, e->requestor, e->property, | |
645 | atoms->atom, 32, | |
646 | PropModeReplace, (unsigned char *)list, | |
647 | count); | |
648 | ||
649 | reply.xselection.property = e->property; | |
650 | } | |
651 | } | |
652 | ||
653 | [self send_reply:&reply]; | |
654 | } | |
655 | ||
656 | - (void) send_string:(XSelectionRequestEvent *)e utf8:(BOOL)utf8 pasteboard:( | |
657 | NSPasteboard *)pb | |
658 | { | |
659 | XEvent reply; | |
660 | NSArray *pbtypes; | |
661 | NSString *data; | |
662 | const char *bytes; | |
663 | NSUInteger length; | |
664 | ||
665 | TRACE(); | |
666 | ||
667 | [self init_reply:&reply request:e]; | |
668 | ||
669 | pbtypes = [pb types]; | |
670 | ||
671 | if (![pbtypes containsObject:NSStringPboardType]) { | |
672 | [self send_reply:&reply]; | |
673 | return; | |
674 | } | |
675 | ||
676 | #ifdef __LP64__ | |
677 | DebugF("pbtypes retainCount after containsObject: %lu\n", | |
678 | [pbtypes retainCount]); | |
679 | #else | |
680 | DebugF("pbtypes retainCount after containsObject: %u\n", | |
681 | [pbtypes retainCount]); | |
682 | #endif | |
683 | ||
684 | data = [pb stringForType:NSStringPboardType]; | |
685 | ||
686 | if (nil == data) { | |
687 | [self send_reply:&reply]; | |
688 | return; | |
689 | } | |
690 | ||
691 | if (utf8) { | |
692 | bytes = [data UTF8String]; | |
693 | /* | |
694 | * We don't want the UTF-8 string length here. | |
695 | * We want the length in bytes. | |
696 | */ | |
697 | length = strlen(bytes); | |
698 | ||
699 | if (length < 50) { | |
700 | DebugF("UTF-8: %s\n", bytes); | |
701 | #ifdef __LP64__ | |
702 | DebugF("UTF-8 length: %lu\n", length); | |
703 | #else | |
704 | DebugF("UTF-8 length: %u\n", length); | |
705 | #endif | |
706 | } | |
707 | } | |
708 | else { | |
709 | DebugF("Latin-1\n"); | |
710 | bytes = [data cStringUsingEncoding:NSISOLatin1StringEncoding]; | |
711 | /*WARNING: bytes is not NUL-terminated. */ | |
712 | length = [data lengthOfBytesUsingEncoding:NSISOLatin1StringEncoding]; | |
713 | } | |
714 | ||
715 | DebugF("e->target %s\n", XGetAtomName(xpbproxy_dpy, e->target)); | |
716 | ||
717 | XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target, | |
718 | 8, PropModeReplace, (unsigned char *)bytes, length); | |
719 | ||
720 | reply.xselection.property = e->property; | |
721 | ||
722 | [self send_reply:&reply]; | |
723 | } | |
724 | ||
725 | - (void) send_compound_text:(XSelectionRequestEvent *)e pasteboard:( | |
726 | NSPasteboard *)pb | |
727 | { | |
728 | XEvent reply; | |
729 | NSArray *pbtypes; | |
730 | ||
731 | TRACE(); | |
732 | ||
733 | [self init_reply:&reply request:e]; | |
734 | ||
735 | pbtypes = [pb types]; | |
736 | ||
737 | if ([pbtypes containsObject: NSStringPboardType]) { | |
738 | NSString *data = [pb stringForType:NSStringPboardType]; | |
739 | if (nil != data) { | |
740 | /* | |
741 | * Cast to (void *) to avoid a const warning. | |
742 | * AFAIK Xutf8TextListToTextProperty does not modify the input memory. | |
743 | */ | |
744 | void *utf8 = (void *)[data UTF8String]; | |
745 | char *list[] = { utf8, NULL }; | |
746 | XTextProperty textprop; | |
747 | ||
748 | textprop.value = NULL; | |
749 | ||
750 | if (Success == Xutf8TextListToTextProperty(xpbproxy_dpy, list, 1, | |
751 | XCompoundTextStyle, | |
752 | &textprop)) { | |
753 | ||
754 | if (8 != textprop.format) | |
755 | DebugF( | |
756 | "textprop.format is unexpectedly not 8 - it's %d instead\n", | |
757 | textprop.format); | |
758 | ||
759 | XChangeProperty(xpbproxy_dpy, e->requestor, e->property, | |
760 | atoms->compound_text, textprop.format, | |
761 | PropModeReplace, textprop.value, | |
762 | textprop.nitems); | |
763 | ||
764 | reply.xselection.property = e->property; | |
765 | } | |
766 | ||
767 | if (textprop.value) | |
768 | XFree(textprop.value); | |
769 | ||
770 | } | |
771 | } | |
772 | ||
773 | [self send_reply:&reply]; | |
774 | } | |
775 | ||
776 | /* Finding a test application that uses MULTIPLE has proven to be difficult. */ | |
777 | - (void) send_multiple:(XSelectionRequestEvent *)e | |
778 | { | |
779 | XEvent reply; | |
780 | ||
781 | TRACE(); | |
782 | ||
783 | [self init_reply:&reply request:e]; | |
784 | ||
785 | if (None != e->property) {} | |
786 | ||
787 | [self send_reply:&reply]; | |
788 | } | |
789 | ||
790 | /* Return nil if an error occured. */ | |
791 | /* DO NOT retain the encdata for longer than the length of an event response. | |
792 | * The autorelease pool will reuse/free it. | |
793 | */ | |
794 | - (NSData *) encode_image_data:(NSData *)data type:(NSBitmapImageFileType) | |
795 | enctype | |
796 | { | |
797 | NSBitmapImageRep *bmimage = nil; | |
798 | NSData *encdata = nil; | |
799 | NSDictionary *dict = nil; | |
800 | ||
801 | bmimage = [[NSBitmapImageRep alloc] initWithData:data]; | |
802 | ||
803 | if (nil == bmimage) | |
804 | return nil; | |
805 | ||
806 | dict = [[NSDictionary alloc] init]; | |
807 | encdata = [bmimage representationUsingType:enctype properties:dict]; | |
808 | ||
809 | if (nil == encdata) { | |
810 | [dict autorelease]; | |
811 | [bmimage autorelease]; | |
812 | return nil; | |
813 | } | |
814 | ||
815 | [dict autorelease]; | |
816 | [bmimage autorelease]; | |
817 | ||
818 | return encdata; | |
819 | } | |
820 | ||
821 | /* Return YES when an error has occured when trying to send the PICT. */ | |
822 | /* The caller should send a default reponse with a property of None when an error occurs. */ | |
823 | - (BOOL) send_image_pict_reply:(XSelectionRequestEvent *)e | |
824 | pasteboard:(NSPasteboard *)pb | |
825 | type:(NSBitmapImageFileType)imagetype | |
826 | { | |
827 | XEvent reply; | |
828 | NSImage *img = nil; | |
829 | NSData *data = nil, *encdata = nil; | |
830 | NSUInteger length; | |
831 | const void *bytes = NULL; | |
832 | ||
833 | img = [[NSImage alloc] initWithPasteboard:pb]; | |
834 | ||
835 | if (nil == img) { | |
836 | return YES; | |
837 | } | |
838 | ||
839 | data = [img TIFFRepresentation]; | |
840 | ||
841 | if (nil == data) { | |
842 | [img autorelease]; | |
843 | ErrorF("unable to convert PICT to TIFF!\n"); | |
844 | return YES; | |
845 | } | |
846 | ||
847 | encdata = [self encode_image_data:data type:imagetype]; | |
848 | if (nil == encdata) { | |
849 | [img autorelease]; | |
850 | return YES; | |
851 | } | |
852 | ||
853 | [self init_reply:&reply request:e]; | |
854 | ||
855 | length = [encdata length]; | |
856 | bytes = [encdata bytes]; | |
857 | ||
858 | XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target, | |
859 | 8, PropModeReplace, bytes, length); | |
860 | reply.xselection.property = e->property; | |
861 | ||
862 | [self send_reply:&reply]; | |
863 | ||
864 | [img autorelease]; | |
865 | ||
866 | return NO; /*no error*/ | |
867 | } | |
868 | ||
869 | /* Return YES if an error occured. */ | |
870 | /* The caller should send a reply with a property of None when an error occurs. */ | |
871 | - (BOOL) send_image_tiff_reply:(XSelectionRequestEvent *)e | |
872 | pasteboard:(NSPasteboard *)pb | |
873 | type:(NSBitmapImageFileType)imagetype | |
874 | { | |
875 | XEvent reply; | |
876 | NSData *data = nil; | |
877 | NSData *encdata = nil; | |
878 | NSUInteger length; | |
879 | const void *bytes = NULL; | |
880 | ||
881 | data = [pb dataForType:NSTIFFPboardType]; | |
882 | ||
883 | if (nil == data) | |
884 | return YES; | |
885 | ||
886 | encdata = [self encode_image_data:data type:imagetype]; | |
887 | ||
888 | if (nil == encdata) | |
889 | return YES; | |
890 | ||
891 | [self init_reply:&reply request:e]; | |
892 | ||
893 | length = [encdata length]; | |
894 | bytes = [encdata bytes]; | |
895 | ||
896 | XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target, | |
897 | 8, PropModeReplace, bytes, length); | |
898 | reply.xselection.property = e->property; | |
899 | ||
900 | [self send_reply:&reply]; | |
901 | ||
902 | return NO; /*no error*/ | |
903 | } | |
904 | ||
905 | - (void) send_image:(XSelectionRequestEvent *)e pasteboard:(NSPasteboard *)pb | |
906 | { | |
907 | NSArray *pbtypes = nil; | |
908 | NSBitmapImageFileType imagetype = NSPNGFileType; | |
909 | ||
910 | TRACE(); | |
911 | ||
912 | if (e->target == atoms->image_png) | |
913 | imagetype = NSPNGFileType; | |
914 | else if (e->target == atoms->image_jpeg) | |
915 | imagetype = NSJPEGFileType; | |
916 | else { | |
917 | ErrorF( | |
918 | "internal failure in xpbproxy! imagetype being sent isn't PNG or JPEG.\n"); | |
919 | } | |
920 | ||
921 | pbtypes = [pb types]; | |
922 | ||
923 | if (pbtypes) { | |
924 | if ([pbtypes containsObject:NSTIFFPboardType]) { | |
925 | if (NO == | |
926 | [self send_image_tiff_reply:e pasteboard:pb type:imagetype]) | |
927 | return; | |
928 | } | |
929 | #ifdef __clang__ | |
930 | #pragma clang diagnostic push | |
931 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" // NSPICTPboardType | |
932 | #endif | |
933 | else if ([pbtypes containsObject:NSPICTPboardType]) | |
934 | #ifdef __clang__ | |
935 | #pragma clang diagnostic pop | |
936 | #endif | |
937 | { | |
938 | if (NO == | |
939 | [self send_image_pict_reply:e pasteboard:pb type:imagetype]) | |
940 | return; | |
941 | ||
942 | /* Fall through intentionally to the send_none: */ | |
943 | } | |
944 | } | |
945 | ||
946 | [self send_none:e]; | |
947 | } | |
948 | ||
949 | - (void)send_none:(XSelectionRequestEvent *)e | |
950 | { | |
951 | XEvent reply; | |
952 | ||
953 | TRACE(); | |
954 | ||
955 | [self init_reply:&reply request:e]; | |
956 | [self send_reply:&reply]; | |
957 | } | |
958 | ||
959 | /* Another client requested the data or targets of data available from the clipboard. */ | |
960 | - (void)request_event:(XSelectionRequestEvent *)e | |
961 | { | |
962 | NSPasteboard *pb; | |
963 | ||
964 | TRACE(); | |
965 | ||
966 | /* TODO We should also keep track of the time of the selection, and | |
967 | * according to the ICCCM "refuse the request" if the event timestamp | |
968 | * is before we owned it. | |
969 | * What should we base the time on? How can we get the current time just | |
970 | * before an XSetSelectionOwner? Is it the server's time, or the clients? | |
971 | * According to the XSelectionRequestEvent manual page, the Time value | |
972 | * may be set to CurrentTime or a time, so that makes it a bit different. | |
973 | * Perhaps we should just punt and ignore races. | |
974 | */ | |
975 | ||
976 | /*TODO we need a COMPOUND_TEXT test app*/ | |
977 | /*TODO we need a MULTIPLE test app*/ | |
978 | ||
979 | pb = [NSPasteboard generalPasteboard]; | |
980 | if (nil == pb) { | |
981 | [self send_none:e]; | |
982 | return; | |
983 | } | |
984 | ||
985 | if (None != e->target) | |
986 | DebugF("e->target %s\n", XGetAtomName(xpbproxy_dpy, e->target)); | |
987 | ||
988 | if (e->target == atoms->targets) { | |
989 | /* The paste requestor wants to know what TARGETS we support. */ | |
990 | [self send_targets:e pasteboard:pb]; | |
991 | } | |
992 | else if (e->target == atoms->multiple) { | |
993 | /* | |
994 | * This isn't finished, and may never be, unless I can find | |
995 | * a good test app. | |
996 | */ | |
997 | [self send_multiple:e]; | |
998 | } | |
999 | else if (e->target == atoms->utf8_string) { | |
1000 | [self send_string:e utf8:YES pasteboard:pb]; | |
1001 | } | |
1002 | else if (e->target == atoms->string) { | |
1003 | [self send_string:e utf8:NO pasteboard:pb]; | |
1004 | } | |
1005 | else if (e->target == atoms->compound_text) { | |
1006 | [self send_compound_text:e pasteboard:pb]; | |
1007 | } | |
1008 | else if (e->target == atoms->multiple) { | |
1009 | [self send_multiple:e]; | |
1010 | } | |
1011 | else if (e->target == atoms->image_png || e->target == | |
1012 | atoms->image_jpeg) { | |
1013 | [self send_image:e pasteboard:pb]; | |
1014 | } | |
1015 | else { | |
1016 | [self send_none:e]; | |
1017 | } | |
1018 | } | |
1019 | ||
1020 | /* This handles the events resulting from an XConvertSelection request. */ | |
1021 | - (void) notify_event:(XSelectionEvent *)e | |
1022 | { | |
1023 | Atom type; | |
1024 | struct propdata pdata; | |
1025 | ||
1026 | TRACE(); | |
1027 | ||
1028 | [self release_pending]; | |
1029 | ||
1030 | if (None == e->property) { | |
1031 | DebugF("e->property is None.\n"); | |
1032 | [self copy_completed:e->selection]; | |
1033 | /* Nothing is selected. */ | |
1034 | return; | |
1035 | } | |
1036 | ||
1037 | #if 0 | |
1038 | ErrorF("e->selection %s\n", XGetAtomName(xpbproxy_dpy, e->selection)); | |
1039 | ErrorF("e->property %s\n", XGetAtomName(xpbproxy_dpy, e->property)); | |
1040 | #endif | |
1041 | ||
1042 | if ([self is_incr_type:e]) { | |
1043 | /* | |
1044 | * This is an INCR-style transfer, which means that we | |
1045 | * will get the data after a series of PropertyNotify events. | |
1046 | */ | |
1047 | DebugF("is INCR\n"); | |
1048 | ||
1049 | if (get_property(e->requestor, e->property, &pdata, /*Delete*/ True, | |
1050 | &type)) { | |
1051 | /* | |
1052 | * An error occured, so we should invoke the copy_completed:, but | |
1053 | * not handle_selection:type:propdata: | |
1054 | */ | |
1055 | [self copy_completed:e->selection]; | |
1056 | return; | |
1057 | } | |
1058 | ||
1059 | free_propdata(&pdata); | |
1060 | ||
1061 | pending.requestor = e->requestor; | |
1062 | pending.selection = e->selection; | |
1063 | ||
1064 | DebugF("set pending.requestor to 0x%lx\n", pending.requestor); | |
1065 | } | |
1066 | else { | |
1067 | if (get_property(e->requestor, e->property, &pdata, /*Delete*/ True, | |
1068 | &type)) { | |
1069 | [self copy_completed:e->selection]; | |
1070 | return; | |
1071 | } | |
1072 | ||
1073 | /* We have the complete selection data.*/ | |
1074 | [self handle_selection:e->selection type:type propdata:&pdata]; | |
1075 | ||
1076 | DebugF("handled selection with the first notify_event\n"); | |
1077 | } | |
1078 | } | |
1079 | ||
1080 | /* This is used for INCR transfers. See the ICCCM for the details. */ | |
1081 | /* This is used to retrieve PRIMARY and CLIPBOARD selections. */ | |
1082 | - (void) property_event:(XPropertyEvent *)e | |
1083 | { | |
1084 | struct propdata pdata; | |
1085 | Atom type; | |
1086 | ||
1087 | TRACE(); | |
1088 | ||
1089 | if (None != e->atom) { | |
1090 | #ifdef DEBUG | |
1091 | char *name = XGetAtomName(xpbproxy_dpy, e->atom); | |
1092 | ||
1093 | if (name) { | |
1094 | DebugF("e->atom %s\n", name); | |
1095 | XFree(name); | |
1096 | } | |
1097 | #endif | |
1098 | } | |
1099 | ||
1100 | if (None != pending.requestor && PropertyNewValue == e->state) { | |
1101 | DebugF("pending.requestor 0x%lx\n", pending.requestor); | |
1102 | ||
1103 | if (get_property(e->window, e->atom, &pdata, /*Delete*/ True, | |
1104 | &type)) { | |
1105 | [self copy_completed:pending.selection]; | |
1106 | [self release_pending]; | |
1107 | return; | |
1108 | } | |
1109 | ||
1110 | if (0 == pdata.length) { | |
1111 | /* | |
1112 | * We completed the transfer. | |
1113 | * handle_selection will call copy_completed: for us. | |
1114 | */ | |
1115 | [self handle_selection:pending.selection type:type propdata:& | |
1116 | pending.propdata]; | |
1117 | free_propdata(&pdata); | |
1118 | pending.propdata = null_propdata; | |
1119 | pending.requestor = None; | |
1120 | pending.selection = None; | |
1121 | } | |
1122 | else { | |
1123 | [self append_to_pending:&pdata requestor:e->window]; | |
1124 | free_propdata(&pdata); | |
1125 | } | |
1126 | } | |
1127 | } | |
1128 | ||
1129 | - (void) xfixes_selection_notify:(XFixesSelectionNotifyEvent *)e | |
1130 | { | |
1131 | if (!pbproxy_prefs.active) | |
1132 | return; | |
1133 | ||
1134 | switch (e->subtype) { | |
1135 | case XFixesSetSelectionOwnerNotify: | |
1136 | if (e->selection == atoms->primary && pbproxy_prefs.primary_on_grab) | |
1137 | [self x_copy:e->timestamp]; | |
1138 | break; | |
1139 | ||
1140 | case XFixesSelectionWindowDestroyNotify: | |
1141 | case XFixesSelectionClientCloseNotify: | |
1142 | default: | |
1143 | ErrorF("Unhandled XFixesSelectionNotifyEvent: subtype=%d\n", | |
1144 | e->subtype); | |
1145 | break; | |
1146 | } | |
1147 | } | |
1148 | ||
1149 | - (void) handle_targets: (Atom)selection propdata:(struct propdata *)pdata | |
1150 | { | |
1151 | /* Find a type we can handle and prefer from the list of ATOMs. */ | |
1152 | Atom preferred; | |
1153 | char *name; | |
1154 | ||
1155 | TRACE(); | |
1156 | ||
1157 | preferred = [self find_preferred:pdata]; | |
1158 | ||
1159 | if (None == preferred) { | |
1160 | /* | |
1161 | * This isn't required by the ICCCM, but some apps apparently | |
1162 | * don't respond to TARGETS properly. | |
1163 | */ | |
1164 | preferred = atoms->string; | |
1165 | } | |
1166 | ||
1167 | (void)name; /* Avoid a warning with non-debug compiles. */ | |
1168 | #ifdef DEBUG | |
1169 | name = XGetAtomName(xpbproxy_dpy, preferred); | |
1170 | ||
1171 | if (name) { | |
1172 | DebugF("requesting %s\n", name); | |
1173 | } | |
1174 | #endif | |
1175 | request_atom = preferred; | |
1176 | XConvertSelection(xpbproxy_dpy, selection, preferred, selection, | |
1177 | _selection_window, CurrentTime); | |
1178 | } | |
1179 | ||
1180 | /* This handles the image type of selection (typically in CLIPBOARD). */ | |
1181 | /* We convert to a TIFF, so that other applications can paste more easily. */ | |
1182 | - (void) handle_image: (struct propdata *)pdata pasteboard:(NSPasteboard *)pb | |
1183 | { | |
1184 | NSArray *pbtypes; | |
1185 | NSUInteger length; | |
1186 | NSData *data, *tiff; | |
1187 | NSBitmapImageRep *bmimage; | |
1188 | ||
1189 | TRACE(); | |
1190 | ||
1191 | length = pdata->length; | |
1192 | data = [[NSData alloc] initWithBytes:pdata->data length:length]; | |
1193 | ||
1194 | if (nil == data) { | |
1195 | DebugF("unable to create NSData object!\n"); | |
1196 | return; | |
1197 | } | |
1198 | ||
1199 | #ifdef __LP64__ | |
1200 | DebugF("data retainCount before NSBitmapImageRep initWithData: %lu\n", | |
1201 | [data retainCount]); | |
1202 | #else | |
1203 | DebugF("data retainCount before NSBitmapImageRep initWithData: %u\n", | |
1204 | [data retainCount]); | |
1205 | #endif | |
1206 | ||
1207 | bmimage = [[NSBitmapImageRep alloc] initWithData:data]; | |
1208 | ||
1209 | if (nil == bmimage) { | |
1210 | [data autorelease]; | |
1211 | DebugF("unable to create NSBitmapImageRep!\n"); | |
1212 | return; | |
1213 | } | |
1214 | ||
1215 | #ifdef __LP64__ | |
1216 | DebugF("data retainCount after NSBitmapImageRep initWithData: %lu\n", | |
1217 | [data retainCount]); | |
1218 | #else | |
1219 | DebugF("data retainCount after NSBitmapImageRep initWithData: %u\n", | |
1220 | [data retainCount]); | |
1221 | #endif | |
1222 | ||
1223 | @try | |
1224 | { | |
1225 | tiff = [bmimage TIFFRepresentation]; | |
1226 | } | |
1227 | ||
1228 | @catch (NSException *e) | |
1229 | { | |
1230 | DebugF("NSTIFFException!\n"); | |
1231 | [data autorelease]; | |
1232 | [bmimage autorelease]; | |
1233 | return; | |
1234 | } | |
1235 | ||
1236 | #ifdef __LP64__ | |
1237 | DebugF("bmimage retainCount after TIFFRepresentation %lu\n", | |
1238 | [bmimage retainCount]); | |
1239 | #else | |
1240 | DebugF("bmimage retainCount after TIFFRepresentation %u\n", | |
1241 | [bmimage retainCount]); | |
1242 | #endif | |
1243 | ||
1244 | pbtypes = [NSArray arrayWithObjects:NSTIFFPboardType, nil]; | |
1245 | ||
1246 | if (nil == pbtypes) { | |
1247 | [data autorelease]; | |
1248 | [bmimage autorelease]; | |
1249 | return; | |
1250 | } | |
1251 | ||
1252 | [pb declareTypes:pbtypes owner:nil]; | |
1253 | if (YES != [pb setData:tiff forType:NSTIFFPboardType]) { | |
1254 | DebugF("writing pasteboard data failed!\n"); | |
1255 | } | |
1256 | ||
1257 | [data autorelease]; | |
1258 | ||
1259 | #ifdef __LP64__ | |
1260 | DebugF("bmimage retainCount before release %lu\n", [bmimage retainCount]); | |
1261 | #else | |
1262 | DebugF("bmimage retainCount before release %u\n", [bmimage retainCount]); | |
1263 | #endif | |
1264 | ||
1265 | [bmimage autorelease]; | |
1266 | } | |
1267 | ||
1268 | /* This handles the UTF8_STRING type of selection. */ | |
1269 | - (void) handle_utf8_string:(struct propdata *)pdata pasteboard:(NSPasteboard | |
1270 | *)pb | |
1271 | { | |
1272 | NSString *string; | |
1273 | NSArray *pbtypes; | |
1274 | ||
1275 | TRACE(); | |
1276 | ||
1277 | string = | |
1278 | [[NSString alloc] initWithBytes:pdata->data length:pdata->length | |
1279 | encoding: | |
1280 | NSUTF8StringEncoding]; | |
1281 | ||
1282 | if (nil == string) | |
1283 | return; | |
1284 | ||
1285 | pbtypes = [NSArray arrayWithObjects:NSStringPboardType, nil]; | |
1286 | ||
1287 | if (nil == pbtypes) { | |
1288 | [string autorelease]; | |
1289 | return; | |
1290 | } | |
1291 | ||
1292 | [pb declareTypes:pbtypes owner:nil]; | |
1293 | ||
1294 | if (YES != [pb setString:string forType:NSStringPboardType]) { | |
1295 | ErrorF("pasteboard setString:forType: failed!\n"); | |
1296 | } | |
1297 | [string autorelease]; | |
1298 | DebugF("done handling utf8 string\n"); | |
1299 | } | |
1300 | ||
1301 | /* This handles the STRING type, which should be in Latin-1. */ | |
1302 | - (void) handle_string: (struct propdata *)pdata pasteboard:(NSPasteboard *) | |
1303 | pb | |
1304 | { | |
1305 | NSString *string; | |
1306 | NSArray *pbtypes; | |
1307 | ||
1308 | TRACE(); | |
1309 | ||
1310 | string = | |
1311 | [[NSString alloc] initWithBytes:pdata->data length:pdata->length | |
1312 | encoding: | |
1313 | NSISOLatin1StringEncoding]; | |
1314 | ||
1315 | if (nil == string) | |
1316 | return; | |
1317 | ||
1318 | pbtypes = [NSArray arrayWithObjects:NSStringPboardType, nil]; | |
1319 | ||
1320 | if (nil == pbtypes) { | |
1321 | [string autorelease]; | |
1322 | return; | |
1323 | } | |
1324 | ||
1325 | [pb declareTypes:pbtypes owner:nil]; | |
1326 | if (YES != [pb setString:string forType:NSStringPboardType]) { | |
1327 | ErrorF("pasteboard setString:forType failed in handle_string!\n"); | |
1328 | } | |
1329 | [string autorelease]; | |
1330 | } | |
1331 | ||
1332 | /* This is called when the selection is completely retrieved from another client. */ | |
1333 | /* Warning: this frees the propdata. */ | |
1334 | - (void) handle_selection:(Atom)selection type:(Atom)type propdata:(struct | |
1335 | propdata | |
1336 | *)pdata | |
1337 | { | |
1338 | NSPasteboard *pb; | |
1339 | ||
1340 | TRACE(); | |
1341 | ||
1342 | pb = [NSPasteboard generalPasteboard]; | |
1343 | ||
1344 | if (nil == pb) { | |
1345 | [self copy_completed:selection]; | |
1346 | free_propdata(pdata); | |
1347 | return; | |
1348 | } | |
1349 | ||
1350 | /* | |
1351 | * Some apps it seems set the type to TARGETS instead of ATOM, such as Eterm. | |
1352 | * These aren't ICCCM compliant apps, but we need these to work... | |
1353 | */ | |
1354 | if (request_atom == atoms->targets | |
1355 | && (type == atoms->atom || type == atoms->targets)) { | |
1356 | [self handle_targets:selection propdata:pdata]; | |
1357 | free_propdata(pdata); | |
1358 | return; | |
1359 | } | |
1360 | else if (type == atoms->image_png) { | |
1361 | [self handle_image:pdata pasteboard:pb]; | |
1362 | } | |
1363 | else if (type == atoms->image_jpeg) { | |
1364 | [self handle_image:pdata pasteboard:pb]; | |
1365 | } | |
1366 | else if (type == atoms->utf8_string) { | |
1367 | [self handle_utf8_string:pdata pasteboard:pb]; | |
1368 | } | |
1369 | else if (type == atoms->string) { | |
1370 | [self handle_string:pdata pasteboard:pb]; | |
1371 | } | |
1372 | ||
1373 | free_propdata(pdata); | |
1374 | ||
1375 | [self copy_completed:selection]; | |
1376 | } | |
1377 | ||
1378 | - (void) copy_completed:(Atom)selection | |
1379 | { | |
1380 | TRACE(); | |
1381 | char *name; | |
1382 | ||
1383 | (void)name; /* Avoid warning with non-debug compiles. */ | |
1384 | #ifdef DEBUG | |
1385 | name = XGetAtomName(xpbproxy_dpy, selection); | |
1386 | if (name) { | |
1387 | DebugF("copy_completed: %s\n", name); | |
1388 | XFree(name); | |
1389 | } | |
1390 | #endif | |
1391 | ||
1392 | if (selection == atoms->primary && pending_copy > 0) { | |
1393 | --pending_copy; | |
1394 | if (pending_copy > 0) { | |
1395 | /* Copy PRIMARY again. */ | |
1396 | [self x_copy_request_targets]; | |
1397 | return; | |
1398 | } | |
1399 | } | |
1400 | else if (selection == atoms->clipboard && pending_clipboard > 0) { | |
1401 | --pending_clipboard; | |
1402 | if (pending_clipboard > 0) { | |
1403 | /* Copy CLIPBOARD. */ | |
1404 | [self claim_clipboard]; | |
1405 | return; | |
1406 | } | |
1407 | else { | |
1408 | /* We got the final data. Now set pbproxy as the owner. */ | |
1409 | [self own_clipboard]; | |
1410 | return; | |
1411 | } | |
1412 | } | |
1413 | ||
1414 | /* | |
1415 | * We had 1 or more primary in progress, and the clipboard arrived | |
1416 | * while we were busy. | |
1417 | */ | |
1418 | if (pending_clipboard > 0) { | |
1419 | [self claim_clipboard]; | |
1420 | } | |
1421 | } | |
1422 | ||
1423 | - (void) reload_preferences | |
1424 | { | |
1425 | /* | |
1426 | * It's uncertain how we could handle the synchronization failing, so cast to void. | |
1427 | * The prefs_get_bool should fall back to defaults if the org.x.X11 plist doesn't exist or is invalid. | |
1428 | */ | |
1429 | (void)CFPreferencesAppSynchronize(app_prefs_domain_cfstr); | |
1430 | #ifdef STANDALONE_XPBPROXY | |
1431 | if (xpbproxy_is_standalone) | |
1432 | pbproxy_prefs.active = YES; | |
1433 | else | |
1434 | #endif | |
1435 | pbproxy_prefs.active = prefs_get_bool(CFSTR( | |
1436 | "sync_pasteboard"), | |
1437 | pbproxy_prefs.active); | |
1438 | pbproxy_prefs.primary_on_grab = | |
1439 | prefs_get_bool(CFSTR( | |
1440 | "sync_primary_on_select"), | |
1441 | pbproxy_prefs.primary_on_grab); | |
1442 | pbproxy_prefs.clipboard_to_pasteboard = | |
1443 | prefs_get_bool(CFSTR( | |
1444 | "sync_clipboard_to_pasteboard"), | |
1445 | pbproxy_prefs.clipboard_to_pasteboard); | |
1446 | pbproxy_prefs.pasteboard_to_primary = | |
1447 | prefs_get_bool(CFSTR( | |
1448 | "sync_pasteboard_to_primary"), | |
1449 | pbproxy_prefs.pasteboard_to_primary); | |
1450 | pbproxy_prefs.pasteboard_to_clipboard = | |
1451 | prefs_get_bool(CFSTR( | |
1452 | "sync_pasteboard_to_clipboard"), | |
1453 | pbproxy_prefs.pasteboard_to_clipboard); | |
1454 | ||
1455 | /* This is used for debugging. */ | |
1456 | //dump_prefs(); | |
1457 | ||
1458 | if (pbproxy_prefs.active && pbproxy_prefs.primary_on_grab && | |
1459 | !xpbproxy_have_xfixes) { | |
1460 | ErrorF( | |
1461 | "Disabling sync_primary_on_select functionality due to missing XFixes extension.\n"); | |
1462 | pbproxy_prefs.primary_on_grab = NO; | |
1463 | } | |
1464 | ||
1465 | /* Claim or release the CLIPBOARD_MANAGER atom */ | |
1466 | if (![self set_clipboard_manager_status:(pbproxy_prefs.active && | |
1467 | pbproxy_prefs. | |
1468 | clipboard_to_pasteboard)]) | |
1469 | pbproxy_prefs.clipboard_to_pasteboard = NO; | |
1470 | ||
1471 | if (pbproxy_prefs.active && pbproxy_prefs.clipboard_to_pasteboard) | |
1472 | [self claim_clipboard]; | |
1473 | } | |
1474 | ||
1475 | - (BOOL) is_active | |
1476 | { | |
1477 | return pbproxy_prefs.active; | |
1478 | } | |
1479 | ||
1480 | /* NSPasteboard-required methods */ | |
1481 | ||
1482 | - (void) paste:(id)sender | |
1483 | { | |
1484 | TRACE(); | |
1485 | } | |
1486 | ||
1487 | - (void) pasteboard:(NSPasteboard *)pb provideDataForType:(NSString *)type | |
1488 | { | |
1489 | TRACE(); | |
1490 | } | |
1491 | ||
1492 | - (void) pasteboardChangedOwner:(NSPasteboard *)pb | |
1493 | { | |
1494 | TRACE(); | |
1495 | ||
1496 | /* Right now we don't care with this. */ | |
1497 | } | |
1498 | ||
1499 | /* Allocation */ | |
1500 | ||
1501 | - (id) init | |
1502 | { | |
1503 | unsigned long pixel; | |
1504 | ||
1505 | self = [super init]; | |
1506 | if (self == nil) | |
1507 | return nil; | |
1508 | ||
1509 | atoms->primary = XInternAtom(xpbproxy_dpy, "PRIMARY", False); | |
1510 | atoms->clipboard = XInternAtom(xpbproxy_dpy, "CLIPBOARD", False); | |
1511 | atoms->text = XInternAtom(xpbproxy_dpy, "TEXT", False); | |
1512 | atoms->utf8_string = XInternAtom(xpbproxy_dpy, "UTF8_STRING", False); | |
1513 | atoms->string = XInternAtom(xpbproxy_dpy, "STRING", False); | |
1514 | atoms->targets = XInternAtom(xpbproxy_dpy, "TARGETS", False); | |
1515 | atoms->multiple = XInternAtom(xpbproxy_dpy, "MULTIPLE", False); | |
1516 | atoms->cstring = XInternAtom(xpbproxy_dpy, "CSTRING", False); | |
1517 | atoms->image_png = XInternAtom(xpbproxy_dpy, "image/png", False); | |
1518 | atoms->image_jpeg = XInternAtom(xpbproxy_dpy, "image/jpeg", False); | |
1519 | atoms->incr = XInternAtom(xpbproxy_dpy, "INCR", False); | |
1520 | atoms->atom = XInternAtom(xpbproxy_dpy, "ATOM", False); | |
1521 | atoms->clipboard_manager = XInternAtom(xpbproxy_dpy, "CLIPBOARD_MANAGER", | |
1522 | False); | |
1523 | atoms->compound_text = XInternAtom(xpbproxy_dpy, "COMPOUND_TEXT", False); | |
1524 | atoms->atom_pair = XInternAtom(xpbproxy_dpy, "ATOM_PAIR", False); | |
1525 | ||
1526 | pixel = BlackPixel(xpbproxy_dpy, DefaultScreen(xpbproxy_dpy)); | |
1527 | _selection_window = | |
1528 | XCreateSimpleWindow(xpbproxy_dpy, DefaultRootWindow(xpbproxy_dpy), | |
1529 | 0, 0, 1, 1, 0, pixel, pixel); | |
1530 | ||
1531 | /* This is used to get PropertyNotify events when doing INCR transfers. */ | |
1532 | XSelectInput(xpbproxy_dpy, _selection_window, PropertyChangeMask); | |
1533 | ||
1534 | request_atom = None; | |
1535 | ||
1536 | init_propdata(&pending.propdata); | |
1537 | pending.requestor = None; | |
1538 | pending.selection = None; | |
1539 | ||
1540 | pending_copy = 0; | |
1541 | pending_clipboard = 0; | |
1542 | ||
1543 | if (xpbproxy_have_xfixes) | |
1544 | XFixesSelectSelectionInput(xpbproxy_dpy, _selection_window, | |
1545 | atoms->primary, | |
1546 | XFixesSetSelectionOwnerNotifyMask); | |
1547 | ||
1548 | [self reload_preferences]; | |
1549 | ||
1550 | return self; | |
1551 | } | |
1552 | ||
1553 | - (void) dealloc | |
1554 | { | |
1555 | if (None != _selection_window) { | |
1556 | XDestroyWindow(xpbproxy_dpy, _selection_window); | |
1557 | _selection_window = None; | |
1558 | } | |
1559 | ||
1560 | free_propdata(&pending.propdata); | |
1561 | ||
1562 | [super dealloc]; | |
1563 | } | |
1564 | ||
1565 | @end |