Imported Upstream version 1.15.1
[deb_xorg-server.git] / hw / xquartz / pbproxy / x-selection.m
CommitLineData
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
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#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