Skip to content

Commit

Permalink
Add menu item and keyCommand support to Web Extension commands.
Browse files Browse the repository at this point in the history
https://webkit.org/b/265772
rdar://problem/119111895

Reviewed by Brian Weinstein.

Adds a menuItem and keyCommand property to _WKWebExtensionCommand for macOS and iOS.
Also adds performCommandForEvent: and commandForEvent: to _WKWebExtensionContext for macOS.
Adds a private _shortcut to _WKWebExtensionCommand for use by Safari.

* Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionCommand.h:
* Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionCommand.mm:
(-[_WKWebExtensionCommand menuItem]): Added.
(-[_WKWebExtensionCommand keyCommand]): Added.
(-[_WKWebExtensionCommand _shortcut]): Added.
(-[_WKWebExtensionCommand _matchesEvent:]): Added.
* Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionCommandPrivate.h:
* Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionContext.h:
* Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionContext.mm:
(-[_WKWebExtensionContext performCommandForEvent:]): Added.
(-[_WKWebExtensionContext commandForEvent:]): Added.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCommandCocoa.mm:
(+[_WKWebExtensionKeyCommand commandWithTitle:image:input:modifierFlags:handler:]): Added.
(-[_WKWebExtensionKeyCommand copyWithZone:]):
(-[_WKWebExtensionKeyCommand _resolvedTargetFromFirstTarget:]):
(-[_WKWebExtensionKeyCommand _performWebExtensionKeyCommand:]):
(WebKit::WebExtensionCommand::platformMenuItem const): Added.
(WebKit::WebExtensionCommand::keyCommand const): Added.
(WebKit::WebExtensionCommand::matchesEvent const): Added.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionContextCocoa.mm:
(WebKit::WebExtensionContext::performCommand): Added.
(WebKit::WebExtensionContext::command): Added.
(WebKit::WebExtensionContext::performMenuItem): Added.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionMenuItemCocoa.mm:
(-[_WKWebExtensionMenuItem initWithTitle:handler:]):
(-[_WKWebExtensionMenuItem copyWithZone:]): Added.
(-[_WKWebExtensionMenuItem _performAction:]):
* Source/WebKit/UIProcess/Extensions/WebExtensionCommand.h:
* Source/WebKit/UIProcess/Extensions/WebExtensionContext.h:
* Source/WebKit/UIProcess/Extensions/WebExtensionMenuItem.h:
* Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPICommands.mm:
(TestWebKitAPI::TEST):

Canonical link: https://commits.webkit.org/271532@main
  • Loading branch information
xeenon committed Dec 5, 2023
1 parent 46fcc22 commit 2a0077a
Show file tree
Hide file tree
Showing 12 changed files with 537 additions and 32 deletions.
26 changes: 24 additions & 2 deletions Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
#import <Foundation/Foundation.h>

#if TARGET_OS_IPHONE
#import <UIKit/UICommand.h>
#import <UIKit/UIKeyCommand.h>
#endif

@class _WKWebExtensionContext;
Expand All @@ -51,7 +51,7 @@ NS_SWIFT_NAME(WKWebExtension.Command)
/*! @abstract The web extension context associated with the command. */
@property (nonatomic, readonly, weak) _WKWebExtensionContext *webExtensionContext;

/*! @abstract Unique identifier for the command. */
/*! @abstract A unique identifier for the command. */
@property (nonatomic, readonly, copy) NSString *identifier;

/*!
Expand Down Expand Up @@ -80,6 +80,28 @@ NS_SWIFT_NAME(WKWebExtension.Command)
@property (nonatomic) NSEventModifierFlags modifierFlags;
#endif

/*!
@abstract A menu item representation of the web extension command for use in menus.
@discussion This property provides a representation of the web extension command as a menu item to display in the app.
Selecting the menu item will perform the command, offering a convenient and visual way for users to execute this web extension command.
*/
#if TARGET_OS_IPHONE
@property (nonatomic, readonly, copy) UIMenuElement *menuItem;
#else
@property (nonatomic, readonly, copy) NSMenuItem *menuItem;
#endif

