diff mbox series

ui/cocoa: Add clipboard support

Message ID 20210616141954.54291-1-akihiko.odaki@gmail.com (mailing list archive)
State New, archived
Headers show
Series ui/cocoa: Add clipboard support | expand

Commit Message

Akihiko Odaki June 16, 2021, 2:19 p.m. UTC
Signed-off-by: Akihiko Odaki <akihiko.odaki@gmail.com>
---
 include/ui/clipboard.h |   2 +-
 ui/clipboard.c         |   2 +-
 ui/cocoa.m             | 109 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 111 insertions(+), 2 deletions(-)

Comments

Gerd Hoffmann June 23, 2021, 12:44 p.m. UTC | #1
On Wed, Jun 16, 2021 at 11:19:54PM +0900, Akihiko Odaki wrote:
> Signed-off-by: Akihiko Odaki <akihiko.odaki@gmail.com>

Added to UI queue.

thanks,
  Gerd
Peter Maydell Feb. 8, 2022, 5:54 p.m. UTC | #2
On Wed, 16 Jun 2021 at 15:20, Akihiko Odaki <akihiko.odaki@gmail.com> wrote:
>
> Signed-off-by: Akihiko Odaki <akihiko.odaki@gmail.com>

Hi Akihiko -- I have a similar question here to the other
patch about doing things not on the Cocoa UI thread...

> +static void cocoa_clipboard_notify(Notifier *notifier, void *data)
> +{
> +    QemuClipboardInfo *info = data;
> +
> +    if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) {
> +        return;
> +    }
> +
> +    if (info != cbinfo) {
> +        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
> +        qemu_clipboard_info_unref(cbinfo);
> +        cbinfo = qemu_clipboard_info_ref(info);
> +        cbchangecount = [[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:cbowner];
> +        [pool release];

Is this OK to do on a non-Cocoa thread with an autorelease pool,
or should it be done via dispatch_async ?

> +    }
> +
> +    qemu_event_set(&cbevent);
> +}

