464 lines
13 KiB
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];
|
||
|
}
|