Skip to content

Commit

Permalink
Merge pull request #3 from mmuffins/dev
Browse files Browse the repository at this point in the history
Update to version 3
  • Loading branch information
mmuffins authored Jun 21, 2019
2 parents fdb21f1 + a0efdef commit fd31c42
Show file tree
Hide file tree
Showing 9 changed files with 1,009 additions and 160 deletions.
83 changes: 35 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,46 @@ This extension adds an additional item to the MediaMonkey 5 main menu bar that a
Download the latest release from the releases section and double click extensionsMenu.mmip. An MediaMonkey dialog will automatically pop up, prompting you to confirm the installation.

## Registering actions
To add an action to the Extensions menu, follow the steps below.
Create an action for the function of the calling Extensions
```javascript
actions.testExtensions = {
create: {
title: function() {
return _('&TestExtension Create Action')
},
hotkeyAble: false,
category: actionCategories.extensions,
icon: 'createIcon',
execute: function() {
...
}
},
The extension menu automatically imports all valid actions from the global actions object if they are in the Extenions category. The following properties are required for an action to be considered valid:

delete: {
title: function() {
return _('&TestExtension Delete Action')
},
hotkeyAble: false,
category: actionCategories.extensions,
icon: 'deleteIcon',
execute: function() {
...
}
},
}
```
Since it's not possible to enforce a load order for extensions or otherwise ensure that the Extensions Menu extension is loaded before an extension wants to register an action, new menu items are added indirectly by pushing them into an import queue, which is picked up and processed once the Extensions Menu extension is loaded.
| Name | Type | Description |
| :------------ |:---------- | :---------- |
| title | function | Display name of the action |
| extension | function | Display name of the extension |
| execute | function | Function to execute when calling the action |

## Example
To create new actions, simply add new actions to the global actions object.

```javascript
// Add global section to register extension actions if it doesn't exist yet
if (typeof extensions == "undefined")
var extensions = {}
// Make sure to keep this header as it's needed to properly discover and
// import extension actions
if(!actionCategories.hasOwnProperty('extensions')){
actionCategories.extensions = () => _('Extensions');
}

// If the Extensions Menu was not loaded yet, not import queue exists.
if (!extensions.hasOwnProperty('extensionsMenuImportQueue'))
extensions.extensionsMenuImportQueue = []
// Create the needed actions
actions.MyAddonAction = {
title: () => _('&My Addon Action'),
hotkeyAble: true,
category: actionCategories.extensions,
icon: 'myAddonIcon',
extension: () => _("My &Addon"),
execute: () => alert('My Addon Action')
}

// Push actions that should be added to the menu to the import queue.
// Do not replace the import queue with a new array as this might remove already queued actions from other extensions
extensions.extensionsMenuImportQueue.push({action:actions.testExtension.create, order: 10, category:'TestExtension'})
extensions.extensionsMenuImportQueue.push({action:actions.testExtension.delete, order: 20, category:'TestExtension'})
actions.MyOtherAddonAction = {
title: () => _('My &Other Addon Action'),
hotkeyAble: true,
category: actionCategories.extensions,
icon: 'myOtherAddonIcon',
extension: () => _("My &Addon"),
execute: () => alert('My Other Addon Action')
}

// Refresh the menu to import actions that were added to the queue.
// Refresh the extension menu to import new actions.
// No need to worry if the menu can't be refreshed at this point because it's not loaded yet.
// It will automatically import all pending entries in the import queue as soon as its loaded.
if(extensions.extensionsMenu != null)
// It will automatically import all actions as soon it gets loaded by MediaMonkey.
if(typeof extensions != "undefined" && extensions.extensionsMenu != null)
extensions.extensionsMenu.refresh();
```

