Skip to content

Commit

Permalink
Add GM.registerMenuCommand().
Browse files Browse the repository at this point in the history
Resolves one part of greasemonkey#2714 .
  • Loading branch information
esperecyan committed Jan 10, 2021
1 parent e0caac0 commit 51c1c77
Show file tree
Hide file tree
Showing 14 changed files with 600 additions and 1 deletion.
12 changes: 12 additions & 0 deletions _locales/en/messages.json
Expand Up @@ -102,6 +102,12 @@
"gm_opentab_bad_URL": {
"message": "GM.openInTab: Could not understand the URL: $1"
},
"gm_rmc_bad_access_key": {
"message": "GM.registerMenuCommand: \"accessKey\" must be a single character"
},
"gm_rmc_bad_command_func": {
"message": "GM.registerMenuCommand: \"commandFunc\" must be a function"
},
"gm_script_id": {
"message": "[Greasemonkey script $1]"
},
Expand Down Expand Up @@ -238,6 +244,12 @@
"user_matches": {
"message": "User matches"
},
"user_script_commands": {
"message": "User script commands"
},
"user_script_commands_button": {
"message": "User script commands..."
},
"user_script_options": {
"message": "User script options"
},
Expand Down
55 changes: 55 additions & 0 deletions doc/Messages.md
Expand Up @@ -98,6 +98,25 @@ Received by: `bg/export-db.js`

Send with no data to export the entire Userscript dataset.

# ListMenuCommands
Sent by: `browser/monkey-menu.js`
Received by: `bg/on-user-script-menu-command.js`

Triggered when the popup menu is loaded.
Lists menu commands registered by the `GM.registerMenuCommand()` method called by user scripts on the specified tab.

Data:

* `tabId` A tab's ID (integer).

Response data:

* An array of command objects. Each object has
- `id` A command id that can be used as HTML/XML ID.
- `caption` A string given as the first parameter of the `GM.registerMenuCommand()` method.
- `accessKey` A code point (string) or empty string given as the third parameter of the `GM.registerMenuCommand()` method.
- `icon` A URL (string) of an icon which is returned by `iconUrl()` function.

# ListUserScripts
Received by: `bg/user-script-registry.js`.

Expand All @@ -111,6 +130,22 @@ Response data:

* An array of `.details` objects from installed `EditableUserScript`s.

# MenuCommandClick
Sent by: `browser/monkey-menu.js`
Received by: `bg/on-user-script-menu-command.js`

Triggered when a command button on the popup menu is clicked by the user.
Posts message `{type: 'onclick'}` on `UserScriptMenuCommand` channel
to call a function given as the second parameter of the `GM.registerMenuCommand()` method.

Data:

* `id` The command id (string) as returned by `ListMenuCommands` message.

Response data:

* `undefined`

# OptionsLoad
Sent by: `browser/monkey-menu.js`
Received by: `bg/options.js`
Expand Down Expand Up @@ -152,6 +187,26 @@ user. Data:

Callers should specify one or the other, not both.

# UserScriptMenuCommand
Sent by: `content/api-provider-source.js`
Received by: `bg/on-user-script-menu-command.js`

This is a channel (not a message).
Triggered when the `GM.registerMenuCommand()` method is called by an user script.

Data:

* `details` The details object specifying the command. It has
- `caption` A string given as the first parameter of the `GM.registerMenuCommand()` method.
- `accessKey` A code point (string) or empty string given as the third parameter of the `GM.registerMenuCommand()` method.

Response data:

* `undefined`

Messages exchanged via this channel are private to its implementation.
See sender and receiver for further detail.