#if TARGET_OS_IPHONE
/*!
@abstract A key command representation of the web extension command for use in the responder chain.
@discussion This property provides a `UIKeyCommand` instance representing the web extension command, ready for integration in the app.
The property is `nil` if no shortcut is defined. Otherwise, the key command is fully configured with the necessary input key and modifier flags
to perform the associated command upon activation. It can be included in a view controller or other responder's `keyCommands` property, enabling
keyboard activation and discoverability of the web extension command.
*/
@property (nonatomic, readonly, copy, nullable) UIKeyCommand *keyCommand;
#endif // TARGET_OS_IPHONE

@end

NS_ASSUME_NONNULL_END
52 changes: 52 additions & 0 deletions Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionCommand.mm
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@

#if USE(APPKIT)
using CocoaModifierFlags = NSEventModifierFlags;
using CocoaMenuItem = NSMenuItem;
#else
using CocoaModifierFlags = UIKeyModifierFlags;
using CocoaMenuItem = UIMenuElement;
#endif

@implementation _WKWebExtensionCommand
Expand Down Expand Up @@ -122,6 +124,32 @@ - (void)setModifierFlags:(CocoaModifierFlags)modifierFlags
_webExtensionCommand->setModifierFlags(optionSet);
}

- (CocoaMenuItem *)menuItem
{
return _webExtensionCommand->platformMenuItem();
}

#if PLATFORM(IOS_FAMILY)
- (UIKeyCommand *)keyCommand
{
return _webExtensionCommand->keyCommand();
}
#endif

- (NSString *)_shortcut
{
return _webExtensionCommand->shortcutString();
}

#if USE(APPKIT)
- (BOOL)_matchesEvent:(NSEvent *)event
{
NSParameterAssert([event isKindOfClass:NSEvent.class]);

return _webExtensionCommand->matchesEvent(event);
}
#endif

#pragma mark WKObject protocol implementation

- (API::Object&)_apiObject
Expand Down Expand Up @@ -169,6 +197,30 @@ - (void)setModifierFlags:(CocoaModifierFlags)modifierFlags
{
}

- (CocoaMenuItem *)menuItem
{
return nil;
}

#if PLATFORM(IOS_FAMILY)
- (UIKeyCommand *)keyCommand
{
return nil;
}
#endif

- (NSString *)_shortcut
{
return nil;
}

#if USE(APPKIT)
- (BOOL)_matchesEvent:(NSEvent *)event
{
return NO;
}
#endif

#endif // ENABLE(WK_WEB_EXTENSIONS)

@end
18 changes: 18 additions & 0 deletions Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionCommandPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,22 @@

@interface _WKWebExtensionCommand ()

/*!
@abstract Represents the shortcut for the web extension, formatted according to web extension specification.
@discussion This property provides a string representation of the shortcut, incorporating any customizations made to the `activationKey`
and `modifierFlags` properties. It will be empty if no shortcut is defined for the command.
*/
@property (nonatomic, readonly, copy) NSString *_shortcut;

#if TARGET_OS_OSX
/*!
@abstract Determines whether an event matches the command's activation key and modifier flags.
@discussion This method can be used to check if a given keyboard event corresponds to the command's activation key and modifiers, if any.
The app can use this during event handling in the app, without showing the command in a menu.
@param event The event to be checked against the command's activation key and modifiers.
@result A Boolean value indicating whether the event matches the command's shortcut.
*/
- (BOOL)_matchesEvent:(NSEvent *)event;
#endif // TARGET_OS_OSX

@end
22 changes: 21 additions & 1 deletion Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ WK_CLASS_AVAILABLE(macos(13.3), ios(16.4))
@property (nonatomic, copy) NSURL *baseURL;

/*!
@abstract An unique identifier used to distinguish the extension from other extensions and target it for messages.
@abstract A unique identifier used to distinguish the extension from other extensions and target it for messages.
@discussion The default value is a unique value that matches the host in the default base URL. The identifier can be any
value that is unique. Setting is only allowed when the context is not loaded. This value is accessible by the extension via
`browser.runtime.id` and is used for messaging the extension via `browser.runtime.sendMessage()`.
Expand Down Expand Up @@ -531,6 +531,26 @@ WK_CLASS_AVAILABLE(macos(13.3), ios(16.4))
*/
- (void)performCommand:(_WKWebExtensionCommand *)command;

