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 Mar 4, 2018
1 parent 0fb3c97 commit 6be13bf
Show file tree
Hide file tree
Showing 14 changed files with 619 additions and 6 deletions.
12 changes: 12 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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 @@ -117,6 +123,12 @@
"unnamed_script_RAND": {
"message": "Unnamed Script $1"
},
"user_script_commands": {
"message": "User script commands"
},
"user_script_commands_button": {
"message": "User script commands..."
},
"user_scripts_for_this_tab": {
"message": "User scripts for this tab"
},
Expand Down
55 changes: 55 additions & 0 deletions doc/Messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ Received by: `bg/is-enabled.js`.

Send this to toggle the global enabled status. No data.

# 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 @@ -103,6 +122,22 @@ Response data:

* An array of `.details` objects from installed `RunnableUserScript`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`

# UserScriptChanged
Sent by: `bg/user-script-registry.js`

Expand Down Expand Up @@ -136,6 +171,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.

# UserScriptToggleEnabled
Sent by: `content/manage-user-scripts.js`
Received by: `bg/user-script-registry.js`
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"src/bg/on-message.js",
"src/bg/on-user-script-notification.js",
"src/bg/on-user-script-open-in-tab.js",
"src/bg/on-user-script-menu-command.js",
"src/bg/on-user-script-xhr.js",
"src/bg/user-script-detect.js",
"src/bg/user-script-registry.js",
Expand All @@ -44,6 +45,7 @@
"src/parse-user-script.js",
"src/supported-apis.js",
"src/user-script-obj.js",
"src/util/iconUrl.js",
"src/util/check-api-call-allowed.js",
"third-party/convert2RegExp.js",
"third-party/MatchPattern.js",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"karma-sinon": "^1.0.5",
"karma-sinon-chrome": "^0.2.0",
"mocha": "^3.1.2",
"sinon": "^4.1.5"
"sinon": "^4.1.5",
"sinon-chrome": "^2.2.1"
}
}
40 changes: 39 additions & 1 deletion src/bg/api-provider-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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 @@ -55,7 +59,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 @@ -190,6 +193,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
Original file line number Diff line number Diff line change
@@ -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);

})();
14 changes: 11 additions & 3 deletions src/browser/monkey-menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,20 @@ body #menu {
transition-property: margin;
}

body.detail #menu {
body.detail #menu,
body.commands #menu {
margin-left: -100vw;
}

body #user-script-detail {
body #user-script-detail,
body #menu-commands {
visibility: hidden;
transition-duration: 0.25s;
transition-property: visibility;
}

body.detail #user-script-detail {
body.detail #user-script-detail,
body.commands #menu-commands {
visibility: visible;
}

Expand Down Expand Up @@ -201,6 +204,11 @@ section {
text-overflow: ellipsis;
}

.subview-item .access-key {
text-decoration: underline;
text-transform: uppercase;
}

.subview-item::after {
margin-inline-start: 10px;
content: attr(after);
Expand Down
27 changes: 27 additions & 0 deletions src/browser/monkey-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
</div>
-->

<hr rv-hide="menuCommands | empty">
<a href="#menu-commands" class="subview-item next-menu"
id="open-menu-commands" rv-hide="menuCommands | empty">
<div class="icon fa fa-fw fa-list-alt "></div>
<div class="text">{'user_script_commands_button'|i18n}</div>
</a>

<div rv-hide="userScripts.active | empty">
<hr>
<div class="subview-item heading">
Expand Down Expand Up @@ -121,6 +128,26 @@
</section>


<section id="menu-commands" class="subview">
<header class="subview-header">
<a href="#open-menu-commands" class="subview-back"></a>
<div class="subview-title">{'user_script_commands'|i18n}</div>
</header>

<div class="subview-body">
<a rv-each-command="menuCommands"
rv-href="command.id | mmUuidMenu"
rv-aria-keyshortcuts="command.accessKey"
class="subview-item">
<div class="icon"><img rv-src="command.icon"></div>
<div class="text">
{command.caption}<span rv-if="command.accessKey">(<span class="access-key">{command.accessKey}</span>)</span>
</div>
</a>
</div>
</section>


<script src="/src/i18n.js"></script>
<script src="/third-party/rivets/rivets.bundled.min.js"></script>
<script src="/third-party/convert2RegExp.js"></script>
Expand Down

0 comments on commit 6be13bf

Please sign in to comment.