# UserScriptOptionsSave
Sent by: `browser/monkey-menu.js`
Received by: `bg/user-script-registry.js`
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
@@ -1,6 +1,7 @@
module.exports = function(config) {
config.set({
files: [
'./node_modules/sinon-chrome/bundle/sinon-chrome-webextensions.min.js',
'./test/setup.js',
'./third-party/convert2RegExp.js',
'./third-party/MatchPattern.js',
Expand Down
2 changes: 2 additions & 0 deletions manifest.json
Expand Up @@ -33,6 +33,7 @@
"/src/bg/execute.js",
"/src/bg/export-db.js",
"/src/bg/on-message.js",
"/src/bg/on-user-script-menu-command.js",
"/src/bg/on-user-script-notification.js",
"/src/bg/on-user-script-open-in-tab.js",
"/src/bg/on-user-script-xhr.js",
Expand All @@ -48,6 +49,7 @@
"/src/supported-apis.js",
"/src/user-script-obj.js",
"/src/util/check-api-call-allowed.js",
"/src/util/iconUrl.js",
"/src/util/log-unhandled-error.js",
"/src/util/open-editor.js",
"/third-party/convert2RegExp.js",
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -15,6 +15,7 @@
"karma-sinon-chrome": "github:arantius/karma-sinon-chrome",
"mocha": "^3.1.2",
"sinon": "^4.1.5",
"sinon-chrome": "^2.2.1",
"tinybind": "^0.11.0"
}
}
40 changes: 39 additions & 1 deletion src/bg/api-provider-source.js
Expand Up @@ -48,6 +48,10 @@ function apiProviderSource(userScript) {
source += 'GM.openInTab = ' + GM_openInTab.toString() + ';\n\n';
}

if (grants.includes('GM.registerMenuCommand')) {
source += 'GM.registerMenuCommand = ' + GM_registerMenuCommand.toString() + ';\n\n';
}

if (grants.includes('GM.setClipboard')) {
source += 'GM.setClipboard = ' + GM_setClipboard.toString() + ';\n\n';
}
Expand All @@ -56,7 +60,6 @@ function apiProviderSource(userScript) {
source += 'GM.xmlHttpRequest = ' + GM_xmlHttpRequest.toString() + ';\n\n';
}

// TODO: GM_registerMenuCommand -- maybe.
// TODO: GM_getResourceText -- maybe.

source += '})();';
Expand Down Expand Up @@ -191,6 +194,41 @@ function GM_openInTab(url, openInBackground) {
}


function GM_registerMenuCommand(caption, commandFunc, accessKey = '') {
if (typeof caption != 'string') {
caption = String(caption);
}

if (typeof commandFunc != 'function') {
throw new Error(_('gm_rmc_bad_command_func'));
}

if (typeof accessKey != 'string' || Array.from(accessKey).length > 1) {
throw new Error(_('gm_rmc_bad_access_key'));
}

let port = chrome.runtime.connect({name: 'UserScriptMenuCommand'});
port.onMessage.addListener(msg => {
if (msg.type === 'onclick') {
commandFunc();
}
});
port.postMessage({
'details': {
caption,
accessKey,
},
'name': 'register',
'uuid': _uuid,
});
addEventListener('unload', event => {
if (event.isTrusted) {
port.disconnect();
}
});
}


function GM_setClipboard(text) {
// TODO: This. The check only works background side, but this implementation
// relies on clipboardWrite permission leaking to the content script so we
Expand Down
74 changes: 74 additions & 0 deletions src/bg/on-user-script-menu-command.js
@@ -0,0 +1,74 @@
/*
This file is responsible for providing the GM.registerMenuCommand API method.
*/

// Private implementation
(function () {

const commandMap = new Map();


function _randomId() {
return 'id' + window.crypto.getRandomValues(new Uint8Array(16)).join('');
}


function onListMenuCommands(message, sender, sendResponse) {
sendResponse(
Array.from(commandMap.values())
.filter(command => command.port.sender.tab.id === message.tabId)
.map(command => ({
id: command.id,
caption: command.caption,
accessKey: command.accessKey,
icon: command.icon,
})));
}
window.onListMenuCommands = onListMenuCommands;


function onMenuCommandClick(message, sender, sendResponse) {
if (commandMap.has(message.id)) {
commandMap.get(message.id).port.postMessage({type: 'onclick'});
}
}
window.onMenuCommandClick = onMenuCommandClick;


function registerMenuCommand({port, caption, accessKey, uuid}) {
const command = {
id: _randomId(),
port,
caption,
accessKey,
icon: iconUrl(UserScriptRegistry.scriptByUuid(uuid)),
};

commandMap.set(command.id, command);

port.onDisconnect.addListener(port => {
commandMap.delete(command.id);
});
}


function onUserScriptMenuCommand(port) {
if (port.name != 'UserScriptMenuCommand') return;

port.onMessage.addListener((msg, port) => {
checkApiCallAllowed('GM.registerMenuCommand', msg.uuid);
switch (msg.name) {
case 'register':
registerMenuCommand(Object.assign({
port,
uuid: msg.uuid,
}, msg.details));
break;
default:
console.warn('UserScriptMenuCommand port un-handled message name:', msg.name);
}
});
}
chrome.runtime.onConnect.addListener(onUserScriptMenuCommand);

})();
9 changes: 9 additions & 0 deletions src/browser/monkey-menu.css
Expand Up @@ -51,6 +51,7 @@ section:focus {

/* Hide non-main sections by default. */
section.options,
section.menu-commands,
section.user-script,
section.user-script-options
{
Expand All @@ -67,6 +68,7 @@ body#main-menu section.main-menu

/* Slide the other menu in when it's active. */
body#options section.options,
body#menu-commands section.menu-commands,
body#user-script section.user-script,
body#user-script-options section.user-script-options
{ margin-left: -100vw; }
Expand Down Expand Up @@ -226,3 +228,10 @@ section.options #add-exclude-current {
white-space: nowrap;
width: 90vw;
}

/***************************** MENU COMMANDS *********************************/

section.menu-commands .access-key {
text-decoration: underline;
text-transform: uppercase;
}
29 changes: 29 additions & 0 deletions src/browser/monkey-menu.html
Expand Up @@ -23,6 +23,13 @@
<span class="arrow"></span>
</menuitem>

<hr rv-hide="menuCommands | empty">
<menuitem id="open-menu-commands" tabindex="0" role="button" rv-hide="menuCommands | empty">
<i class="icon fa fa-fw fa-list-alt"></i>
<span class="text">{'user_script_commands_button'|i18n}</span>
<span class="arrow"></span>
</menuitem>

<hr rv-if="userScripts.active | bothArraysEmpty userScripts.inactive | not">

<div id="script-list-scroll" tabindex="-1">
Expand Down Expand Up @@ -137,6 +144,28 @@ <h2>{'editor'|i18n}</h2>
</section>


<section class="menu-commands" tabindex="-1" role="dialog">
<header>
<menuitem tabindex="0" class="go-back" role="button"
rv-aria-label="'back'|i18n"></menuitem>
<span role="heading" aria-level="1">{'user_script_commands'|i18n}</span>
</header>

<hr>

<menuitem rv-each-command="menuCommands"
rv-command="command.id | mmUuidMenu"
rv-aria-keyshortcuts="command.accessKey"
tabindex="0"
role="button">
<i class="icon"><img rv-src="command.icon"></i>
<span class="text">
{command.caption}<span rv-if="command.accessKey">(<span class="access-key">{command.accessKey}</span>)</span>
</span>
</menuitem>
</section>


<section class="user-script" tabindex="-1" role="dialog">
<header>
<menuitem tabindex="0" class="go-back" role="button"
Expand Down

0 comments on commit 51c1c77

Please sign in to comment.