Skip to content

Commit e95d4f9

Browse files
committed
Implement screen color picker for newer Mac versions
1 parent f57d55c commit e95d4f9

5 files changed

Lines changed: 142 additions & 33 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ For personal usage, you can use pip and homebrew to install almost all of Aegisu
5353
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"
5454

5555
When compiling on Apple Silicon, replace `/usr/local` with `/opt/homebrew`.
56-
When compiling on macOS 15.0 (Sequoia) or later, you may also want to `export MACOS_X_DEPLOYMENT_TARGET=14.0` to make the color picker work.
5756

5857
Once the dependencies are installed, build Aegisub with `meson build && meson compile -C build`.
5958

meson.build

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,8 @@ if host_machine.system() == 'windows' and cc.has_header('dwrite_3.h')
303303
endif
304304

305305
if host_machine.system() == 'darwin'
306-
frameworks_dep = dependency('appleframeworks', modules : ['CoreText', 'CoreFoundation', 'AppKit', 'Carbon', 'IOKit', 'QuartzCore'])
307-
deps += frameworks_dep
306+
deps += dependency('appleframeworks', modules : ['CoreText', 'CoreFoundation', 'AppKit', 'Carbon', 'IOKit', 'QuartzCore'])
307+
deps += dependency('appleframeworks', modules : ['ScreenCaptureKit'], required : false)
308308
endif
309309

310310
# TODO: OSS

src/dialog_colorpicker.cpp

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
#include <wx/textctrl.h>
6767

6868
#ifdef __WXMAC__
69-
#include <ApplicationServices/ApplicationServices.h>
69+
namespace osx { void DropFromScreen(int x, int y, int resx, int resy, int magnification, wxMemoryDC &capdc); }
7070
#endif
7171