Each pushed to the import queue needs to have three properties:
* action: Contains the action to be executed.
* category: Category/Grouping name for all entries. This should usually be the name of the extension.
* order: Sort order for the respective entry within its category.
```
112 changes: 1 addition & 111 deletions actions_add.js
Original file line number Diff line number Diff line change
@@ -1,111 +1 @@
"use strict";

window.actionCategories.extensions = function(){
return _('Extensions');
}

// Add global section to register extension actions if it doesn't exist yet
if (typeof extensions == "undefined")
var extensions = {}

extensions.extensionsMenu = {
menuOrder: 55,
menuGrouporder: 10,
menu: [],

refresh: async function(){
let _this = this;
if (!extensions.hasOwnProperty('extensionsMenuImportQueue') || !extensions.extensionsMenuImportQueue instanceof Array) {
extensions.extensionsMenuImportQueue = [];
return;
}

if(extensions.extensionsMenuImportQueue.length == 0)
return;

let importItems = extensions.extensionsMenuImportQueue;
extensions.extensionsMenuImportQueue = [];

// filter out items with missing properties
importItems = importItems.filter(menuItm => {
return (menuItm.hasOwnProperty('action')
&& menuItm.action.hasOwnProperty('title')
&& menuItm.hasOwnProperty('order')
&& menuItm.hasOwnProperty('category'));
});

// filter out items with missing title function or blank title
importItems = importItems.filter(menuItm => {
return (menuItm.action.title instanceof Function && menuItm.action.title() != '');
});

// Add imported entries to menu, but skip duplicates
importItems.forEach(extensionItm => {
if (typeof (_this.menu.find(item => item.action.title() == extensionItm.action.title() && item.category == extensionItm.category)) != "undefined") {
return Promise.reject('Item already exists');
}

_this.menu.push({action: extensionItm.action, order: extensionItm.order, category: extensionItm.category, grouporder: 100});
})

_this.sortMenu();
await _this.pushToUi();
},

pushToUi: async function(){
// pushes the internal menu to the ui
let _this = this;

if(_this.menu.length == 0)
return;

// Check if the menu was already pushed to UI, and only update the menu items if it was
for (let i = 0; i < window.mainMenuItems.length; i++) {
const itm = window.mainMenuItems[i].action;

if(itm.hasOwnProperty('title') && itm.title instanceof Function && itm.title() == '&Extensions'){
itm.submenu = _this.menu;
return;
}
}

let newMenu = {
action: {
title: function () {
return _('&Extensions');
},
visible: !webApp,
submenu: _this.menu
},
order: _this.menuOrder,
grouporder: _this.menuGrouporder,
}

window.mainMenuItems.push(newMenu);
uitools.switchMainMenu(false);
uitools.switchMainMenu(true);
},

getCategories: function(){
// returns an array with all categories in the menu
return [...new Set(this.menu.map(x => x.category))];
},

sortMenu: function(){
// sorts the menu by category

// get list of all categories and add a sort value
let cat = this.getCategories().sort();
let catOrder = {};
for (let index = 0; index < cat.length; index++) {
catOrder[cat[index]] = (index + 1) * 100;
}

// update each menu item with the sort index
this.menu.forEach(el => {
el.grouporder = catOrder[el.category]
});
}
}

extensions.extensionsMenu.refresh()
actionCategories.extensions = () => _('Extensions');
9 changes: 9 additions & 0 deletions config.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div data-id="pnlCollectionsRoot" class="padding fill flex column" data-tip="Organize the displayed extension actions into groups.">
<div data-id="lvTreeView" class="fill flex column hSeparatorTiny" data-control-class="ExtensionTree" data-init-params="{enableDragDrop: true}"></div>
<div data-control-class="Buttons">
<div data-id='btnNewGroup' data-control-class="Button" data-position='opposite' data-tip="Add a new group.">New</div>
<div data-id='btnDeleteGroup' data-control-class="Button" data-position='opposite' data-tip="Delete the selected group.">Delete</div>
<div data-id='btnRenameGroup' data-control-class="Button" data-position='opposite' data-tip="Rename the selected group.">Rename</div>
<div data-id='btnResetTree' data-control-class="Button" data-position='opposite' data-tip="Reset the tree to its initial state.">Reset</div>
</div>
</div>
64 changes: 64 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use strict";

requirejs('viewHandlers.js');
requirejs("Scripts/ExtensionsMenu/extensionsMenu")
requirejs("controls/extensionTree")

window.configInfo = {
load: function(panel, addon){
let _this = this;
panel.innerHTML = window.loadFile(addon.configFile.replace('config.js','config.html'));
let pnl = panel.firstElementChild;
initializeControls(pnl);

let UI = getAllUIElements(qid('pnlCollectionsRoot'));
let TV = UI.lvTreeView;
let ds = TV.controlClass.dataSource;

extensions.extensionsMenu.discardChanges();
ds.root.handlerID = 'extensionsMenuTreeRoot';
ds.root.dataSource = extensions.extensionsMenu.getEditRootNode();

TV.controlClass.expandAll()

app.listen(UI.btnNewGroup, 'click', function () {
let newGroupNode = extensions.extensionsMenu.newGroup("New Group");
nodeUtils.refreshNodeChildren(TV.controlClass.root);
let newGroup = TV.controlClass.root.findChild(`extensionsGroupNode:${newGroupNode.id}`);

// focus node and enter edit node
TV.controlClass.focusNode(newGroup);
TV.controlClass.editStart()
});

app.listen(UI.btnDeleteGroup, 'click', function () {
TV.controlClass.deleteSelected();
});

app.listen(UI.btnRenameGroup, 'click', function () {
TV.controlClass.editStart()
});

app.listen(UI.btnResetTree, 'click', () => {
extensions.extensionsMenu.resetActionTree();
let tree = app.createTree();
tree.root.handlerID = 'extensionsMenuTreeRoot';
tree.root.dataSource = extensions.extensionsMenu.getEditRootNode();
TV.controlClass.dataSource = tree;
TV.controlClass.expandAll()
});
},

save: function(panel, addon){

extensions.extensionsMenu.applyChanges()
extensions.extensionsMenu.saveSettings();
extensions.extensionsMenu.reloadActionTree();

// the config menu runs in a separate context from the main window
let mainAppWindow = app.dialogs.getMainWindow()._window;
mainAppWindow.extensions.extensionsMenu.refresh();
},
}


Loading

0 comments on commit fd31c42

Please sign in to comment.