Skip to content

Commit

Permalink
Launch custom commands directly from Menu Item or Dropdown #59 - Draft
Browse files Browse the repository at this point in the history
Register action programmatically via template in layer.xml (else it does not work using instanceCreate)
  • Loading branch information
markiewb committed Mar 25, 2016
1 parent e4b6116 commit 92f431a
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 55 deletions.
18 changes: 18 additions & 0 deletions QuickOpener/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,24 @@
<artifactId>org-netbeans-modules-javahelp</artifactId>
<version>${version.netbeans}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.netbeans.api</groupId>
<artifactId>org-netbeans-modules-nbjunit</artifactId>
<version>${version.netbeans}</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,126 @@
package me.dsnet.quickopener.actions.layer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import javax.swing.Action;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;

/**
*
* Taken from http://wiki.netbeans.org/DevFaqActionsAddAtRuntime
* Based on from http://wiki.netbeans.org/DevFaqActionsAddAtRuntime and
* https://github.com/Tateology/java-corpus/blob/master/netbeans/maven/src/org/netbeans/modules/maven/actions/RunCustomMavenAction.java#L160
*/
public class ActionRegistrationService {

private static final String ACTIONS = "Actions/";
private static final String MENU = "Menu/";
private static final String SHORTCUTS = "Shortcuts";
private static final String TOOLBARS = "Toolbars/";

/**
* Registers an action with the platform along with optional shortcuts and
* menu items.
* <pre>
* &lt;file name="action1.instance"&gt;
* &lt;attr methodvalue="me.dsnet.quickopener.actions.layer.LayerXMLConfiguredCustomRunnerAction.create" name="instanceCreate"/&gt;
* &lt;attr name="imagePath" stringvalue="me/dsnet/quickopener/icons/run.png"/&gt;
* &lt;attr name="displayName" stringvalue="Notepad"/&gt;
* &lt;attr name="custom-command" stringvalue="notepad ${file}"/&gt;
* &lt;/file&gt;
* </pre>
*
*
* @param name Display name of the action.
* @param category Category in the Keymap tool.
* @param shortcut Default shortcut, use an empty string or null for none.
* @param menuPath Menu location starting with "Menu", like "Menu/File"
* @param action an action object to attach to the action entry.
* @throws IOException
*/
public static void registerAction(String name, String category, String shortcut, String menuPath, Action action) throws IOException {
///////////////////////
// Add/Update Action //
///////////////////////
String originalFile = "Actions/" + category + "/" + name + ".instance";
FileObject in = getFolderAt("Actions/" + category);
FileObject obj = in.getFileObject(name, "instance");
if (obj == null) {
obj = in.createData(name, "instance");
public static void registerActionAsMenuAndToolbar(String name, String category) {
try {
String originalFile = String.format("%s/%s.instance", ACTIONS + category, name);
registerActionForMenu(originalFile, category, name);
registerActionForToolbar(originalFile, category, name);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}

public static void unregisterAction(String name, String category) throws IOException {
String originalFile = String.format("%s/%s.instance", ACTIONS + category, name);

deleteFileObject(getFolderAt(ACTIONS + category), name, "instance");
deleteFileObject(getFolderAt(TOOLBARS + category), name, "shadow");
deleteFileObject(getFolderAt(MENU + category), name, "shadow");

// iterate over all shortcuts, remove the shortcut which links to originalFileName
// FIXME does it really work?
FileObject folderAt = getFolderAt(SHORTCUTS);
List<FileObject> forRemoval = new ArrayList<FileObject>();

for (FileObject fo : folderAt.getChildren()) {
if (originalFile.equals(fo.getAttribute("originalFile"))) {
//found shortcut by its reference to the original action
forRemoval.add(fo);
}
}
action.putValue(Action.NAME, name);
obj.setAttribute("instanceCreate", action);
obj.setAttribute("instanceClass", action.getClass().getName());

/////////////////////
// Add/Update Menu //
/////////////////////
in = getFolderAt(menuPath);
obj = in.getFileObject(name, "shadow");
// Create if missing.
if (obj == null) {
obj = in.createData(name, "shadow");
obj.setAttribute("originalFile", originalFile);
for (FileObject fo : forRemoval) {
fo.delete();
}
}

public static void registerAction(final String category, String id, String displayName, String command, String imagePath, String fqnFactoryMethodName) throws IOException {
//NOTE cannot create 'name="instanceCreate" methodvalue="xxx"' in code, so using a template
FileObject template = FileUtil.getConfigFile("QuickOpener/actionTemplate.instance");
FileObject obj = template.copy(getFolderAt(ACTIONS + category), displayName, "instance");

/////////////////////////
// Add/Update Shortcut //
/////////////////////////
in = getFolderAt("Shortcuts");
obj = in.getFileObject(shortcut, "shadow");
if (obj == null) {
obj = in.createData(shortcut, "shadow");
obj.setAttribute("originalFile", originalFile);
obj.setAttribute("imagePath", imagePath);
obj.setAttribute("displayName", escapeXMLCharacters(displayName));
obj.setAttribute("custom-command", escapeXMLCharacters(command));
}

public static void registerAction(String id, final String category, String displayName, String command) {
try {
ActionRegistrationService.registerAction(category, id, displayName, command, "me/dsnet/quickopener/icons/run.png", "me.dsnet.quickopener.actions.layer.LayerXMLConfiguredCustomRunnerAction.create");
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}

private static void deleteFileObject(FileObject folder, String name, final String ext) throws IOException {
FileObject fo = folder.getFileObject(name, ext);
if (fo != null) {
fo.delete();
}
}

/**
* Replace xml-characters like &lt with &amp;lt;
*
* @param text
* @return
*/
private static String escapeXMLCharacters(String text) {
//TODO replace xml-characters like < with &lt;
return text;
}

/**
* Replace xml-characters like &amp;lt; with &lt
*
* @param text
* @return
*/
public static String unescapeXMLCharacters(String text) {
//TODO replace xml-characters like &lt; with <
return text;
}

private static FileObject getFolderAt(String inputPath) throws IOException {
final String[] split = inputPath.split("/");
if (null == split || split.length == 0) {
Expand Down Expand Up @@ -88,6 +149,16 @@ private static FileObject getFolderAt(String inputPath) throws IOException {
return FileUtil.getConfigFile(inputPath);
}

private static FileObject getOrCreateFileObject(FileObject folder, String name, final String ext) throws IOException {
FileObject fo = folder.getFileObject(name, ext);
if (fo == null) {
fo = folder.createData(name, ext);
}
FileObject configFile = FileUtil.getConfigFile(String.format("%s/%s.%s", folder.getPath(), name, ext));
return configFile;
// return fo;
}

private static String join(String separator, List<String> list) {
StringBuilder sb = new StringBuilder();
final int size = list.size();
Expand All @@ -100,4 +171,17 @@ private static String join(String separator, List<String> list) {
}
return sb.toString();
}

private static void registerActionForMenu(String originalFile, String category, String name) throws IOException {
FileObject obj = getOrCreateFileObject(getFolderAt(MENU + category), name, "shadow");
obj.setAttribute("originalFile", originalFile);
}

private static void registerActionForToolbar(String originalFile, String category, String name) throws IOException {

FileObject obj = getOrCreateFileObject(getFolderAt(TOOLBARS + category), name, "shadow");
obj.setAttribute("originalFile", originalFile);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import javax.swing.AbstractAction;
import javax.swing.Action;
import me.dsnet.quickopener.actions.RunCommand;
import static me.dsnet.quickopener.actions.layer.ActionRegistrationService.unescapeXMLCharacters;

/**
* Action for running custom commands. The configuration is purely based on
* custom attribute tags of the action registration in the layer.xml file.
*
* <pre>
* &lt;file name="action1.instance"&gt;
* &lt;attr methodvalue="me.dsnet.quickopener.actions.layer.SuperclassSensitiveAction.create" name="instanceCreate"/&gt;
* &lt;attr methodvalue="me.dsnet.quickopener.actions.layer.LayerXMLConfiguredCustomRunnerAction.create" name="instanceCreate"/&gt;
* &lt;attr name="imagePath" stringvalue="me/dsnet/quickopener/icons/run.png"/&gt;
* &lt;attr name="displayName" stringvalue="Notepad"/&gt;
* &lt;attr name="custom-command" stringvalue="notepad ${file}"/&gt;
Expand All @@ -33,14 +34,13 @@ public final class LayerXMLConfiguredCustomRunnerAction extends AbstractAction {
private final RunCommand runCommand;

/**
* Referenced from layer.xml like this
*
* @param map
* @return
*/
static Action create(Map map) {
final String command = (String) map.get("custom-command");
final String displayName = (String) map.get("displayName");
public static Action create(Map map) {
final String command = unescapeXMLCharacters((String) map.get("custom-command"));
final String displayName = unescapeXMLCharacters((String) map.get("displayName"));
final String iconBase = (String) map.get("imagePath");
return new LayerXMLConfiguredCustomRunnerAction(iconBase, displayName, command);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
import com.sessonad.oscommands.commands.Commands;
import me.dsnet.quickopener.QuickMessages;
import me.dsnet.quickopener.actions.popup.PropertyTableModel;
import java.util.List;
import java.util.prefs.BackingStoreException;
import javax.swing.JTable;
import me.dsnet.quickopener.actions.layer.ActionRegistrationService;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;

/**
Expand Down Expand Up @@ -211,6 +210,12 @@ private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRS
return;
}
PrefsUtil.store("command" + description, value);
//TODO escape id
//TODO delete previous registrations to prevent duplicates
String id = description;
ActionRegistrationService.registerAction(id, "QuickOpener", description, value);
ActionRegistrationService.registerActionAsMenuAndToolbar(id, "QuickOpener");

jTable2.setModel(new PropertyTableModel("command"));
}//GEN-LAST:event_jButton1ActionPerformed

Expand Down
25 changes: 9 additions & 16 deletions QuickOpener/src/main/resources/me/dsnet/quickopener/layer.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "http://www.netbeans.org/dtds/filesystem-1_2.dtd">
<filesystem>
<folder name="QuickOpener">
<file name="actionTemplate.instance">
<attr methodvalue="me.dsnet.quickopener.actions.layer.LayerXMLConfiguredCustomRunnerAction.create" name="instanceCreate"/>
<attr name="uuid" stringvalue="0000"/>
<attr name="imagePath" stringvalue="me/dsnet/quickopener/icons/run.png"/>
<attr name="displayName" stringvalue="Notepad"/>
<attr name="custom-command" stringvalue="notepad ${file} ${line} ${selectedText}"/>
</file>
</folder>
<folder name="Actions">
<folder name="QuickOpener">
<file name="foo-action1.instance">
<attr methodvalue="me.dsnet.quickopener.actions.layer.LayerXMLConfiguredCustomRunnerAction.create" name="instanceCreate"/>
<attr name="imagePath" stringvalue="me/dsnet/quickopener/icons/run.png"/>
<attr name="displayName" stringvalue="Notepad"/>
<attr name="custom-command" stringvalue="notepad ${file} ${line} ${selectedText}"/>
</file>
</folder>

<folder name="Tools">
<file name="me-dsnet-quickopener-actions-ToolbarPresenter.instance">
<!--me.dsnet.quickopener.actions.CustomCommand-->
Expand Down Expand Up @@ -114,11 +114,6 @@
<attr name="originalFile" stringvalue="Actions/Tools/me-dsnet-quickopener-actions-ToolbarPresenter.instance"/>
<attr intvalue="800" name="position"/>
</file>
<!-- <file name="foo-action1.shadow">
<attr name="originalFile" stringvalue="Actions/QuickOpener/foo-action1.instance"/>
<attr intvalue="900" name="position"/>
</file>-->

</folder>
</folder>
<folder name="Shortcuts">
Expand All @@ -128,7 +123,5 @@
<file name="O-4.shadow"><attr name="originalFile" stringvalue="Actions/Tools/me-dsnet-quickopener-actions-popup-CustomCommandPopupAction.instance"/></file>
<file name="O-5.shadow"><attr name="originalFile" stringvalue="Actions/Tools/me-dsnet-quickopener-actions-popup-CustomFileSystemPopupAction.instance"/></file>
<file name="O-6.shadow"><attr name="originalFile" stringvalue="Actions/Tools/me-dsnet-quickopener-actions-popup-CustomTerminalPopupAction.instance"/></file>

<file name="O-7.shadow"><attr name="originalFile" stringvalue="Actions/QuickOpener/foo-action1.instance"/></file>
</folder>
</filesystem>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package me.dsnet.quickopener.actions.layer;

import org.junit.Test;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;

/**
*
* @author markiewb
*/
public class ActionRegistrationServiceTest extends org.netbeans.junit.NbTestCase {

public ActionRegistrationServiceTest(String name) {
super(name);
}

@Test
public void testRegisterAction() throws Exception {
ActionRegistrationService.registerAction("id", "category", "displayName", "command");
{
FileObject configFile = FileUtil.getConfigFile("Actions/category/displayName.instance");
assertNotNull(configFile);
//check for attribute
assertEquals("displayName", configFile.getAttribute("displayName"));
assertEquals("command", configFile.getAttribute("custom-command"));
assertNotNull(configFile.getAttribute("imagePath"));
}

}

@Test
public void testRegisterMenu() throws Exception {
ActionRegistrationService.registerActionAsMenuAndToolbar("action1", "category");
{
FileObject configFile = FileUtil.getConfigFile("Menu/category/action1.shadow");
assertNotNull(configFile);
}
}

@Test
public void testRegisterToolbar() throws Exception {
ActionRegistrationService.registerActionAsMenuAndToolbar("action1", "category");
{
FileObject configFile = FileUtil.getConfigFile("Toolbars/category/action1.shadow");
assertNotNull(configFile);
}
}

@Test
public void testUnregisterAction() throws Exception {
ActionRegistrationService.registerAction("id", "category", "displayName", "command");
ActionRegistrationService.registerActionAsMenuAndToolbar("displayName", "category");

ActionRegistrationService.unregisterAction("displayName", "category");
assertNull(FileUtil.getConfigFile("Actions/category/displayName.instance"));
assertNull(FileUtil.getConfigFile("Menu/category/displayName.shadow"));
assertNull(FileUtil.getConfigFile("Toolbars/category/displayName.shadow"));
}

}

0 comments on commit 92f431a

Please sign in to comment.