7272
namespace {
@@ -387,42 +387,17 @@ class ColorPickerScreenDropper final : public wxControl {
387387
void DropFromScreenXY(int x, int y);
388388
};
389389

390-
#ifndef MAC_OS_VERSION_15_0
391-
#define MAC_OS_VERSION_15_0 150000
392-
#endif
393-
394390
void ColorPickerScreenDropper::DropFromScreenXY(int x, int y) {
395391
wxMemoryDC capdc(capture);
396392
capdc.SetPen(*wxTRANSPARENT_PEN);
397-
#if !(defined(__WXMAC__) && (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_15_0))
393+
394+
#ifndef __WXMAC__
398395
wxScreenDC screen;
399396
capdc.StretchBlit(0, 0, resx * magnification, resy * magnification,
400397
&screen, x - resx / 2, y - resy / 2, resx, resy);
401398
#else
402-
// wxScreenDC doesn't work on recent versions of OS X so do it manually
403-
404-
// Doesn't bother handling the case where the rect overlaps two monitors
405-
CGDirectDisplayID display_id;
406-
uint32_t display_count;
407-
CGGetDisplaysWithPoint(CGPointMake(x, y), 1, &display_id, &display_count);
408-
409-
agi::scoped_holder<CGImageRef> img(CGDisplayCreateImageForRect(display_id, CGRectMake(x - resx / 2, y - resy / 2, resx, resy)), CGImageRelease);
410-
size_t width = CGImageGetWidth(img);
411-
size_t height = CGImageGetHeight(img);
412-
std::vector<uint8_t> imgdata(height * width * 4);
413-
414-
agi::scoped_holder<CGColorSpaceRef> colorspace(CGColorSpaceCreateDeviceRGB(), CGColorSpaceRelease);
415-
agi::scoped_holder<CGContextRef> bmp_context(CGBitmapContextCreate(&imgdata[0], width, height, 8, 4 * width, colorspace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big), CGContextRelease);
416-
417-
CGContextDrawImage(bmp_context, CGRectMake(0, 0, width, height), img);
418-
419-
for (int x = 0; x < resx; x++) {
420-
for (int y = 0; y < resy; y++) {
421-
uint8_t *pixel = &imgdata[y * width * 4 + x * 4];
422-
capdc.SetBrush(wxBrush(wxColour(pixel[0], pixel[1], pixel[2])));
423-
capdc.DrawRectangle(x * magnification, y * magnification, magnification, magnification);
424-
}
425-
}
399+
// wxScreenDC doesn't work on OS X so do it manually
400+
osx::DropFromScreen(x, y, resx, resy, magnification, capdc);
426401
#endif
427402

428403
Refresh(false);

src/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ if host_machine.system() == 'darwin'
164164
aegisub_src += files(
165165
'font_file_lister_coretext.mm',
166166
'osx/osx_utils.mm',
167+
'osx/screenpicker.mm',
167168
)
168169

169170
if conf.get('WITH_INTERNAL_WXWIDGETS', 0) != 0

src/osx/screenpicker.mm

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) 2026, arch1t3cht <arch1t3cht@gmail.com>
2+
// Copyright (c) 2012 Thomas Goyne, <plorkyeran@aegisub.org>
3+
//
4+
// Permission to use, copy, modify, and distribute this software for any
5+
// purpose with or without fee is hereby granted, provided that the above
6+
// copyright notice and this permission notice appear in all copies.
7+
//
8+
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9+
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10+
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11+
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12+
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13+
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14+
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15+
//
16+
// Aegisub Project https://aegisub.org/
17+
18+
#include <libaegisub/scoped_ptr.h>
19+
20+
#include <wx/dcmemory.h>
21+
#include <wx/thread.h>
22+
23+
#import <ApplicationServices/ApplicationServices.h>
24+
#import <CoreGraphics/CGDirectDisplay.h>
25+
#import <ScreenCaptureKit/ScreenCaptureKit.h>
26+
27+
namespace osx {
28+
29+
namespace {
30+
31+
void CopyToDC(CGImageRef img, wxMemoryDC &capdc, int resx, int resy, int magnification) {
32+
size_t width = CGImageGetWidth(img);
33+
size_t height = CGImageGetHeight(img);
34+
std::vector<uint8_t> imgdata(height * width * 4);
35+
36+
agi::scoped_holder<CGColorSpaceRef> colorspace(CGColorSpaceCreateDeviceRGB(), CGColorSpaceRelease);
37+
agi::scoped_holder<CGContextRef> bmp_context(CGBitmapContextCreate(&imgdata[0], width, height, 8, 4 * width, colorspace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big), CGContextRelease);
38+
39+
CGContextDrawImage(bmp_context, CGRectMake(0, 0, width, height), img);
40+
41+
for (int x = 0; x < resx; x++) {
42+
for (int y = 0; y < resy; y++) {
43+
uint8_t *pixel = &imgdata[y * width * 4 + x * 4];
44+
capdc.SetBrush(wxBrush(wxColour(pixel[0], pixel[1], pixel[2])));
45+
capdc.DrawRectangle(x * magnification, y * magnification, magnification, magnification);
46+
}
47+
}
48+
}
49+
50+
}
51+
52+
void DropFromScreen(int x, int y, int resx, int resy, int magnification, wxMemoryDC &capdc) {
53+
CGRect rect = CGRectMake(x - resx / 2., y - resy / 2., resx, resy);
54+
55+
#if MAC_OS_X_VERSION_MIN_REQUIRED < 150000
56+
// Doesn't bother handling the case where the rect overlaps two monitors
57+
CGDirectDisplayID display_id;
58+
uint32_t display_count;
59+
CGGetDisplaysWithPoint(CGPointMake(x, y), 1, &display_id, &display_count);
60+
61+
agi::scoped_holder<CGImageRef> img(CGDisplayCreateImageForRect(display_id, rect), CGImageRelease);
62+
CopyToDC(img, capdc, resx, resy, magnification);
63+
#else
64+
wxSemaphore screenshot_notify{0, 1};
65+
wxSemaphore &screenshot_notify_ref = screenshot_notify;
66+
67+
#if MAC_OS_X_VERSION_MIN_REQUIRED < 152000
68+
// captureImageInRect is only available from 15.2 onward so before that we need to do it the cumbersome (and slow) way.
69+
70+
rect.origin.x = std::roundl(rect.origin.x);
71+
rect.origin.y = std::roundl(rect.origin.y);
72+
73+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable shareableContent, NSError * _Nullable error) {
74+
if (error) {
75+
screenshot_notify_ref.Post();
76+
return;
77+
}
78+
79+
CGDirectDisplayID display_id;
80+
uint32_t display_count;
81+
CGGetDisplaysWithPoint(CGPointMake(x, y), 1, &display_id, &display_count);
82+
83+
CGDisplayModeRef displaymode = CGDisplayCopyDisplayMode(display_id);
84+
int scale_factor = CGDisplayModeGetPixelWidth(displaymode) / CGDisplayModeGetWidth(displaymode);
85+
86+
SCDisplay* point_display = nullptr;
87+
88+
for (SCDisplay *display in shareableContent.displays) {
89+
if (display.displayID == display_id) {
90+
point_display = display;
91+
}
92+
}
93+
94+
if (!point_display) {
95+
screenshot_notify_ref.Post();
96+
return;
97+
}
98+
99+
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:point_display excludingWindows:@[]];
100+
SCStreamConfiguration *configuration = [[SCStreamConfiguration alloc] init];
101+
configuration.capturesAudio = NO;
102+
configuration.showsCursor = NO;
103+
configuration.captureDynamicRange = SCCaptureDynamicRangeSDR;
104+
configuration.sourceRect = rect;
105+
configuration.width = resx * scale_factor;
106+
configuration.height = resy * scale_factor;
107+
108+
[SCScreenshotManager captureImageWithFilter:filter configuration:configuration completionHandler:^(CGImageRef _Nullable sampleBuffer, NSError * _Nullable error) {
109+
if (error) {
110+
screenshot_notify_ref.Post();
111+
return;
112+
}
113+
114+
CopyToDC(sampleBuffer, capdc, resx, resy, magnification);
115+
screenshot_notify_ref.Post();
116+
}];
117+
}];
118+
119+
#else
120+
[SCScreenshotManager captureImageInRect:rect completionHandler:^(CGImageRef _Nullable image, NSError * _Nullable error){
121+
if (!error)
122+
CopyToDC(image, capdc, resx, resy, magnification);
123+
124+
screenshot_notify_ref.Post();
125+
}];
126+
#endif
127+
128+
screenshot_notify.Wait();
129+
return;
130+
#endif
131+
132+
}
133+
134+
}

0 commit comments

Comments
 (0)