>  /*
>   * The startup process for the OSX/Cocoa UI is complicated, because
>   * OSX insists that the UI runs on the initial main thread, and so we
> @@ -1845,6 +1937,7 @@ static void addRemovableDevicesMenuItems(void)
>      COCOA_DEBUG("Second thread: calling qemu_main()\n");
>      status = qemu_main(gArgc, gArgv, *_NSGetEnviron());
>      COCOA_DEBUG("Second thread: qemu_main() returned, exiting\n");
> +    [cbowner release];
>      exit(status);
>  }
>
> @@ -1965,6 +2058,18 @@ static void cocoa_refresh(DisplayChangeListener *dcl)
>              [cocoaView setAbsoluteEnabled:YES];
>          });
>      }
> +
> +    if (cbchangecount != [[NSPasteboard generalPasteboard] changeCount]) {
> +        qemu_clipboard_info_unref(cbinfo);
> +        cbinfo = qemu_clipboard_info_new(&cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
> +        if ([[NSPasteboard generalPasteboard] availableTypeFromArray:@[NSPasteboardTypeString]]) {
> +            cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
> +        }
> +        qemu_clipboard_update(cbinfo);
> +        cbchangecount = [[NSPasteboard generalPasteboard] changeCount];
> +        qemu_event_set(&cbevent);
> +    }
> +

This work in the cocoa_refresh() function is done not on the Cocoa
UI thread. Is it OK for it to do that, or should we put it into a
dispatch_async block ?

>      [pool release];
>  }

thanks
-- PMM
Akihiko Odaki Feb. 9, 2022, 11:27 a.m. UTC | #3
On Wed, Feb 9, 2022 at 2:54 AM Peter Maydell <peter.maydell@linaro.org> wrote:
>
> On Wed, 16 Jun 2021 at 15:20, Akihiko Odaki <akihiko.odaki@gmail.com> wrote:
> >
> > Signed-off-by: Akihiko Odaki <akihiko.odaki@gmail.com>
>
> Hi Akihiko -- I have a similar question here to the other
> patch about doing things not on the Cocoa UI thread...
>
> > +static void cocoa_clipboard_notify(Notifier *notifier, void *data)
> > +{
> > +    QemuClipboardInfo *info = data;
> > +
> > +    if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) {
> > +        return;
> > +    }
> > +
> > +    if (info != cbinfo) {
> > +        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
> > +        qemu_clipboard_info_unref(cbinfo);
> > +        cbinfo = qemu_clipboard_info_ref(info);
> > +        cbchangecount = [[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:cbowner];
> > +        [pool release];
>
> Is this OK to do on a non-Cocoa thread with an autorelease pool,
> or should it be done via dispatch_async ?
>
> > +    }
> > +
> > +    qemu_event_set(&cbevent);
> > +}
>
> >  /*
> >   * The startup process for the OSX/Cocoa UI is complicated, because
> >   * OSX insists that the UI runs on the initial main thread, and so we
> > @@ -1845,6 +1937,7 @@ static void addRemovableDevicesMenuItems(void)
> >      COCOA_DEBUG("Second thread: calling qemu_main()\n");
> >      status = qemu_main(gArgc, gArgv, *_NSGetEnviron());
> >      COCOA_DEBUG("Second thread: qemu_main() returned, exiting\n");
> > +    [cbowner release];
> >      exit(status);
> >  }
> >
> > @@ -1965,6 +2058,18 @@ static void cocoa_refresh(DisplayChangeListener *dcl)
> >              [cocoaView setAbsoluteEnabled:YES];
> >          });
> >      }
> > +
> > +    if (cbchangecount != [[NSPasteboard generalPasteboard] changeCount]) {
> > +        qemu_clipboard_info_unref(cbinfo);
> > +        cbinfo = qemu_clipboard_info_new(&cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
> > +        if ([[NSPasteboard generalPasteboard] availableTypeFromArray:@[NSPasteboardTypeString]]) {
> > +            cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
> > +        }
> > +        qemu_clipboard_update(cbinfo);
> > +        cbchangecount = [[NSPasteboard generalPasteboard] changeCount];
> > +        qemu_event_set(&cbevent);
> > +    }
> > +
>
> This work in the cocoa_refresh() function is done not on the Cocoa
> UI thread. Is it OK for it to do that, or should we put it into a
> dispatch_async block ?
>
> >      [pool release];
> >  }
>
> thanks
> -- PMM

It should be fine since it doesn't touch NSView. The following
documentation is the latest one which I have found so far:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-SW1

It is unfortunate that Apple no longer updates it. They should at
least note about the main thread requirement in the documentation of
NSView.

Regards,
Akihiko Odaki
diff mbox series

Patch

diff --git a/include/ui/clipboard.h b/include/ui/clipboard.h
index e5bcb365ed6..b45b984c9fe 100644
--- a/include/ui/clipboard.h
+++ b/include/ui/clipboard.h
@@ -187,7 +187,7 @@  void qemu_clipboard_set_data(QemuClipboardPeer *peer,
                              QemuClipboardInfo *info,
                              QemuClipboardType type,
                              uint32_t size,
-                             void *data,
+                             const void *data,
                              bool update);
 
 #endif /* QEMU_CLIPBOARD_H */