#if TARGET_OS_OSX
/*!
@abstract Performs the command associated with the given event.
@discussion This method checks for a command corresponding to the provided event and performs it, if available. The app should use this method to perform
any extension commands at an appropriate time in the app's event handling, like in `sendEvent:` of `NSApplication` or `NSWindow` subclasses.
@param event The event representing the user input.
@result Returns `YES` if a command corresponding to the event was found and performed, `NO` otherwise.
*/
- (BOOL)performCommandForEvent:(NSEvent *)event;

/*!
@abstract Retrieves the command associated with the given event without performing it.
@discussion This method returns the command that corresponds to the provided event, if such a command exists. This provides a way to programmatically
determine what action would occur for a given event, without triggering the command.
@param event The event for which to retrieve the corresponding command.
@result The command associated with the event, or `nil` if there is no such command.
*/
- (nullable _WKWebExtensionCommand *)commandForEvent:(NSEvent *)event;
#endif // TARGET_OS_OSX

/*!
@abstract Retrieves an array of menu items for a given tab.
@param tab The tab for which to retrieve the menu items.
Expand Down
30 changes: 30 additions & 0 deletions Source/WebKit/UIProcess/API/Cocoa/_WKWebExtensionContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,24 @@ - (void)performCommand:(_WKWebExtensionCommand *)command
_webExtensionContext->performCommand(command._webExtensionCommand, WebKit::WebExtensionContext::UserTriggered::Yes);
}

#if USE(APPKIT)
- (BOOL)performCommandForEvent:(NSEvent *)event
{
NSParameterAssert([event isKindOfClass:NSEvent.class]);

return _webExtensionContext->performCommand(event);
}

- (_WKWebExtensionCommand *)commandForEvent:(NSEvent *)event
{
NSParameterAssert([event isKindOfClass:NSEvent.class]);

if (RefPtr result = _webExtensionContext->command(event))
return result->wrapper();
return nil;
}
#endif // USE(APPKIT)

- (NSArray<CocoaMenuItem *> *)menuItemsForTab:(id<_WKWebExtensionTab>)tab
{
NSParameterAssert([tab conformsToProtocol:@protocol(_WKWebExtensionTab)]);
Expand Down Expand Up @@ -1051,6 +1069,18 @@ - (void)performCommand:(_WKWebExtensionCommand *)command
{
}

#if USE(APPKIT)
- (BOOL)performCommandForEvent:(NSEvent *)event
{
return NO;
}

- (_WKWebExtensionCommand *)commandForEvent:(NSEvent *)event
{
return nil;
}
#endif // USE(APPKIT)

- (NSArray<CocoaMenuItem *> *)menuItemsForTab:(id<_WKWebExtensionTab>)tab
{
return nil;
Expand Down
134 changes: 134 additions & 0 deletions Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCommandCocoa.mm
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,63 @@
#if ENABLE(WK_WEB_EXTENSIONS)

#import "WebExtensionContext.h"
#import "WebExtensionMenuItem.h"
#import <wtf/BlockPtr.h>
#import <wtf/text/StringBuilder.h>

#if USE(APPKIT)
#import <Carbon/Carbon.h>
#endif

#if PLATFORM(IOS_FAMILY)
#import <UIKit/UIKit.h>
#endif

#if PLATFORM(IOS_FAMILY)
@implementation _WKWebExtensionKeyCommand

+ (instancetype)commandWithTitle:(NSString *)title image:(UIImage *)image input:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags handler:(WebExtensionKeyCommandHandlerBlock)handler
{
RELEASE_ASSERT(title.length);
RELEASE_ASSERT(input.length);
RELEASE_ASSERT(handler);

auto *command = [self commandWithTitle:title image:image action:@selector(_performWithTarget:) input:input modifierFlags:modifierFlags propertyList:nil];
if (!command)
return nil;

command->_handler = [handler copy];

return command;
}

- (id)copyWithZone:(NSZone *)zone
{
_WKWebExtensionKeyCommand *copy = [super copyWithZone:zone];
copy->_handler = [_handler copy];
return copy;
}

- (void)performWithSender:(id)sender target:(id)target
{
[super performWithSender:sender target:self];
}

- (id)_resolvedTargetFromFirstTarget:(id)firstTarget sender:(id)sender
{
return nil;
}

- (void)_performWithTarget:(id)target
{
ASSERT(_handler);
if (_handler)
_handler();
}

@end
#endif // PLATFORM(IOS_FAMILY)

namespace WebKit {

void WebExtensionCommand::dispatchChangedEventSoonIfNeeded()
Expand Down Expand Up @@ -164,6 +214,90 @@
return stringBuilder.toString();
}

CocoaMenuItem *WebExtensionCommand::platformMenuItem() const
{
#if USE(APPKIT)
auto *result = [[_WKWebExtensionMenuItem alloc] initWithTitle:description() handler:makeBlockPtr([this, protectedThis = Ref { *this }](id sender) mutable {
if (RefPtr context = extensionContext())
context->performCommand(const_cast<WebExtensionCommand&>(*this), WebExtensionContext::UserTriggered::Yes);
}).get()];

result.keyEquivalent = activationKey();
result.keyEquivalentModifierMask = modifierFlags().toRaw();

return result;
#else
return [UIAction actionWithTitle:description() image:nil identifier:nil handler:makeBlockPtr([this, protectedThis = Ref { *this }](UIAction *) mutable {
if (RefPtr context = extensionContext())
context->performCommand(const_cast<WebExtensionCommand&>(*this), WebExtensionContext::UserTriggered::Yes);
}).get()];
#endif
}

#if PLATFORM(IOS_FAMILY)
UIKeyCommand *WebExtensionCommand::keyCommand() const
{
if (activationKey().isEmpty())
return nil;

return [_WKWebExtensionKeyCommand commandWithTitle:description() image:nil input:activationKey() modifierFlags:modifierFlags().toRaw() handler:makeBlockPtr([this, protectedThis = Ref { *this }]() mutable {
if (RefPtr context = extensionContext())
context->performCommand(const_cast<WebExtensionCommand&>(*this), WebExtensionContext::UserTriggered::Yes);
}).get()];
}
#endif

#if USE(APPKIT)
bool WebExtensionCommand::matchesEvent(NSEvent *event) const
{
if (event.type != NSEventTypeKeyDown || event.isARepeat)
return false;

if (activationKey().isEmpty())
return false;

auto expectedModifierFlags = modifierFlags().toRaw();
if ((event.modifierFlags & expectedModifierFlags) != expectedModifierFlags)
return false;

static NeverDestroyed<HashMap<String, uint16_t>> specialKeyMap = HashMap<String, uint16_t> {
{ ","_s, kVK_ANSI_Comma },
{ "."_s, kVK_ANSI_Period },
{ " "_s, kVK_Space },
{ @"\uF704", kVK_F1 },
{ @"\uF705", kVK_F2 },
{ @"\uF706", kVK_F3 },
{ @"\uF707", kVK_F4 },
{ @"\uF708", kVK_F5 },
{ @"\uF709", kVK_F6 },
{ @"\uF70A", kVK_F7 },
{ @"\uF70B", kVK_F8 },
{ @"\uF70C", kVK_F9 },
{ @"\uF70D", kVK_F10 },
{ @"\uF70E", kVK_F11 },
{ @"\uF70F", kVK_F12 },
// Insert (\uF727) is not present on Apple keyboards.
{ @"\uF728", kVK_ForwardDelete },
{ @"\uF729", kVK_Home },
{ @"\uF72B", kVK_End },
{ @"\uF72C", kVK_PageUp },
{ @"\uF72D", kVK_PageDown },
{ @"\uF700", kVK_UpArrow },
{ @"\uF701", kVK_DownArrow },
{ @"\uF702", kVK_LeftArrow },
{ @"\uF703", kVK_RightArrow }
};

if (equalIgnoringASCIICase(activationKey(), String(event.charactersIgnoringModifiers)))
return true;

if (auto mappedKeyCode = specialKeyMap.get().get(activationKey()))
return mappedKeyCode == event.keyCode;

return false;
}
#endif // USE(APPKIT)

} // namespace WebKit

#endif // ENABLE(WK_WEB_EXTENSIONS)
Loading

0 comments on commit 2a0077a

Please sign in to comment.