PIKApp/libpikawidgets/pikapickbutton-quartz.c

464 lines
13 KiB
C

/* LIBPIKA - The PIKA Library
* Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
*
* pikapickbutton-quartz.c
* Copyright (C) 2015 Kristian Rietveld <kris@loopnest.org>
*
* 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
* Library 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
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <gegl.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include "libpikacolor/pikacolor.h"
#include "pikawidgetstypes.h"
#include "pikapickbutton.h"
#include "pikapickbutton-private.h"
#include "pikapickbutton-quartz.h"
#ifdef GDK_WINDOWING_QUARTZ
#import <AppKit/AppKit.h>
#include <Carbon/Carbon.h> /* For virtual key codes ... */
#include <ApplicationServices/ApplicationServices.h>
#endif
@interface PikaPickWindowController : NSObject
{
PikaPickButton *button;
NSMutableArray *windows;
}
@property (nonatomic, assign) BOOL firstBecameKey;
@property (readonly, retain) NSCursor *cursor;
- (id)initWithButton:(PikaPickButton *)_button;
- (void)updateKeyWindow;
- (void)shutdown;
@end
@interface PikaPickView : NSView
{
PikaPickButton *button;
PikaPickWindowController *controller;
}
@property (readonly,assign) NSTrackingArea *area;
- (id)initWithButton:(PikaPickButton *)_button controller:(PikaPickWindowController *)controller;
@end
@implementation PikaPickView
@synthesize area;
- (id)initWithButton:(PikaPickButton *)_button controller:(PikaPickWindowController *)_controller
{
self = [super init];
if (self)
{
button = _button;
controller = _controller;
}
return self;
}
- (void)dealloc
{
[self removeTrackingArea:self.area];
[super dealloc];
}
- (void)viewDidMoveToWindow
{
NSTrackingAreaOptions options;
[super viewDidMoveToWindow];
if ([self window] == nil)
return;
options = NSTrackingMouseEnteredAndExited |
NSTrackingMouseMoved |
NSTrackingActiveAlways;
/* Add assume inside if mouse pointer is above this window */
if (NSPointInRect ([NSEvent mouseLocation], self.window.frame))
options |= NSTrackingAssumeInside;
area = [[NSTrackingArea alloc] initWithRect:self.bounds
options:options
owner:self
userInfo:nil];
[self addTrackingArea:self.area];
}
- (void)mouseEntered:(NSEvent *)event
{
/* We handle the mouse cursor manually, see also the comment in
* [PikaPickWindow windowDidBecomeMain below].
*/
if (controller.cursor)
[controller.cursor push];
}
- (void)mouseExited:(NSEvent *)event
{
if (controller.cursor)
[controller.cursor pop];
[controller updateKeyWindow];
}
- (void)mouseMoved:(NSEvent *)event
{
[self pickColor:event];
}
- (void)mouseUp:(NSEvent *)event
{
[self pickColor:event];
[controller shutdown];
}
- (void)rightMouseUp:(NSEvent *)event
{
[self mouseUp:event];
}
- (void)otherMouseUp:(NSEvent *)event
{
[self mouseUp:event];
}
- (void)keyDown:(NSEvent *)event
{
if (event.keyCode == kVK_Escape)
[controller shutdown];
}
- (void)pickColor:(NSEvent *)event
{
CGImageRef root_image_ref;
CFDataRef pixel_data;
const guchar *data;
PikaRGB rgb;
NSPoint point;
PikaColorProfile *profile = NULL;
CGColorSpaceRef color_space = NULL;
/* The event gives us a point in Cocoa window coordinates. The function
* CGWindowListCreateImage expects a rectangle in screen coordinates
* with the origin in the upper left (contrary to Cocoa). The origin is
* on the screen showing the menu bar (this is the screen at index 0 in the
* screens array). So, after converting the rectangle to Cocoa screen
* coordinates, we use the height of this particular screen to translate
* to the coordinate space expected by CGWindowListCreateImage.
*/
point = event.locationInWindow;
NSRect rect = NSMakeRect (point.x, point.y,
1, 1);
rect = [self.window convertRectToScreen:rect];
rect.origin.y = [[[NSScreen screens] objectAtIndex:0] frame].size.height - rect.origin.y;
root_image_ref = CGWindowListCreateImage (rect,
kCGWindowListOptionOnScreenOnly,
kCGNullWindowID,
kCGWindowImageDefault);
pixel_data = CGDataProviderCopyData (CGImageGetDataProvider (root_image_ref));
data = CFDataGetBytePtr (pixel_data);
color_space = CGImageGetColorSpace (root_image_ref);
if (color_space)
{
CFDataRef icc_data = NULL;
icc_data = CGColorSpaceCopyICCProfile (color_space);
if (icc_data)
{
UInt8 *buffer = g_malloc (CFDataGetLength (icc_data));
CFDataGetBytes (icc_data, CFRangeMake (0, CFDataGetLength (icc_data)),
buffer);
profile = pika_color_profile_new_from_icc_profile (buffer,
CFDataGetLength (icc_data),
NULL);
g_free (buffer);
CFRelease (icc_data);
}
}
pika_rgba_set_uchar (&rgb, data[2], data[1], data[0], 255);
if (profile)
{
PikaColorProfile *srgb_profile;
PikaColorTransform *transform;
const Babl *format;
PikaColorTransformFlags flags = 0;
format = babl_format ("R'G'B'A double");
flags |= PIKA_COLOR_TRANSFORM_FLAGS_NOOPTIMIZE;
flags |= PIKA_COLOR_TRANSFORM_FLAGS_BLACK_POINT_COMPENSATION;
srgb_profile = pika_color_profile_new_rgb_srgb ();
transform = pika_color_transform_new (profile, format,
srgb_profile, format,
PIKA_COLOR_RENDERING_INTENT_PERCEPTUAL,
flags);
if (transform)
{
pika_color_transform_process_pixels (transform,
format, &rgb,
format, &rgb,
1);
pika_rgb_clamp (&rgb);
g_object_unref (transform);
}
g_object_unref (srgb_profile);
g_object_unref (profile);
}
CGImageRelease (root_image_ref);
CFRelease (pixel_data);
g_signal_emit_by_name (button, "color-picked", &rgb);
}
@end
@interface PikaPickWindow : NSWindow <NSWindowDelegate>
{
PikaPickWindowController *controller;
}
- (id)initWithButton:(PikaPickButton *)button forScreen:(NSScreen *)screen withController:(PikaPickWindowController *)_controller;
@end
@implementation PikaPickWindow
- (id)initWithButton:(PikaPickButton *)button forScreen:(NSScreen *)screen withController:(PikaPickWindowController *)_controller
{
self = [super initWithContentRect:screen.frame
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
if (self)
{
PikaPickView *view;
controller = _controller;
[self setDelegate:self];
[self setAlphaValue:0.0];
#if 0
/* Useful for debugging purposes */
[self setBackgroundColor:[NSColor redColor]];
[self setAlphaValue:0.2];
#endif
[self setIgnoresMouseEvents:NO];
[self setAcceptsMouseMovedEvents:YES];
[self setHasShadow:NO];
[self setOpaque:NO];
/* Set the highest level, so on top of everything */
[self setLevel:NSScreenSaverWindowLevel];
view = [[PikaPickView alloc] initWithButton:button controller:controller];
[self setContentView:view];
[self makeFirstResponder:view];
[view release];
[self disableCursorRects];
}
return self;
}
/* Borderless windows cannot become key/main by default, so we force it
* to make it so. We need this to receive events.
*/
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
- (void)windowDidBecomeKey:(NSNotification *)aNotification
{
/* We cannot use the usual Cocoa method for handling cursor updates,
* since the GDK Quartz backend is interfering. Additionally, because
* one of the screen-spanning windows pops up under the mouse pointer this
* view will not receive a MouseEntered event. So, we synthesize such
* an event here and the view can set the mouse pointer in response to
* this. So, this event only has to be synthesized once and only for
* the window that pops up under the mouse cursor. Synthesizing multiple
* times messes up the mouse cursor stack.
*
* We cannot set the mouse pointer at this moment, because the GDK window
* will still receive an MouseExited event in which turn it will modify
* the cursor. So, with this synthesized event we also ensure we set
* the mouse cursor *after* the GDK window has manipulated the cursor.
*/
NSEvent *event;
if (!controller.firstBecameKey ||
!NSPointInRect ([NSEvent mouseLocation], self.frame))
return;
controller.firstBecameKey = NO;
event = [NSEvent enterExitEventWithType:NSMouseEntered
location:[self mouseLocationOutsideOfEventStream]
modifierFlags:0
timestamp:[[NSApp currentEvent] timestamp]
windowNumber:self.windowNumber
context:nil
eventNumber:0
trackingNumber:(NSInteger)[[self contentView] area]
userData:nil];
[NSApp postEvent:event atStart:NO];
}
@end
/* To properly handle multi-monitor setups we need to create a
* PikaPickWindow for each monitor (NSScreen). This is necessary because
* a window on Mac OS X (tested on 10.9) cannot span more than one
* monitor, so any approach that attempts to create one large window
* spanning all monitors cannot work. So, we have to create multiple
* windows in case of multi-monitor setups and these different windows
* are managed by PikaPickWindowController.
*/
@implementation PikaPickWindowController
@synthesize firstBecameKey;
@synthesize cursor;
- (id)initWithButton:(PikaPickButton *)_button;
{
self = [super init];
if (self)
{
firstBecameKey = YES;
button = _button;
cursor = [PikaPickWindowController makePickCursor];
windows = [[NSMutableArray alloc] init];
for (NSScreen *screen in [NSScreen screens])
{
PikaPickWindow *window;
window = [[PikaPickWindow alloc] initWithButton:button
forScreen:screen
withController:self];
[window orderFrontRegardless];
[window makeMainWindow];
[windows addObject:window];
}
[self updateKeyWindow];
}
return self;
}
- (void)updateKeyWindow
{
for (PikaPickWindow *window in windows)
{
if (NSPointInRect ([NSEvent mouseLocation], window.frame))
[window makeKeyWindow];
}
}
- (void)shutdown
{
GtkWidget *window;
for (PikaPickWindow *window in windows)
[window close];
[windows release];
if (cursor)
[cursor release];
/* Give focus back to the window containing the pick button */
window = gtk_widget_get_toplevel (GTK_WIDGET (button));
gtk_window_present_with_time (GTK_WINDOW (window), GDK_CURRENT_TIME);
[self release];
}
+ (NSCursor *)makePickCursor
{
GBytes *bytes = NULL;
GError *error = NULL;
bytes = g_resources_lookup_data ("/technology.heckin/color-picker-cursors-raw/cursor-color-picker.png",
G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
if (bytes)
{
NSData *data = [NSData dataWithBytes:g_bytes_get_data (bytes, NULL)
length:g_bytes_get_size (bytes)];
NSImage *image = [[NSImage alloc] initWithData:data];
NSCursor *cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(1, 30)];
[image release];
g_bytes_unref (bytes);
return [cursor retain];
}
else
{
g_critical ("Failed to create cursor image: %s", error->message);
g_clear_error (&error);
}
return NULL;
}
@end
/* entry point to this file, called from pikapickbutton.c */
void
_pika_pick_button_quartz_pick (PikaPickButton *button)
{
PikaPickWindowController *controller;
NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init];
controller = [[PikaPickWindowController alloc] initWithButton:button];
[pool release];
}