diff --git a/ui/clipboard.c b/ui/clipboard.c
index abf2b98f1f8..3525b30178b 100644
--- a/ui/clipboard.c
+++ b/ui/clipboard.c
@@ -73,7 +73,7 @@  void qemu_clipboard_set_data(QemuClipboardPeer *peer,
                              QemuClipboardInfo *info,
                              QemuClipboardType type,
                              uint32_t size,
-                             void *data,
+                             const void *data,
                              bool update)
 {
     if (!info ||
diff --git a/ui/cocoa.m b/ui/cocoa.m
index 995301502be..1efd70ea789 100644
--- a/ui/cocoa.m
+++ b/ui/cocoa.m
@@ -28,6 +28,7 @@ 
 #include <crt_externs.h>
 
 #include "qemu-common.h"
+#include "ui/clipboard.h"
 #include "ui/console.h"
 #include "ui/input.h"
 #include "ui/kbd-state.h"
@@ -106,6 +107,10 @@  static void cocoa_switch(DisplayChangeListener *dcl,
 static QemuSemaphore app_started_sem;
 static bool allow_events;
 
+static NSInteger cbchangecount = -1;
+static QemuClipboardInfo *cbinfo;
+static QemuEvent cbevent;
+
 // Utility functions to run specified code block with iothread lock held
 typedef void (^CodeBlock)(void);
 typedef bool (^BoolCodeBlock)(void);
@@ -1811,6 +1816,93 @@  static void addRemovableDevicesMenuItems(void)
     qapi_free_BlockInfoList(pointerToFree);
 }
 
+@interface QemuCocoaPasteboardTypeOwner : NSObject<NSPasteboardTypeOwner>
+@end
+
+@implementation QemuCocoaPasteboardTypeOwner
+
+- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSPasteboardType)type
+{
+    if (type != NSPasteboardTypeString) {
+        return;
+    }
+
+    with_iothread_lock(^{
+        QemuClipboardInfo *info = qemu_clipboard_info_ref(cbinfo);
+        qemu_event_reset(&cbevent);
+        qemu_clipboard_request(info, QEMU_CLIPBOARD_TYPE_TEXT);
+
+        while (info == cbinfo &&
+               info->types[QEMU_CLIPBOARD_TYPE_TEXT].available &&
+               info->types[QEMU_CLIPBOARD_TYPE_TEXT].data == NULL) {
+            qemu_mutex_unlock_iothread();
+            qemu_event_wait(&cbevent);
+            qemu_mutex_lock_iothread();
+        }
+
+        if (info == cbinfo) {
+            NSData *data = [[NSData alloc] initWithBytes:info->types[QEMU_CLIPBOARD_TYPE_TEXT].data
+                                           length:info->types[QEMU_CLIPBOARD_TYPE_TEXT].size];
+            [sender setData:data forType:NSPasteboardTypeString];
+            [data release];
+        }
+
+        qemu_clipboard_info_unref(info);
+    });
+}
+
+@end
+
+static QemuCocoaPasteboardTypeOwner *cbowner;
+
+static void cocoa_clipboard_notify(Notifier *notifier, void *data);
+static void cocoa_clipboard_request(QemuClipboardInfo *info,
+                                    QemuClipboardType type);
+
+static QemuClipboardPeer cbpeer = {
+    .name = "cocoa",
+    .update = { .notify = cocoa_clipboard_notify },
+    .request = cocoa_clipboard_request
+};
+
+static void cocoa_clipboard_notify(Notifier *notifier, void *data)
+{
+    QemuClipboardInfo *info = data;
+
+    if (info->owner == &cbpeer || info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) {
+        return;
+    }
+
+    if (info != cbinfo) {
+        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
+        qemu_clipboard_info_unref(cbinfo);
+        cbinfo = qemu_clipboard_info_ref(info);
+        cbchangecount = [[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:cbowner];
+        [pool release];
+    }
+
+    qemu_event_set(&cbevent);
+}
+
+static void cocoa_clipboard_request(QemuClipboardInfo *info,
+                                    QemuClipboardType type)
+{
+    NSData *text;
+
+    switch (type) {
+    case QEMU_CLIPBOARD_TYPE_TEXT:
+        text = [[NSPasteboard generalPasteboard] dataForType:NSPasteboardTypeString];
+        if (text) {
+            qemu_clipboard_set_data(&cbpeer, info, type,
+                                    [text length], [text bytes], true);
+            [text release];
+        }
+        break;
+    default:
+        break;
+    }
+}
+
 /*
  * The startup process for the OSX/Cocoa UI is complicated, because
  * OSX insists that the UI runs on the initial main thread, and so we
@@ -1845,6 +1937,7 @@  static void addRemovableDevicesMenuItems(void)
     COCOA_DEBUG("Second thread: calling qemu_main()\n");
     status = qemu_main(gArgc, gArgv, *_NSGetEnviron());
     COCOA_DEBUG("Second thread: qemu_main() returned, exiting\n");
+    [cbowner release];
     exit(status);
 }
 
@@ -1965,6 +2058,18 @@  static void cocoa_refresh(DisplayChangeListener *dcl)
             [cocoaView setAbsoluteEnabled:YES];
         });
     }
+
+    if (cbchangecount != [[NSPasteboard generalPasteboard] changeCount]) {
+        qemu_clipboard_info_unref(cbinfo);
+        cbinfo = qemu_clipboard_info_new(&cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
+        if ([[NSPasteboard generalPasteboard] availableTypeFromArray:@[NSPasteboardTypeString]]) {
+            cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
+        }
+        qemu_clipboard_update(cbinfo);
+        cbchangecount = [[NSPasteboard generalPasteboard] changeCount];
+        qemu_event_set(&cbevent);
+    }
+
     [pool release];
 }
 
@@ -2001,6 +2106,10 @@  static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts)
 
     // register vga output callbacks
     register_displaychangelistener(&dcl);
+
+    qemu_event_init(&cbevent, false);
+    cbowner = [[QemuCocoaPasteboardTypeOwner alloc] init];
+    qemu_clipboard_peer_register(&cbpeer);
 }
 
 static QemuDisplay qemu_display_cocoa = {