mirror of
https://gitlab.gnome.org/GNOME/gtk.git
synced 2024-12-28 06:21:14 +00:00
9dbf99d91a
This is fairly substantial rewrite of the GDK backend for quartz and renamed to macOS to allow for a greenfield implementation. Many things have come across from the quartz implementation fairly intact such as the eventloop integration design and discovery of event windows from the NSEvent. However much has been changed to fit in with the new GDK design and how removal of child GdkWindow have been completely eliminated. Furthermore, the new GdkPopup allows for regular NSWindow to be used to provide popovers unlike the previous implementation. The object design more closely follows the ideal for a GDK backend. Views have been broken out into subclasses so that we can support multiple GSK renderer paths such as GL and Cairo (and Metal in the future). However mixed mode GL and Cairo will not be supported. Currently only the Cairo renderer has been implemented. A new frame clock implementation using CVDisplayLink provides more accurate information about when to draw drawing the next frame. Some testing will need to be done here to understand the power implications of this. This implementation has also gained edge snapping for CSD windows. Some work was also done to ensure that CSD windows have opaque regions registered with the display server. ** This is still very much a work-in-progress ** Some outstanding work that needs to be done: - Finish a GL context for macOS and alternate NSView for GL rendering (possibly using speciailized CALayer for OpenGL). - Input rework to ensure that we don't loose remapping of keys that was dropped from GDK during GTK 4 development. - Make sure input methods continue to work. - Drag-n-Drop is still very much a work in progress - High resolution input scrolling needs various work in GDK to land first before we can plumb that to NSEvent. - gtk/ has a number of things based on GDK_WINDOWING_QUARTZ that need to be updated to use the macOS backend. But this is good enough to start playing with and breaking things which is what I'd like to see.
577 lines
17 KiB
C
577 lines
17 KiB
C
/*
|
|
* Copyright © 2020 Red Hat, Inc.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <glib/gi18n.h>
|
|
|
|
#include "gdkmacosclipboard-private.h"
|
|
#include "gdkmacosutils-private.h"
|
|
|
|
struct _GdkMacosClipboard
|
|
{
|
|
GdkClipboard parent_instance;
|
|
NSPasteboard *pasteboard;
|
|
NSInteger last_change_count;
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
GMemoryOutputStream *stream;
|
|
NSPasteboardItem *item;
|
|
NSPasteboardType type;
|
|
GMainContext *main_context;
|
|
guint done : 1;
|
|
} WriteRequest;
|
|
|
|
G_DEFINE_TYPE (GdkMacosClipboard, _gdk_macos_clipboard, GDK_TYPE_CLIPBOARD)
|
|
|
|
static void
|
|
write_request_free (WriteRequest *wr)
|
|
{
|
|
g_clear_pointer (&wr->main_context, g_main_context_unref);
|
|
g_clear_object (&wr->stream);
|
|
[wr->item release];
|
|
g_slice_free (WriteRequest, wr);
|
|
}
|
|
|
|
const char *
|
|
_gdk_macos_clipboard_from_ns_type (NSPasteboardType type)
|
|
{
|
|
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
|
|
|
|
if ([type isEqualToString:NSPasteboardTypeString] ||
|
|
[type isEqualToString:NSStringPboardType])
|
|
return g_intern_string ("text/plain;charset=utf-8");
|
|
else if ([type isEqualToString:NSPasteboardTypeURL] ||
|
|
[type isEqualToString:NSPasteboardTypeFileURL])
|
|
return g_intern_string ("text/uri-list");
|
|
else if ([type isEqualToString:NSPasteboardTypeColor])
|
|
return g_intern_string ("application/x-color");
|
|
else if ([type isEqualToString:NSPasteboardTypeTIFF])
|
|
return g_intern_string ("image/tiff");
|
|
else if ([type isEqualToString:NSPasteboardTypePNG])
|
|
return g_intern_string ("image/png");
|
|
|
|
G_GNUC_END_IGNORE_DEPRECATIONS;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
NSPasteboardType
|
|
_gdk_macos_clipboard_to_ns_type (const char *mime_type,
|
|
NSPasteboardType *alternate)
|
|
{
|
|
if (alternate)
|
|
*alternate = NULL;
|
|
|
|
if (g_strcmp0 (mime_type, "text/plain;charset=utf-8") == 0)
|
|
{
|
|
return NSPasteboardTypeString;
|
|
}
|
|
else if (g_strcmp0 (mime_type, "text/uri-list") == 0)
|
|
{
|
|
if (alternate)
|
|
*alternate = NSPasteboardTypeURL;
|
|
return NSPasteboardTypeFileURL;
|
|
}
|
|
else if (g_strcmp0 (mime_type, "application/x-color") == 0)
|
|
{
|
|
return NSPasteboardTypeColor;
|
|
}
|
|
else if (g_strcmp0 (mime_type, "image/tiff") == 0)
|
|
{
|
|
return NSPasteboardTypeTIFF;
|
|
}
|
|
else if (g_strcmp0 (mime_type, "image/png") == 0)
|
|
{
|
|
return NSPasteboardTypePNG;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
static void
|
|
populate_content_formats (GdkContentFormatsBuilder *builder,
|
|
NSPasteboardType type)
|
|
{
|
|
const char *mime_type;
|
|
|
|
g_return_if_fail (builder != NULL);
|
|
g_return_if_fail (type != NULL);
|
|
|
|
mime_type = _gdk_macos_clipboard_from_ns_type (type);
|
|
|
|
if (mime_type != NULL)
|
|
gdk_content_formats_builder_add_mime_type (builder, mime_type);
|
|
}
|
|
|
|
static GdkContentFormats *
|
|
load_offer_formats (GdkMacosClipboard *self)
|
|
{
|
|
GDK_BEGIN_MACOS_ALLOC_POOL;
|
|
|
|
GdkContentFormatsBuilder *builder;
|
|
GdkContentFormats *formats;
|
|
|
|
g_assert (GDK_IS_MACOS_CLIPBOARD (self));
|
|
|
|
builder = gdk_content_formats_builder_new ();
|
|
for (NSPasteboardType type in [self->pasteboard types])
|
|
populate_content_formats (builder, type);
|
|
formats = gdk_content_formats_builder_free_to_formats (builder);
|
|
|
|
GDK_END_MACOS_ALLOC_POOL;
|
|
|
|
return g_steal_pointer (&formats);
|
|
}
|
|
|
|
static void
|
|
_gdk_macos_clipboard_load_contents (GdkMacosClipboard *self)
|
|
{
|
|
GdkContentFormats *formats;
|
|
NSInteger change_count;
|
|
|
|
g_assert (GDK_IS_MACOS_CLIPBOARD (self));
|
|
|
|
change_count = [self->pasteboard changeCount];
|
|
|
|
formats = load_offer_formats (self);
|
|
gdk_clipboard_claim_remote (GDK_CLIPBOARD (self), formats);
|
|
gdk_content_formats_unref (formats);
|
|
|
|
self->last_change_count = change_count;
|
|
}
|
|
|
|
static GInputStream *
|
|
create_stream_from_nsdata (NSData *data)
|
|
{
|
|
const guint8 *bytes = [data bytes];
|
|
gsize len = [data length];
|
|
|
|
return g_memory_input_stream_new_from_data (g_memdup (bytes, len), len, g_free);
|
|
}
|
|
|
|
static void
|
|
_gdk_macos_clipboard_read_async (GdkClipboard *clipboard,
|
|
GdkContentFormats *formats,
|
|
int io_priority,
|
|
GCancellable *cancellable,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
GDK_BEGIN_MACOS_ALLOC_POOL;
|
|
|
|
GdkMacosClipboard *self = (GdkMacosClipboard *)clipboard;
|
|
GdkContentFormats *offer_formats = NULL;
|
|
const char *mime_type;
|
|
GInputStream *stream = NULL;
|
|
GTask *task = NULL;
|
|
|
|
g_assert (GDK_IS_MACOS_CLIPBOARD (self));
|
|
g_assert (formats != NULL);
|
|
|
|
task = g_task_new (self, cancellable, callback, user_data);
|
|
g_task_set_source_tag (task, _gdk_macos_clipboard_read_async);
|
|
g_task_set_priority (task, io_priority);
|
|
|
|
offer_formats = load_offer_formats (GDK_MACOS_CLIPBOARD (clipboard));
|
|
mime_type = gdk_content_formats_match_mime_type (formats, offer_formats);
|
|
|
|
if (mime_type == NULL)
|
|
{
|
|
g_task_return_new_error (task,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
"%s",
|
|
_("No compatible transfer format found"));
|
|
goto cleanup;
|
|
}
|
|
|
|
if (strcmp (mime_type, "text/plain;charset=utf-8") == 0)
|
|
{
|
|
NSString *nsstr = [self->pasteboard stringForType:NSPasteboardTypeString];
|
|
|
|
if (nsstr != NULL)
|
|
{
|
|
const char *str = [nsstr UTF8String];
|
|
stream = g_memory_input_stream_new_from_data (g_strdup (str),
|
|
strlen (str) + 1,
|
|
g_free);
|
|
}
|
|
}
|
|
else if (strcmp (mime_type, "text/uri-list") == 0)
|
|
{
|
|
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
|
|
|
|
if ([[self->pasteboard types] containsObject:NSPasteboardTypeFileURL])
|
|
{
|
|
GString *str = g_string_new (NULL);
|
|
NSArray *files = [self->pasteboard propertyListForType:NSFilenamesPboardType];
|
|
gsize n_files = [files count];
|
|
char *data;
|
|
guint len;
|
|
|
|
for (gsize i = 0; i < n_files; ++i)
|
|
{
|
|
NSString* uriString = [files objectAtIndex:i];
|
|
uriString = [@"file://" stringByAppendingString:uriString];
|
|
uriString = [uriString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
g_string_append_printf (str,
|
|
"%s\r\n",
|
|
[uriString cStringUsingEncoding:NSUTF8StringEncoding]);
|
|
}
|
|
|
|
len = str->len;
|
|
data = g_string_free (str, FALSE);
|
|
stream = g_memory_input_stream_new_from_data (data, len, g_free);
|
|
}
|
|
|
|
G_GNUC_END_IGNORE_DEPRECATIONS;
|
|
}
|
|
else if (strcmp (mime_type, "application/x-color") == 0)
|
|
{
|
|
NSColorSpace *colorspace;
|
|
NSColor *nscolor;
|
|
guint16 color[4];
|
|
|
|
colorspace = [NSColorSpace genericRGBColorSpace];
|
|
nscolor = [[NSColor colorFromPasteboard:self->pasteboard]
|
|
colorUsingColorSpace:colorspace];
|
|
|
|
color[0] = 0xffff * [nscolor redComponent];
|
|
color[1] = 0xffff * [nscolor greenComponent];
|
|
color[2] = 0xffff * [nscolor blueComponent];
|
|
color[3] = 0xffff * [nscolor alphaComponent];
|
|
|
|
stream = g_memory_input_stream_new_from_data (g_memdup (&color, sizeof color),
|
|
sizeof color,
|
|
g_free);
|
|
}
|
|
else if (strcmp (mime_type, "image/tiff") == 0)
|
|
{
|
|
NSData *data = [self->pasteboard dataForType:NSPasteboardTypeTIFF];
|
|
stream = create_stream_from_nsdata (data);
|
|
}
|
|
else if (strcmp (mime_type, "image/png") == 0)
|
|
{
|
|
NSData *data = [self->pasteboard dataForType:NSPasteboardTypePNG];
|
|
stream = create_stream_from_nsdata (data);
|
|
}
|
|
|
|
if (stream != NULL)
|
|
{
|
|
g_task_set_task_data (task, g_strdup (mime_type), g_free);
|
|
g_task_return_pointer (task, g_steal_pointer (&stream), g_object_unref);
|
|
}
|
|
else
|
|
{
|
|
g_task_return_new_error (task,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
_("Failed to decode contents with mime-type of '%s'"),
|
|
mime_type);
|
|
}
|
|
|
|
cleanup:
|
|
g_clear_object (&task);
|
|
g_clear_pointer (&offer_formats, gdk_content_formats_unref);
|
|
|
|
GDK_END_MACOS_ALLOC_POOL;
|
|
}
|
|
|
|
static GInputStream *
|
|
_gdk_macos_clipboard_read_finish (GdkClipboard *clipboard,
|
|
GAsyncResult *result,
|
|
const char **out_mime_type,
|
|
GError **error)
|
|
{
|
|
GTask *task = (GTask *)result;
|
|
|
|
g_assert (GDK_IS_MACOS_CLIPBOARD (clipboard));
|
|
g_assert (G_IS_TASK (task));
|
|
|
|
if (out_mime_type != NULL)
|
|
*out_mime_type = g_strdup (g_task_get_task_data (task));
|
|
|
|
return g_task_propagate_pointer (task, error);
|
|
}
|
|
|
|
static void
|
|
_gdk_macos_clipboard_send_to_pasteboard (GdkMacosClipboard *self,
|
|
GdkContentProvider *content)
|
|
{
|
|
GDK_BEGIN_MACOS_ALLOC_POOL;
|
|
|
|
GdkMacosClipboardDataProvider *dataProvider;
|
|
GdkContentFormats *serializable;
|
|
NSPasteboardItem *item;
|
|
const char * const *mime_types;
|
|
gsize n_mime_types;
|
|
|
|
g_return_if_fail (GDK_IS_MACOS_CLIPBOARD (self));
|
|
g_return_if_fail (GDK_IS_CONTENT_PROVIDER (content));
|
|
|
|
serializable = gdk_content_provider_ref_storable_formats (content);
|
|
serializable = gdk_content_formats_union_serialize_mime_types (serializable);
|
|
mime_types = gdk_content_formats_get_mime_types (serializable, &n_mime_types);
|
|
|
|
dataProvider = [[GdkMacosClipboardDataProvider alloc] initClipboard:GDK_CLIPBOARD (self)
|
|
mimetypes:mime_types];
|
|
item = [[NSPasteboardItem alloc] init];
|
|
[item setDataProvider:dataProvider forTypes:[dataProvider types]];
|
|
|
|
[self->pasteboard clearContents];
|
|
if ([self->pasteboard writeObjects:[NSArray arrayWithObject:item]] == NO)
|
|
g_warning ("Failed to write object to pasteboard");
|
|
|
|
self->last_change_count = [self->pasteboard changeCount];
|
|
|
|
g_clear_pointer (&serializable, gdk_content_formats_unref);
|
|
|
|
GDK_END_MACOS_ALLOC_POOL;
|
|
}
|
|
|
|
static gboolean
|
|
_gdk_macos_clipboard_claim (GdkClipboard *clipboard,
|
|
GdkContentFormats *formats,
|
|
gboolean local,
|
|
GdkContentProvider *provider)
|
|
{
|
|
GdkMacosClipboard *self = (GdkMacosClipboard *)clipboard;
|
|
gboolean ret;
|
|
|
|
g_assert (GDK_IS_CLIPBOARD (clipboard));
|
|
g_assert (formats != NULL);
|
|
g_assert (!provider || GDK_IS_CONTENT_PROVIDER (provider));
|
|
|
|
ret = GDK_CLIPBOARD_CLASS (_gdk_macos_clipboard_parent_class)->claim (clipboard, formats, local, provider);
|
|
|
|
if (local)
|
|
_gdk_macos_clipboard_send_to_pasteboard (self, provider);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
_gdk_macos_clipboard_constructed (GObject *object)
|
|
{
|
|
GdkMacosClipboard *self = (GdkMacosClipboard *)object;
|
|
|
|
if (self->pasteboard == nil)
|
|
self->pasteboard = [[NSPasteboard generalPasteboard] retain];
|
|
|
|
G_OBJECT_CLASS (_gdk_macos_clipboard_parent_class)->constructed (object);
|
|
}
|
|
|
|
static void
|
|
_gdk_macos_clipboard_finalize (GObject *object)
|
|
{
|
|
GdkMacosClipboard *self = (GdkMacosClipboard *)object;
|
|
|
|
if (self->pasteboard != nil)
|
|
{
|
|
[self->pasteboard release];
|
|
self->pasteboard = nil;
|
|
}
|
|
|
|
G_OBJECT_CLASS (_gdk_macos_clipboard_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
_gdk_macos_clipboard_class_init (GdkMacosClipboardClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GdkClipboardClass *clipboard_class = GDK_CLIPBOARD_CLASS (klass);
|
|
|
|
object_class->constructed = _gdk_macos_clipboard_constructed;
|
|
object_class->finalize = _gdk_macos_clipboard_finalize;
|
|
|
|
clipboard_class->claim = _gdk_macos_clipboard_claim;
|
|
clipboard_class->read_async = _gdk_macos_clipboard_read_async;
|
|
clipboard_class->read_finish = _gdk_macos_clipboard_read_finish;
|
|
}
|
|
|
|
static void
|
|
_gdk_macos_clipboard_init (GdkMacosClipboard *self)
|
|
{
|
|
}
|
|
|
|
GdkClipboard *
|
|
_gdk_macos_clipboard_new (GdkMacosDisplay *display)
|
|
{
|
|
GdkMacosClipboard *self;
|
|
|
|
g_return_val_if_fail (GDK_IS_MACOS_DISPLAY (display), NULL);
|
|
|
|
self = g_object_new (GDK_TYPE_MACOS_CLIPBOARD,
|
|
"display", display,
|
|
NULL);
|
|
|
|
_gdk_macos_clipboard_load_contents (self);
|
|
|
|
return GDK_CLIPBOARD (self);
|
|
}
|
|
|
|
void
|
|
_gdk_macos_clipboard_check_externally_modified (GdkMacosClipboard *self)
|
|
{
|
|
g_return_if_fail (GDK_IS_MACOS_CLIPBOARD (self));
|
|
|
|
if ([self->pasteboard changeCount] != self->last_change_count)
|
|
_gdk_macos_clipboard_load_contents (self);
|
|
}
|
|
|
|
@implementation GdkMacosClipboardDataProvider
|
|
|
|
-(id)initClipboard:(GdkClipboard *)gdkClipboard mimetypes:(const char * const *)mime_types;
|
|
{
|
|
[super init];
|
|
|
|
self->mimeTypes = g_strdupv ((char **)mime_types);
|
|
self->clipboard = g_object_ref (gdkClipboard);
|
|
|
|
return self;
|
|
}
|
|
|
|
-(void)dealloc
|
|
{
|
|
g_cancellable_cancel (self->cancellable);
|
|
|
|
g_clear_pointer (&self->mimeTypes, g_strfreev);
|
|
g_clear_object (&self->clipboard);
|
|
g_clear_object (&self->cancellable);
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
-(void)pasteboardFinishedWithDataProvider:(NSPasteboard *)pasteboard
|
|
{
|
|
g_clear_object (&self->clipboard);
|
|
}
|
|
|
|
-(NSArray<NSPasteboardType> *)types
|
|
{
|
|
NSMutableArray *ret = [[NSMutableArray alloc] init];
|
|
|
|
for (guint i = 0; self->mimeTypes[i]; i++)
|
|
{
|
|
const char *mime_type = self->mimeTypes[i];
|
|
NSPasteboardType type;
|
|
NSPasteboardType alternate = nil;
|
|
|
|
if ((type = _gdk_macos_clipboard_to_ns_type (mime_type, &alternate)))
|
|
{
|
|
[ret addObject:type];
|
|
if (alternate)
|
|
[ret addObject:alternate];
|
|
}
|
|
}
|
|
|
|
return g_steal_pointer (&ret);
|
|
}
|
|
|
|
static void
|
|
on_data_ready_cb (GObject *object,
|
|
GAsyncResult *result,
|
|
gpointer user_data)
|
|
{
|
|
GDK_BEGIN_MACOS_ALLOC_POOL;
|
|
|
|
GdkClipboard *clipboard = (GdkClipboard *)object;
|
|
WriteRequest *wr = user_data;
|
|
GError *error = NULL;
|
|
NSData *data = nil;
|
|
|
|
g_assert (GDK_IS_CLIPBOARD (clipboard));
|
|
g_assert (G_IS_ASYNC_RESULT (result));
|
|
g_assert (wr != NULL);
|
|
g_assert (G_IS_MEMORY_OUTPUT_STREAM (wr->stream));
|
|
g_assert ([wr->item isKindOfClass:[NSPasteboardItem class]]);
|
|
|
|
if (gdk_clipboard_write_finish (clipboard, result, &error))
|
|
{
|
|
gsize size;
|
|
gpointer bytes;
|
|
|
|
g_output_stream_close (G_OUTPUT_STREAM (wr->stream), NULL, NULL);
|
|
|
|
size = g_memory_output_stream_get_size (wr->stream);
|
|
bytes = g_memory_output_stream_steal_data (wr->stream);
|
|
data = [[NSData alloc] initWithBytesNoCopy:bytes
|
|
length:size
|
|
deallocator:^(void *alloc, NSUInteger length) { g_free (alloc); }];
|
|
}
|
|
else
|
|
{
|
|
g_warning ("Failed to serialize clipboard contents: %s",
|
|
error->message);
|
|
g_clear_error (&error);
|
|
}
|
|
|
|
[wr->item setData:data forType:wr->type];
|
|
|
|
wr->done = TRUE;
|
|
|
|
GDK_END_MACOS_ALLOC_POOL;
|
|
}
|
|
|
|
-(void) pasteboard:(NSPasteboard *)pasteboard
|
|
item:(NSPasteboardItem *)item
|
|
provideDataForType:(NSPasteboardType)type
|
|
{
|
|
const char *mime_type = _gdk_macos_clipboard_from_ns_type (type);
|
|
GMainContext *main_context = g_main_context_default ();
|
|
WriteRequest *wr;
|
|
|
|
if (self->clipboard == NULL || mime_type == NULL)
|
|
{
|
|
[item setData:[NSData data] forType:type];
|
|
return;
|
|
}
|
|
|
|
wr = g_slice_new0 (WriteRequest);
|
|
wr->item = [item retain];
|
|
wr->stream = G_MEMORY_OUTPUT_STREAM (g_memory_output_stream_new_resizable ());
|
|
wr->type = type;
|
|
wr->main_context = g_main_context_ref (main_context);
|
|
wr->done = FALSE;
|
|
|
|
gdk_clipboard_write_async (self->clipboard,
|
|
mime_type,
|
|
G_OUTPUT_STREAM (wr->stream),
|
|
G_PRIORITY_DEFAULT,
|
|
self->cancellable,
|
|
on_data_ready_cb,
|
|
wr);
|
|
|
|
/* We're forced to provide data synchronously via this API
|
|
* so we must block on the main loop. Using another main loop
|
|
* than the default tends to get us locked up here, so that is
|
|
* what we'll do for now.
|
|
*/
|
|
while (!wr->done)
|
|
g_main_context_iteration (wr->main_context, TRUE);
|
|
|
|
write_request_free (wr);
|
|
}
|
|
|
|
@end
|