Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parameter for appearance, some parameters optional, and TypeScript types #2

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ app.on('ready', () => {

const nativePopover = new ElectronMacPopover(popoverWindow.getNativeWindowHandle());

ipcMain.on('open-popover', (e, rect, size, edge, behavior, animate) => {
const options = { rect, size, edge, behavior, animate };
ipcMain.on('open-popover', (e, rect, size, edge, behavior, animate, appearance) => {
const options = { rect, size, edge, behavior, animate, appearance };

nativePopover.show(win.getNativeWindowHandle(), options);
});
Expand All @@ -66,8 +66,28 @@ Opens the NSPopover.

- `positioningWindowHandle`: the native window handle of a BrowserWindow that
the popover will be positioned relative to.
- `options`: can be found in the [official docs](https://developer.apple.com/documentation/appkit/nspopover).
- `options`: (passed through to [NSPopover](https://developer.apple.com/documentation/appkit/nspopover))
- `rect` { x: number, y: number, width: number, height: number }
- `size` { width: number, height: number }
- `edge` 'max-x-edge' | 'max-y-edge' | 'min-x-edge' | 'min-y-edge' (optional, default: 'max-x-edge')
- `behavior` 'transient' | 'semi-transient' | 'application-defined' (optional, default: 'application-defined')
- `animate` boolean (optional, default: false)
- `appearance` 'aqua' | 'darkAqua' | 'vibrantLight' | 'accessibilityHighContrastAqua' | 'accessibilityHighContrastDarkAqua' | 'accessibilityHighContrastVibrantLight' | 'accessibilityHighContrastVibrantDark' (optional, default: 'aqua')

### popover.setSize(size)

Changes the size of the NSPopover.

- `size` { width: number, height: number }
- `animate`: boolean (optional, default: false)
- `duration`: number in seconds (optional, default 0.3)

### popover.close()

Closes the NSPopover.

### popover.onClosed(callback)

Callback is called when the popover closes.

- `callback`: Function
3 changes: 3 additions & 0 deletions electron_mac_popover.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class ElectronMacPopover : public Napi::ObjectWrap<ElectronMacPopover> {
void Close(const Napi::CallbackInfo& info);

void PopoverWindowClosed();
void SetupClosedCallback(const Napi::CallbackInfo &info);

void SetSize(const Napi::CallbackInfo& info);

NSPopover* popover_;
NSView* content_;
Expand Down
188 changes: 174 additions & 14 deletions electron_mac_popover.mm
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <QuartzCore/QuartzCore.h>

#include "electron_mac_popover.h"

static IMP g_originalAllowsVibrancy;

static char kDisallowVibrancyKey;

Napi::ThreadSafeFunction tsfnClosed;
void (*callbackClosed)(Napi::Env env, Napi::Function jsCallback);

BOOL swizzledAllowsVibrancy(id obj, SEL sel) {
NSNumber* disallowVibrancy = objc_getAssociatedObject(obj,
&kDisallowVibrancyKey);
Expand All @@ -23,6 +27,8 @@ BOOL swizzledAllowsVibrancy(id obj, SEL sel) {
Napi::Function func = DefineClass(env, "ElectronMacPopover", {
InstanceMethod("show", &ElectronMacPopover::Show),
InstanceMethod("close", &ElectronMacPopover::Close),
InstanceMethod("onClosed", &ElectronMacPopover::SetupClosedCallback),
InstanceMethod("setSize", &ElectronMacPopover::SetSize),
});

constructor = Napi::Persistent(func);
Expand All @@ -49,6 +55,13 @@ BOOL swizzledAllowsVibrancy(id obj, SEL sel) {
}

content_ = *reinterpret_cast<NSView**>(info[0].As<Napi::Buffer<void*>>().Data());
if (!content_) {
Napi::TypeError::New(env, "Invalid native window handle")
.ThrowAsJavaScriptException();
return;
}

popover_ = nullptr;
}

void ElectronMacPopover::Show(const Napi::CallbackInfo& info) {
Expand All @@ -73,27 +86,62 @@ BOOL swizzledAllowsVibrancy(id obj, SEL sel) {

NSView* positioning_content_view = *reinterpret_cast<NSView**>(
info[0].As<Napi::Buffer<void*>>().Data());
if (!positioning_content_view) {
Napi::TypeError::New(env, "Invalid positioning content view")
.ThrowAsJavaScriptException();
return;
}

Napi::Object options = info[1].As<Napi::Object>();
if (options.IsEmpty()) {
Napi::TypeError::New(env, "Options object expected")
.ThrowAsJavaScriptException();
return;
}

Napi::Object rect = options.Get("rect").As<Napi::Object>();
if (rect.IsEmpty()) {
Napi::TypeError::New(env, "'rect' option is required")
.ThrowAsJavaScriptException();
return;
}
NSRect positioning_rect =
NSMakeRect(rect.Get("x").As<Napi::Number>().DoubleValue(),
rect.Get("y").As<Napi::Number>().DoubleValue(),
rect.Get("width").As<Napi::Number>().DoubleValue(),
rect.Get("height").As<Napi::Number>().DoubleValue());

Napi::Object size_obj = options.Get("size").As<Napi::Object>();
if (size_obj.IsEmpty()) {
Napi::TypeError::New(env, "'size' option is required")
.ThrowAsJavaScriptException();
return;
}
NSSize size = NSMakeSize(size_obj.Get("width").As<Napi::Number>().DoubleValue(),
size_obj.Get("height").As<Napi::Number>().DoubleValue());

std::string behavior = options.Get("behavior").As<Napi::String>().Utf8Value();
std::string preferred_edge = options.Get("edge").As<Napi::String>().Utf8Value();
BOOL animate = options.Get("animate").As<Napi::Boolean>().Value();
std::string behavior = "application-defined";
if (options.Has("behavior")) {
behavior = options.Get("behavior").As<Napi::String>().Utf8Value();
}
std::string preferred_edge = "max-x-edge";
if (options.Has("edge")) {
preferred_edge = options.Get("edge").As<Napi::String>().Utf8Value();
}
BOOL animate = false;
if (options.Has("animate")) {
animate = options.Get("animate").As<Napi::Boolean>().Value();
}
std::string appearance = "aqua";
if (options.Has("appearance")) {
appearance = options.Get("appearance").As<Napi::String>().Utf8Value();
}

NSPopoverBehavior popover_behavior = NSPopoverBehaviorApplicationDefined;
if (behavior == "transient") {
popover_behavior = NSPopoverBehaviorTransient;
} else if (behavior == "semi-transient") {
popover_behavior = NSPopoverBehaviorSemitransient;
}

NSRectEdge popover_edge = NSMaxXEdge;
Expand All @@ -105,6 +153,24 @@ BOOL swizzledAllowsVibrancy(id obj, SEL sel) {
popover_edge = NSMinYEdge;
}

NSAppearanceName popover_appearance = NSAppearanceNameAqua;
if (appearance == "vibrantLight") {
popover_appearance = NSAppearanceNameVibrantLight;
}
if (@available(macOS 10.14, *)) {
if (appearance == "darkAqua") {
popover_appearance = NSAppearanceNameDarkAqua;
} else if (appearance == "accessibilityHighContrastAqua") {
popover_appearance = NSAppearanceNameAccessibilityHighContrastAqua;
} else if (appearance == "accessibilityHighContrastDarkAqua") {
popover_appearance = NSAppearanceNameAccessibilityHighContrastDarkAqua;
} else if (appearance == "accessibilityHighContrastVibrantLight") {
popover_appearance = NSAppearanceNameAccessibilityHighContrastVibrantLight;
} else if (appearance == "accessibilityHighContrastVibrantDark") {
popover_appearance = NSAppearanceNameAccessibilityHighContrastVibrantDark;
}
}

if (!popover_) {
NSViewController* view_controller =
[[[NSViewController alloc] init] autorelease];
Expand All @@ -113,7 +179,19 @@ BOOL swizzledAllowsVibrancy(id obj, SEL sel) {
[popover setContentViewController:view_controller];

[content_ setWantsLayer:YES];
NSView *view = content_.subviews.lastObject.subviews.lastObject;
NSView *view = content_;
if (content_.subviews.lastObject) {
view = content_.subviews.lastObject;
if (view.subviews.lastObject) {
view = view.subviews.lastObject;
}
}

if (!view) {
Napi::Error::New(env, "Missing content view")
.ThrowAsJavaScriptException();
return;
}

objc_setAssociatedObject(view,
&kDisallowVibrancyKey,
Expand All @@ -124,6 +202,8 @@ BOOL swizzledAllowsVibrancy(id obj, SEL sel) {

[popover setContentSize:size];

[popover setAppearance:[NSAppearance appearanceNamed:popover_appearance]];

id observer = [[NSNotificationCenter defaultCenter]
addObserverForName:NSPopoverDidCloseNotification
object:popover
Expand Down Expand Up @@ -153,24 +233,104 @@ BOOL swizzledAllowsVibrancy(id obj, SEL sel) {
}

void ElectronMacPopover::PopoverWindowClosed() {
[[NSNotificationCenter defaultCenter]
removeObserver:popover_closed_observer_];
if (popover_closed_observer_) {
[[NSNotificationCenter defaultCenter]
removeObserver:popover_closed_observer_];
popover_closed_observer_ = nullptr;
}
if (content_ && popover_) {
if (content_.subviews.lastObject && popover_.contentViewController.view) {
[content_.subviews.lastObject addSubview:popover_.contentViewController.view];
}
}
if (tsfnClosed != NULL && callbackClosed != NULL) {
tsfnClosed.BlockingCall(callbackClosed);
}
popover_ = nullptr;
}

// Add back view to BrowserWindow.
[content_.subviews.lastObject addSubview:popover_.contentViewController.view];
void ElectronMacPopover::SetupClosedCallback(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
if (info.Length() > 0 && info[0].IsFunction()) {
tsfnClosed = Napi::ThreadSafeFunction::New(env, info[0].As<Napi::Function>(), "Closed", 0, 1);
callbackClosed = [](Napi::Env env, Napi::Function jsCallback) { jsCallback.Call({}); };
} else {
tsfnClosed = NULL;
callbackClosed = NULL;
}
}

popover_closed_observer_ = nullptr;
popover_ = nullptr;
void ElectronMacPopover::SetSize(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() < 1 || !info[0].IsObject()) {
Napi::TypeError::New(env, "Object expected").ThrowAsJavaScriptException();
return;
}

Napi::Object options = info[0].As<Napi::Object>();

if (!options.Has("size")) {
Napi::TypeError::New(env, "'size' option is required").ThrowAsJavaScriptException();
return;
}

Napi::Value size_val = options.Get("size");
if (!size_val.IsObject()) {
Napi::TypeError::New(env, "'size' must be an object").ThrowAsJavaScriptException();
return;
}

Napi::Object size_obj = size_val.As<Napi::Object>();
if (!size_obj.Has("width") || !size_obj.Has("height")) {
Napi::TypeError::New(env, "'size' must have width and height").ThrowAsJavaScriptException();
return;
}

Napi::Value width = size_obj.Get("width");
Napi::Value height = size_obj.Get("height");

if (!width.IsNumber() || !height.IsNumber()) {
Napi::TypeError::New(env, "width and height must be numbers").ThrowAsJavaScriptException();
return;
}

NSSize size = NSMakeSize(width.As<Napi::Number>().DoubleValue(),
height.As<Napi::Number>().DoubleValue());

BOOL animate = false;
if (options.Has("animate")) {
animate = options.Get("animate").As<Napi::Boolean>().Value();
}

double duration = 0.3;
if (options.Has("duration")) {
duration = options.Get("duration").As<Napi::Number>().DoubleValue();
}

if (popover_) {
if (animate) {
NSAnimationContext *currentContext = [NSAnimationContext currentContext];
currentContext.allowsImplicitAnimation = YES;
currentContext.duration = duration;
currentContext.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[NSAnimationContext beginGrouping];
[popover_ setContentSize:size];
[NSAnimationContext endGrouping];
} else {
[popover_ setContentSize:size];
}
}
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
auto allowsVibrancyMethod = class_getInstanceMethod(
NSClassFromString(@"WebContentsViewCocoa"),
NSSelectorFromString(@"allowsVibrancy"));

g_originalAllowsVibrancy = method_setImplementation(allowsVibrancyMethod,
(IMP)&swizzledAllowsVibrancy);

if (allowsVibrancyMethod) {
g_originalAllowsVibrancy = method_setImplementation(allowsVibrancyMethod,
(IMP)&swizzledAllowsVibrancy);
}
return ElectronMacPopover::Init(env, exports);
}

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "electron-mac-popover",
"version": "1.0.0",
"version": "1.3.0",
"description": "A native module that can display webContents in an NSPopover.",
"main": "index.js",
"scripts": {
Expand Down Expand Up @@ -35,5 +35,6 @@
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^3.0.2"
}
},
"types": "types.d.ts"
}
Loading