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 67static 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 80static struct propdata null_propdata = { 81 NULL, 0, 0 82}; 83 84#ifdef DEBUG 85static void 86dump_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 102extern CFStringRef app_prefs_domain_cfstr; 103 104static BOOL 105prefs_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 114static void 115init_propdata(struct propdata *pdata) 116{ 117 *pdata = null_propdata; 118} 119 120static void 121free_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 */ 132static Bool 133get_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 DebugF("pbtypes retainCount after containsObject: %lu\n", 677 [pbtypes retainCount]); 678 679 data = [pb stringForType:NSStringPboardType]; 680 681 if (nil == data) { 682 [self send_reply:&reply]; 683 return; 684 } 685 686 if (utf8) { 687 bytes = [data UTF8String]; 688 /* 689 * We don't want the UTF-8 string length here. 690 * We want the length in bytes. 691 */ 692 length = strlen(bytes); 693 694 if (length < 50) { 695 DebugF("UTF-8: %s\n", bytes); 696 DebugF("UTF-8 length: %lu\n", length); 697 } 698 } 699 else { 700 DebugF("Latin-1\n"); 701 bytes = [data cStringUsingEncoding:NSISOLatin1StringEncoding]; 702 /*WARNING: bytes is not NUL-terminated. */ 703 length = [data lengthOfBytesUsingEncoding:NSISOLatin1StringEncoding]; 704 } 705 706 DebugF("e->target %s\n", XGetAtomName(xpbproxy_dpy, e->target)); 707 708 XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target, 709 8, PropModeReplace, (unsigned char *)bytes, length); 710 711 reply.xselection.property = e->property; 712 713 [self send_reply:&reply]; 714} 715 716- (void) send_compound_text:(XSelectionRequestEvent *)e pasteboard:( 717 NSPasteboard *)pb 718{ 719 XEvent reply; 720 NSArray *pbtypes; 721 722 TRACE(); 723 724 [self init_reply:&reply request:e]; 725 726 pbtypes = [pb types]; 727 728 if ([pbtypes containsObject: NSStringPboardType]) { 729 NSString *data = [pb stringForType:NSStringPboardType]; 730 if (nil != data) { 731 /* 732 * Cast to (void *) to avoid a const warning. 733 * AFAIK Xutf8TextListToTextProperty does not modify the input memory. 734 */ 735 void *utf8 = (void *)[data UTF8String]; 736 char *list[] = { utf8, NULL }; 737 XTextProperty textprop; 738 739 textprop.value = NULL; 740 741 if (Success == Xutf8TextListToTextProperty(xpbproxy_dpy, list, 1, 742 XCompoundTextStyle, 743 &textprop)) { 744 745 if (8 != textprop.format) 746 DebugF( 747 "textprop.format is unexpectedly not 8 - it's %d instead\n", 748 textprop.format); 749 750 XChangeProperty(xpbproxy_dpy, e->requestor, e->property, 751 atoms->compound_text, textprop.format, 752 PropModeReplace, textprop.value, 753 textprop.nitems); 754 755 reply.xselection.property = e->property; 756 } 757 758 if (textprop.value) 759 XFree(textprop.value); 760 761 } 762 } 763 764 [self send_reply:&reply]; 765} 766 767/* Finding a test application that uses MULTIPLE has proven to be difficult. */ 768- (void) send_multiple:(XSelectionRequestEvent *)e 769{ 770 XEvent reply; 771 772 TRACE(); 773 774 [self init_reply:&reply request:e]; 775 776 if (None != e->property) {} 777 778 [self send_reply:&reply]; 779} 780 781/* Return nil if an error occurred. */ 782/* DO NOT retain the encdata for longer than the length of an event response. 783 * The autorelease pool will reuse/free it. 784 */ 785- (NSData *) encode_image_data:(NSData *)data type:(NSBitmapImageFileType) 786 enctype 787{ 788 NSBitmapImageRep *bmimage = nil; 789 NSData *encdata = nil; 790 NSDictionary *dict = nil; 791 792 bmimage = [[NSBitmapImageRep alloc] initWithData:data]; 793 794 if (nil == bmimage) 795 return nil; 796 797 dict = [[NSDictionary alloc] init]; 798 encdata = [bmimage representationUsingType:enctype properties:dict]; 799 800 if (nil == encdata) { 801 [dict autorelease]; 802 [bmimage autorelease]; 803 return nil; 804 } 805 806 [dict autorelease]; 807 [bmimage autorelease]; 808 809 return encdata; 810} 811 812/* Return YES when an error has occurred when trying to send the PICT. */ 813/* The caller should send a default response with a property of None when an error occurs. */ 814- (BOOL) send_image_pict_reply:(XSelectionRequestEvent *)e 815 pasteboard:(NSPasteboard *)pb 816 type:(NSBitmapImageFileType)imagetype 817{ 818 XEvent reply; 819 NSImage *img = nil; 820 NSData *data = nil, *encdata = nil; 821 NSUInteger length; 822 const void *bytes = NULL; 823 824 img = [[NSImage alloc] initWithPasteboard:pb]; 825 826 if (nil == img) { 827 return YES; 828 } 829 830 data = [img TIFFRepresentation]; 831 832 if (nil == data) { 833 [img autorelease]; 834 ErrorF("unable to convert PICT to TIFF!\n"); 835 return YES; 836 } 837 838 encdata = [self encode_image_data:data type:imagetype]; 839 if (nil == encdata) { 840 [img autorelease]; 841 return YES; 842 } 843 844 [self init_reply:&reply request:e]; 845 846 length = [encdata length]; 847 bytes = [encdata bytes]; 848 849 XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target, 850 8, PropModeReplace, bytes, length); 851 reply.xselection.property = e->property; 852 853 [self send_reply:&reply]; 854 855 [img autorelease]; 856 857 return NO; /*no error*/ 858} 859 860/* Return YES if an error occurred. */ 861/* The caller should send a reply with a property of None when an error occurs. */ 862- (BOOL) send_image_tiff_reply:(XSelectionRequestEvent *)e 863 pasteboard:(NSPasteboard *)pb 864 type:(NSBitmapImageFileType)imagetype 865{ 866 XEvent reply; 867 NSData *data = nil; 868 NSData *encdata = nil; 869 NSUInteger length; 870 const void *bytes = NULL; 871 872 data = [pb dataForType:NSTIFFPboardType]; 873 874 if (nil == data) 875 return YES; 876 877 encdata = [self encode_image_data:data type:imagetype]; 878 879 if (nil == encdata) 880 return YES; 881 882 [self init_reply:&reply request:e]; 883 884 length = [encdata length]; 885 bytes = [encdata bytes]; 886 887 XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target, 888 8, PropModeReplace, bytes, length); 889 reply.xselection.property = e->property; 890 891 [self send_reply:&reply]; 892 893 return NO; /*no error*/ 894} 895 896- (void) send_image:(XSelectionRequestEvent *)e pasteboard:(NSPasteboard *)pb 897{ 898 NSArray *pbtypes = nil; 899 NSBitmapImageFileType imagetype = NSPNGFileType; 900 901 TRACE(); 902 903 if (e->target == atoms->image_png) 904 imagetype = NSPNGFileType; 905 else if (e->target == atoms->image_jpeg) 906 imagetype = NSJPEGFileType; 907 else { 908 ErrorF( 909 "internal failure in xpbproxy! imagetype being sent isn't PNG or JPEG.\n"); 910 } 911 912 pbtypes = [pb types]; 913 914 if (pbtypes) { 915 if ([pbtypes containsObject:NSTIFFPboardType]) { 916 if (NO == 917 [self send_image_tiff_reply:e pasteboard:pb type:imagetype]) 918 return; 919 } 920#ifdef __clang__ 921#pragma clang diagnostic push 922#pragma clang diagnostic ignored "-Wdeprecated-declarations" // NSPICTPboardType 923#endif 924 else if ([pbtypes containsObject:NSPICTPboardType]) 925#ifdef __clang__ 926#pragma clang diagnostic pop 927#endif 928 { 929 if (NO == 930 [self send_image_pict_reply:e pasteboard:pb type:imagetype]) 931 return; 932 933 /* Fall through intentionally to the send_none: */ 934 } 935 } 936 937 [self send_none:e]; 938} 939 940- (void)send_none:(XSelectionRequestEvent *)e 941{ 942 XEvent reply; 943 944 TRACE(); 945 946 [self init_reply:&reply request:e]; 947 [self send_reply:&reply]; 948} 949 950/* Another client requested the data or targets of data available from the clipboard. */ 951- (void)request_event:(XSelectionRequestEvent *)e 952{ 953 NSPasteboard *pb; 954 955 TRACE(); 956 957 /* TODO We should also keep track of the time of the selection, and 958 * according to the ICCCM "refuse the request" if the event timestamp 959 * is before we owned it. 960 * What should we base the time on? How can we get the current time just 961 * before an XSetSelectionOwner? Is it the server's time, or the clients? 962 * According to the XSelectionRequestEvent manual page, the Time value 963 * may be set to CurrentTime or a time, so that makes it a bit different. 964 * Perhaps we should just punt and ignore races. 965 */ 966 967 /*TODO we need a COMPOUND_TEXT test app*/ 968 /*TODO we need a MULTIPLE test app*/ 969 970 pb = [NSPasteboard generalPasteboard]; 971 if (nil == pb) { 972 [self send_none:e]; 973 return; 974 } 975 976 if (None != e->target) 977 DebugF("e->target %s\n", XGetAtomName(xpbproxy_dpy, e->target)); 978 979 if (e->target == atoms->targets) { 980 /* The paste requestor wants to know what TARGETS we support. */ 981 [self send_targets:e pasteboard:pb]; 982 } 983 else if (e->target == atoms->multiple) { 984 /* 985 * This isn't finished, and may never be, unless I can find 986 * a good test app. 987 */ 988 [self send_multiple:e]; 989 } 990 else if (e->target == atoms->utf8_string) { 991 [self send_string:e utf8:YES pasteboard:pb]; 992 } 993 else if (e->target == atoms->string) { 994 [self send_string:e utf8:NO pasteboard:pb]; 995 } 996 else if (e->target == atoms->compound_text) { 997 [self send_compound_text:e pasteboard:pb]; 998 } 999 else if (e->target == atoms->multiple) { 1000 [self send_multiple:e]; 1001 } 1002 else if (e->target == atoms->image_png || e->target == 1003 atoms->image_jpeg) { 1004 [self send_image:e pasteboard:pb]; 1005 } 1006 else { 1007 [self send_none:e]; 1008 } 1009} 1010 1011/* This handles the events resulting from an XConvertSelection request. */ 1012- (void) notify_event:(XSelectionEvent *)e 1013{ 1014 Atom type; 1015 struct propdata pdata; 1016 1017 TRACE(); 1018 1019 [self release_pending]; 1020 1021 if (None == e->property) { 1022 DebugF("e->property is None.\n"); 1023 [self copy_completed:e->selection]; 1024 /* Nothing is selected. */ 1025 return; 1026 } 1027 1028#if 0 1029 ErrorF("e->selection %s\n", XGetAtomName(xpbproxy_dpy, e->selection)); 1030 ErrorF("e->property %s\n", XGetAtomName(xpbproxy_dpy, e->property)); 1031#endif 1032 1033 if ([self is_incr_type:e]) { 1034 /* 1035 * This is an INCR-style transfer, which means that we 1036 * will get the data after a series of PropertyNotify events. 1037 */ 1038 DebugF("is INCR\n"); 1039 1040 if (get_property(e->requestor, e->property, &pdata, /*Delete*/ True, 1041 &type)) { 1042 /* 1043 * An error occurred, so we should invoke the copy_completed:, but 1044 * not handle_selection:type:propdata: 1045 */ 1046 [self copy_completed:e->selection]; 1047 return; 1048 } 1049 1050 free_propdata(&pdata); 1051 1052 pending.requestor = e->requestor; 1053 pending.selection = e->selection; 1054 1055 DebugF("set pending.requestor to 0x%lx\n", pending.requestor); 1056 } 1057 else { 1058 if (get_property(e->requestor, e->property, &pdata, /*Delete*/ True, 1059 &type)) { 1060 [self copy_completed:e->selection]; 1061 return; 1062 } 1063 1064 /* We have the complete selection data.*/ 1065 [self handle_selection:e->selection type:type propdata:&pdata]; 1066 1067 DebugF("handled selection with the first notify_event\n"); 1068 } 1069} 1070 1071/* This is used for INCR transfers. See the ICCCM for the details. */ 1072/* This is used to retrieve PRIMARY and CLIPBOARD selections. */ 1073- (void) property_event:(XPropertyEvent *)e 1074{ 1075 struct propdata pdata; 1076 Atom type; 1077 1078 TRACE(); 1079 1080 if (None != e->atom) { 1081#ifdef DEBUG 1082 char *name = XGetAtomName(xpbproxy_dpy, e->atom); 1083 1084 if (name) { 1085 DebugF("e->atom %s\n", name); 1086 XFree(name); 1087 } 1088#endif 1089 } 1090 1091 if (None != pending.requestor && PropertyNewValue == e->state) { 1092 DebugF("pending.requestor 0x%lx\n", pending.requestor); 1093 1094 if (get_property(e->window, e->atom, &pdata, /*Delete*/ True, 1095 &type)) { 1096 [self copy_completed:pending.selection]; 1097 [self release_pending]; 1098 return; 1099 } 1100 1101 if (0 == pdata.length) { 1102 /* 1103 * We completed the transfer. 1104 * handle_selection will call copy_completed: for us. 1105 */ 1106 [self handle_selection:pending.selection type:type propdata:& 1107 pending.propdata]; 1108 free_propdata(&pdata); 1109 pending.propdata = null_propdata; 1110 pending.requestor = None; 1111 pending.selection = None; 1112 } 1113 else { 1114 [self append_to_pending:&pdata requestor:e->window]; 1115 free_propdata(&pdata); 1116 } 1117 } 1118} 1119 1120- (void) xfixes_selection_notify:(XFixesSelectionNotifyEvent *)e 1121{ 1122 if (!pbproxy_prefs.active) 1123 return; 1124 1125 switch (e->subtype) { 1126 case XFixesSetSelectionOwnerNotify: 1127 if (e->selection == atoms->primary && pbproxy_prefs.primary_on_grab) 1128 [self x_copy:e->timestamp]; 1129 break; 1130 1131 case XFixesSelectionWindowDestroyNotify: 1132 case XFixesSelectionClientCloseNotify: 1133 default: 1134 ErrorF("Unhandled XFixesSelectionNotifyEvent: subtype=%d\n", 1135 e->subtype); 1136 break; 1137 } 1138} 1139 1140- (void) handle_targets: (Atom)selection propdata:(struct propdata *)pdata 1141{ 1142 /* Find a type we can handle and prefer from the list of ATOMs. */ 1143 Atom preferred; 1144 char *name; 1145 1146 TRACE(); 1147 1148 preferred = [self find_preferred:pdata]; 1149 1150 if (None == preferred) { 1151 /* 1152 * This isn't required by the ICCCM, but some apps apparently 1153 * don't respond to TARGETS properly. 1154 */ 1155 preferred = atoms->string; 1156 } 1157 1158 (void)name; /* Avoid a warning with non-debug compiles. */ 1159#ifdef DEBUG 1160 name = XGetAtomName(xpbproxy_dpy, preferred); 1161 1162 if (name) { 1163 DebugF("requesting %s\n", name); 1164 } 1165#endif 1166 request_atom = preferred; 1167 XConvertSelection(xpbproxy_dpy, selection, preferred, selection, 1168 _selection_window, CurrentTime); 1169} 1170 1171/* This handles the image type of selection (typically in CLIPBOARD). */ 1172/* We convert to a TIFF, so that other applications can paste more easily. */ 1173- (void) handle_image: (struct propdata *)pdata pasteboard:(NSPasteboard *)pb 1174{ 1175 NSArray *pbtypes; 1176 NSUInteger length; 1177 NSData *data, *tiff; 1178 NSBitmapImageRep *bmimage; 1179 1180 TRACE(); 1181 1182 length = pdata->length; 1183 data = [[NSData alloc] initWithBytes:pdata->data length:length]; 1184 1185 if (nil == data) { 1186 DebugF("unable to create NSData object!\n"); 1187 return; 1188 } 1189 1190 DebugF("data retainCount before NSBitmapImageRep initWithData: %lu\n", 1191 [data retainCount]); 1192 1193 bmimage = [[NSBitmapImageRep alloc] initWithData:data]; 1194 1195 if (nil == bmimage) { 1196 [data autorelease]; 1197 DebugF("unable to create NSBitmapImageRep!\n"); 1198 return; 1199 } 1200 1201 DebugF("data retainCount after NSBitmapImageRep initWithData: %lu\n", 1202 [data retainCount]); 1203 1204 @try 1205 { 1206 tiff = [bmimage TIFFRepresentation]; 1207 } 1208 1209 @catch (NSException *e) 1210 { 1211 DebugF("NSTIFFException!\n"); 1212 [data autorelease]; 1213 [bmimage autorelease]; 1214 return; 1215 } 1216 1217 DebugF("bmimage retainCount after TIFFRepresentation %lu\n", 1218 [bmimage retainCount]); 1219 1220 pbtypes = [NSArray arrayWithObjects:NSTIFFPboardType, nil]; 1221 1222 if (nil == pbtypes) { 1223 [data autorelease]; 1224 [bmimage autorelease]; 1225 return; 1226 } 1227 1228 [pb declareTypes:pbtypes owner:nil]; 1229 if (YES != [pb setData:tiff forType:NSTIFFPboardType]) { 1230 DebugF("writing pasteboard data failed!\n"); 1231 } 1232 1233 [data autorelease]; 1234 1235 DebugF("bmimage retainCount before release %lu\n", [bmimage retainCount]); 1236 1237 [bmimage autorelease]; 1238} 1239 1240/* This handles the UTF8_STRING type of selection. */ 1241- (void) handle_utf8_string:(struct propdata *)pdata pasteboard:(NSPasteboard 1242 *)pb 1243{ 1244 NSString *string; 1245 NSArray *pbtypes; 1246 1247 TRACE(); 1248 1249 string = 1250 [[NSString alloc] initWithBytes:pdata->data length:pdata->length 1251 encoding: 1252 NSUTF8StringEncoding]; 1253 1254 if (nil == string) 1255 return; 1256 1257 pbtypes = [NSArray arrayWithObjects:NSStringPboardType, nil]; 1258 1259 if (nil == pbtypes) { 1260 [string autorelease]; 1261 return; 1262 } 1263 1264 [pb declareTypes:pbtypes owner:nil]; 1265 1266 if (YES != [pb setString:string forType:NSStringPboardType]) { 1267 ErrorF("pasteboard setString:forType: failed!\n"); 1268 } 1269 [string autorelease]; 1270 DebugF("done handling utf8 string\n"); 1271} 1272 1273/* This handles the STRING type, which should be in Latin-1. */ 1274- (void) handle_string: (struct propdata *)pdata pasteboard:(NSPasteboard *) 1275 pb 1276{ 1277 NSString *string; 1278 NSArray *pbtypes; 1279 1280 TRACE(); 1281 1282 string = 1283 [[NSString alloc] initWithBytes:pdata->data length:pdata->length 1284 encoding: 1285 NSISOLatin1StringEncoding]; 1286 1287 if (nil == string) 1288 return; 1289 1290 pbtypes = [NSArray arrayWithObjects:NSStringPboardType, nil]; 1291 1292 if (nil == pbtypes) { 1293 [string autorelease]; 1294 return; 1295 } 1296 1297 [pb declareTypes:pbtypes owner:nil]; 1298 if (YES != [pb setString:string forType:NSStringPboardType]) { 1299 ErrorF("pasteboard setString:forType failed in handle_string!\n"); 1300 } 1301 [string autorelease]; 1302} 1303 1304/* This is called when the selection is completely retrieved from another client. */ 1305/* Warning: this frees the propdata. */ 1306- (void) handle_selection:(Atom)selection type:(Atom)type propdata:(struct 1307 propdata 1308 *)pdata 1309{ 1310 NSPasteboard *pb; 1311 1312 TRACE(); 1313 1314 pb = [NSPasteboard generalPasteboard]; 1315 1316 if (nil == pb) { 1317 [self copy_completed:selection]; 1318 free_propdata(pdata); 1319 return; 1320 } 1321 1322 /* 1323 * Some apps it seems set the type to TARGETS instead of ATOM, such as Eterm. 1324 * These aren't ICCCM compliant apps, but we need these to work... 1325 */ 1326 if (request_atom == atoms->targets 1327 && (type == atoms->atom || type == atoms->targets)) { 1328 [self handle_targets:selection propdata:pdata]; 1329 free_propdata(pdata); 1330 return; 1331 } 1332 else if (type == atoms->image_png) { 1333 [self handle_image:pdata pasteboard:pb]; 1334 } 1335 else if (type == atoms->image_jpeg) { 1336 [self handle_image:pdata pasteboard:pb]; 1337 } 1338 else if (type == atoms->utf8_string) { 1339 [self handle_utf8_string:pdata pasteboard:pb]; 1340 } 1341 else if (type == atoms->string) { 1342 [self handle_string:pdata pasteboard:pb]; 1343 } 1344 1345 free_propdata(pdata); 1346 1347 [self copy_completed:selection]; 1348} 1349 1350- (void) copy_completed:(Atom)selection 1351{ 1352 TRACE(); 1353 char *name; 1354 1355 (void)name; /* Avoid warning with non-debug compiles. */ 1356#ifdef DEBUG 1357 name = XGetAtomName(xpbproxy_dpy, selection); 1358 if (name) { 1359 DebugF("copy_completed: %s\n", name); 1360 XFree(name); 1361 } 1362#endif 1363 1364 if (selection == atoms->primary && pending_copy > 0) { 1365 --pending_copy; 1366 if (pending_copy > 0) { 1367 /* Copy PRIMARY again. */ 1368 [self x_copy_request_targets]; 1369 return; 1370 } 1371 } 1372 else if (selection == atoms->clipboard && pending_clipboard > 0) { 1373 --pending_clipboard; 1374 if (pending_clipboard > 0) { 1375 /* Copy CLIPBOARD. */ 1376 [self claim_clipboard]; 1377 return; 1378 } 1379 else { 1380 /* We got the final data. Now set pbproxy as the owner. */ 1381 [self own_clipboard]; 1382 return; 1383 } 1384 } 1385 1386 /* 1387 * We had 1 or more primary in progress, and the clipboard arrived 1388 * while we were busy. 1389 */ 1390 if (pending_clipboard > 0) { 1391 [self claim_clipboard]; 1392 } 1393} 1394 1395- (void) reload_preferences 1396{ 1397 /* 1398 * It's uncertain how we could handle the synchronization failing, so cast to void. 1399 * The prefs_get_bool should fall back to defaults if the org.x.X11 plist doesn't exist or is invalid. 1400 */ 1401 (void)CFPreferencesAppSynchronize(app_prefs_domain_cfstr); 1402#ifdef STANDALONE_XPBPROXY 1403 if (xpbproxy_is_standalone) 1404 pbproxy_prefs.active = YES; 1405 else 1406#endif 1407 pbproxy_prefs.active = prefs_get_bool(CFSTR( 1408 "sync_pasteboard"), 1409 pbproxy_prefs.active); 1410 pbproxy_prefs.primary_on_grab = 1411 prefs_get_bool(CFSTR( 1412 "sync_primary_on_select"), 1413 pbproxy_prefs.primary_on_grab); 1414 pbproxy_prefs.clipboard_to_pasteboard = 1415 prefs_get_bool(CFSTR( 1416 "sync_clipboard_to_pasteboard"), 1417 pbproxy_prefs.clipboard_to_pasteboard); 1418 pbproxy_prefs.pasteboard_to_primary = 1419 prefs_get_bool(CFSTR( 1420 "sync_pasteboard_to_primary"), 1421 pbproxy_prefs.pasteboard_to_primary); 1422 pbproxy_prefs.pasteboard_to_clipboard = 1423 prefs_get_bool(CFSTR( 1424 "sync_pasteboard_to_clipboard"), 1425 pbproxy_prefs.pasteboard_to_clipboard); 1426 1427 /* This is used for debugging. */ 1428 //dump_prefs(); 1429 1430 if (pbproxy_prefs.active && pbproxy_prefs.primary_on_grab && 1431 !xpbproxy_have_xfixes) { 1432 ErrorF( 1433 "Disabling sync_primary_on_select functionality due to missing XFixes extension.\n"); 1434 pbproxy_prefs.primary_on_grab = NO; 1435 } 1436 1437 /* Claim or release the CLIPBOARD_MANAGER atom */ 1438 if (![self set_clipboard_manager_status:(pbproxy_prefs.active && 1439 pbproxy_prefs. 1440 clipboard_to_pasteboard)]) 1441 pbproxy_prefs.clipboard_to_pasteboard = NO; 1442 1443 if (pbproxy_prefs.active && pbproxy_prefs.clipboard_to_pasteboard) 1444 [self claim_clipboard]; 1445} 1446 1447- (BOOL) is_active 1448{ 1449 return pbproxy_prefs.active; 1450} 1451 1452/* NSPasteboard-required methods */ 1453 1454- (void) paste:(id)sender 1455{ 1456 TRACE(); 1457} 1458 1459- (void) pasteboard:(NSPasteboard *)pb provideDataForType:(NSString *)type 1460{ 1461 TRACE(); 1462} 1463 1464- (void) pasteboardChangedOwner:(NSPasteboard *)pb 1465{ 1466 TRACE(); 1467 1468 /* Right now we don't care with this. */ 1469} 1470 1471/* Allocation */ 1472 1473- (id) init 1474{ 1475 unsigned long pixel; 1476 1477 self = [super init]; 1478 if (self == nil) 1479 return nil; 1480 1481 atoms->primary = XInternAtom(xpbproxy_dpy, "PRIMARY", False); 1482 atoms->clipboard = XInternAtom(xpbproxy_dpy, "CLIPBOARD", False); 1483 atoms->text = XInternAtom(xpbproxy_dpy, "TEXT", False); 1484 atoms->utf8_string = XInternAtom(xpbproxy_dpy, "UTF8_STRING", False); 1485 atoms->string = XInternAtom(xpbproxy_dpy, "STRING", False); 1486 atoms->targets = XInternAtom(xpbproxy_dpy, "TARGETS", False); 1487 atoms->multiple = XInternAtom(xpbproxy_dpy, "MULTIPLE", False); 1488 atoms->cstring = XInternAtom(xpbproxy_dpy, "CSTRING", False); 1489 atoms->image_png = XInternAtom(xpbproxy_dpy, "image/png", False); 1490 atoms->image_jpeg = XInternAtom(xpbproxy_dpy, "image/jpeg", False); 1491 atoms->incr = XInternAtom(xpbproxy_dpy, "INCR", False); 1492 atoms->atom = XInternAtom(xpbproxy_dpy, "ATOM", False); 1493 atoms->clipboard_manager = XInternAtom(xpbproxy_dpy, "CLIPBOARD_MANAGER", 1494 False); 1495 atoms->compound_text = XInternAtom(xpbproxy_dpy, "COMPOUND_TEXT", False); 1496 atoms->atom_pair = XInternAtom(xpbproxy_dpy, "ATOM_PAIR", False); 1497 1498 pixel = BlackPixel(xpbproxy_dpy, DefaultScreen(xpbproxy_dpy)); 1499 _selection_window = 1500 XCreateSimpleWindow(xpbproxy_dpy, DefaultRootWindow(xpbproxy_dpy), 1501 0, 0, 1, 1, 0, pixel, pixel); 1502 1503 /* This is used to get PropertyNotify events when doing INCR transfers. */ 1504 XSelectInput(xpbproxy_dpy, _selection_window, PropertyChangeMask); 1505 1506 request_atom = None; 1507 1508 init_propdata(&pending.propdata); 1509 pending.requestor = None; 1510 pending.selection = None; 1511 1512 pending_copy = 0; 1513 pending_clipboard = 0; 1514 1515 if (xpbproxy_have_xfixes) 1516 XFixesSelectSelectionInput(xpbproxy_dpy, _selection_window, 1517 atoms->primary, 1518 XFixesSetSelectionOwnerNotifyMask); 1519 1520 [self reload_preferences]; 1521 1522 return self; 1523} 1524 1525- (void) dealloc 1526{ 1527 if (None != _selection_window) { 1528 XDestroyWindow(xpbproxy_dpy, _selection_window); 1529 _selection_window = None; 1530 } 1531 1532 free_propdata(&pending.propdata); 1533 1534 [super dealloc]; 1535} 1536 1537@end 1538