diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcda348 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Freeplane's map backup files +*.mm.bak \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..38d33e8 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Freeplane "Desktop Actions" Addon + +This add-on provides a script to open or browse files, directories and URIs using the [java.awt.Desktop](https://docs.oracle.com/javase/8/docs/api/java/awt/Desktop.html) class: it looks for the associated application registered on the current platform, and launch it to handle the resource. + +## The "Open or browse resource.." action + +First, it tries to resolve the text contained in the selected node to an _URI_ resource or a _path_ (be it an _absolute_ or _relative_ referring to a _file_ or a _directory_). + +For example, consider the following directory tree: + +``` +svu.mm +theme.mp3 +/images + benson.jpg + stabler.jpg +``` + +Then, the following scenarios apply for the `svu.mm` map: + +- Opening a node containing the text `images/benson.jpg` or `images/stabler.jpg` launches the default image viewer for our viewing pleasure. +- Opening a node containing the text `theme.mp3` instead launches the default media player which plays that audio file, recommended. +- Opening a node containing the text `https://en.wikipedia.org/wiki/Law_%26_Order:_Special_Victims_Unit` launches the default browser and displays the page. diff --git a/desktopActions-v0.1.addon.mm b/desktopActions-v0.1.addon.mm new file mode 100644 index 0000000..8b09d0f --- /dev/null +++ b/desktopActions-v0.1.addon.mm @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ The homepage of this add-on should be set as the link of the root node. +

+

+ The basic properties of this add-on. They can be used in script names and other attributes, e.g. "${name}.groovy". +

+
    +
  • + name: The name of the add-on, normally a technically one (no spaces, no special characters except _.-). +
  • +
  • + author: Author's name(s) and (optionally) email adresses. +
  • +
  • + version: Since it's difficult to protect numbers like 1.0 from Freeplane's number parser it's advised to prepend a 'v' to the number, e.g. 'v1.0'. +
  • +
  • + freeplane-version-from: The oldest compatible Freeplane version. The add-on will not be installed if the Freeplane version is too old. +
  • +
  • + freeplane-version-to: Normally empty: The newest compatible Freeplane version. The add-on will not be installed if the Freeplane version is too new. +
  • +
  • + updateUrl: URL of the file containing information (version, download url) on the latest version of this add-on. By default: "${homepage}/version.properties" +
  • +
+ + +
+ + + + + + + + + +

+ Description would be awkward to edit as an attribute. +

+

+ So you have to put the add-on description as a child of the 'description' node. +

+

+ To translate the description you have to define a translation for the key 'addons.${name}.description'. +

+ + +
+ + + + + + + +

+ This add-on provides a meaningful script to open files, directories and URIs using the java.awt.Desktop class: it looks for the associated application registered on the current platform, and launch it to handle a URI or file. +

+ + +
+
+
+ + + + + + + + + +

+ Change log of this add-on: append one node for each noteworthy version and put the details for each version into a child node. +

+ + +
+
+ + + + + + + + + +

+ The add-ons's license that the user has to accept before she can install it. +

+

+ +

+

+ The License text has to be entered as a child of the 'license' node, either as plain text or as HTML. +

+ + +
+ + + + + + + + + + + +

+ The child node contains the add-on configuration as an extension to mindmapmodemenu.xml (in Tools->Preferences->Add-ons). +

+

+ Every property in the configuration should receive a default value in default.properties node. +

+ + +
+
+ + + + + + + + + +

+ These properties are used for: +

+
    +
  • + Each property defined in the preferences should have a default value in the attributes of this node. +
  • +
  • + For each menu item with an icon add an attribute with the icon key (use developer tool menuItemInfo) as key and the icon path as value. Example: '${name}.icon': '/images/${name}-icon.png' +
  • +
+ + +
+
+ + + + + + + + + +

+ The translation keys that this script uses. Define one child node per supported locale. The attributes contain the translations. Define at least +

+
    +
  • + 'addons.${name}' for the add-on's name +
  • +
  • + 'addons.${name}.description' for the description, e.g. in the add-on overview dialog (not necessary for English) +
  • +
  • + 'addons.${name}.<scriptname>' for each script since it will be the menu title. +
  • +
+ + +
+ + + + + +
+ + + + + + + + + +

+ List of files and/or directories to remove on uninstall +

+ + +
+ + +
+ + + + + + + + + +

+ An add-on may contain multiple scripts. The node text defines the script name (e.g. insertInlineImage.groovy). The name must have a suffix of a supported script language like .groovy or .js and may only consist of letters and digits. The script properties have to be configured via attributes: +

+

+ +

+

+ * menuLocation: <locationkey> +

+

+    - Defines the menu location, defaults a sub menu 'main_menu_scripting/addons.${name}'. +

+

+    - Use developer tool menuItemInfo to inspect menu location keys. +

+

+    - This attribute is mandatory +

+

+ +

+

+ * menuTitleKey: <key> +

+

+    - The menu item title will be looked up under the translation key <key> - don't forget to define its translation. +

+

+    - This attribute is mandatory +

+

+ +

+

+ * executionMode: <mode> +

+

+    - The execution mode as described in the Freeplane wiki (http://freeplane.sourceforge.net/wiki/index.php/Scripting) +

+

+    - ON_SINGLE_NODE: Execute the script once. The node variable is set to the selected node. +

+

+    - ON_SELECTED_NODE: Execute the script n times for n selected nodes, once for each node. +

+

+    - ON_SELECTED_NODE_RECURSIVELY: Execute the script on every selected node and recursively on all of its children. +

+

+    - In doubt use ON_SINGLE_NODE. +

+

+    - This attribute is mandatory +

+

+ +

+

+ * keyboardShortcut: <shortcut> +

+

+    - Optional: keyboard combination / accelerator for this script, e.g. control alt I +

+

+    - Use lowercase letters for modifiers and uppercase for letters. Use no + signs. +

+

+    - The available key names are listed at http://download.oracle.com/javase/1.4.2/docs/api/java/awt/event/KeyEvent.html#VK_0 +

+

+      In the list only entries with a 'VK_' prefix count. Omit the prefix in the shortcut definition. +

+

+ +

+

+ * Permissions that the script(s) require, each either false or true: +

+

+    - execute_scripts_without_asking +

+

+    - execute_scripts_without_file_restriction: permission to read files +

+

+    - execute_scripts_without_write_restriction: permission to create/change/delete files +

+

+    - execute_scripts_without_exec_restriction: permission to execute other programs +

+

+    - execute_scripts_without_network_restriction: permission to access the network +

+

+   Notes: +

+

+   - The set of permissions is fixed. +

+

+   - Don't change the attribute names, don't omit one. +

+

+   - Set the values either to true or to false +

+

+   - In any case set execute_scripts_without_asking to true unless you want to annoy users. +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +

+ An add-on may contain any number of nodes containing binary files (normally .jar files) to be added to the add-on's classpath. +

+

+ +

+

+  - The immediate child nodes contain the name of the file, e.g. 'mysql-connector-java-5.1.25.jar'). Put the file into a 'lib' subdirectory of the add-on base directory. +

+

+ +

+

+  - The child nodes of these nodes contain the actual files. +

+

+ +

+

+  - Any lib file will be extracted in <installationbase>/<addonname>/lib. +

+

+ +

+

+  - The files will be processed in the sequence as seen in the map. +

+ + +
+
+ + + + + + + + + +

+ An add-on may contain any number of nodes containing zip files. +

+

+ +

+

+  - The immediate child nodes contain a description of the zip. The devtools script releaseAddOn.groovy allows automatic zip creation if the name of this node matches a directory in the current directory. +

+

+ +

+

+  - The child nodes of these nodes contain the actual zip files. +

+

+ +

+

+  - Any zip file will be extracted in the <installationbase>. Currently, <installationbase> is always Freeplane's <userhome>, e.g. ~/.freeplane/1.3. +

+

+ +

+

+  - The files will be processed in the sequence as seen in the map. +

+ + +
+
+ + + + + + + + + +

+ An add-on may define any number of images as child nodes of the images node. The actual image data has to be placed as base64 encoded binary data into the text of a subnode. +

+

+ The images are saved to the ${installationbase}/resources/images directory. +

+

+ +

+

+ The following images should be present: +

+
    +
  • + ${name}-icon.png, like oldicons-theme-icon.png. This will be used in the app-on overview. +
  • +
  • + ${name}-screenshot-1.png, like oldicons-theme-screenshot-1.png. This will be used in the app-on details dialog. Further images can be included but they are not used yet. +
  • +
+

+ Images can be added automatically by releaseAddOn.groovy or must be uploaded into the map via the script Tools->Scripts->Insert Binary since they have to be (base64) encoded as simple strings. +

+ + +
+
+
+
diff --git a/desktopActions.mm b/desktopActions.mm new file mode 100644 index 0000000..cc9e761 --- /dev/null +++ b/desktopActions.mm @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ The homepage of this add-on should be set as the link of the root node. +

+

+ The basic properties of this add-on. They can be used in script names and other attributes, e.g. "${name}.groovy". +

+
    +
  • + name: The name of the add-on, normally a technically one (no spaces, no special characters except _.-). +
  • +
  • + author: Author's name(s) and (optionally) email adresses. +
  • +
  • + version: Since it's difficult to protect numbers like 1.0 from Freeplane's number parser it's advised to prepend a 'v' to the number, e.g. 'v1.0'. +
  • +
  • + freeplane-version-from: The oldest compatible Freeplane version. The add-on will not be installed if the Freeplane version is too old. +
  • +
  • + freeplane-version-to: Normally empty: The newest compatible Freeplane version. The add-on will not be installed if the Freeplane version is too new. +
  • +
  • + updateUrl: URL of the file containing information (version, download url) on the latest version of this add-on. By default: "${homepage}/version.properties" +
  • +
+ + +
+ + + + + + + + + +

+ Description would be awkward to edit as an attribute. +

+

+ So you have to put the add-on description as a child of the 'description' node. +

+

+ To translate the description you have to define a translation for the key 'addons.${name}.description'. +

+ + +
+ + + + + + + +

+ This add-on provides a meaningful script to open files, directories and URIs using the java.awt.Desktop class: it looks for the associated application registered on the current platform, and launch it to handle a URI or file. +

+ + + +
+
+
+ + + + + + + + + +

+ Change log of this add-on: append one node for each noteworthy version and put the details for each version into a child node. +

+ + +
+
+ + + + + + + + + +

+ The add-ons's license that the user has to accept before she can install it. +

+

+ +

+

+ The License text has to be entered as a child of the 'license' node, either as plain text or as HTML. +

+ + +
+ + + + + + + + + + + +

+ The child node contains the add-on configuration as an extension to mindmapmodemenu.xml (in Tools->Preferences->Add-ons). +

+

+ Every property in the configuration should receive a default value in default.properties node. +

+ + +
+
+ + + + + + + + + +

+ These properties are used for: +

+
    +
  • + Each property defined in the preferences should have a default value in the attributes of this node. +
  • +
  • + For each menu item with an icon add an attribute with the icon key (use developer tool menuItemInfo) as key and the icon path as value. Example: '${name}.icon': '/images/${name}-icon.png' +
  • +
+ + +
+
+ + + + + + + + + +

+ The translation keys that this script uses. Define one child node per supported locale. The attributes contain the translations. Define at least +

+
    +
  • + 'addons.${name}' for the add-on's name +
  • +
  • + 'addons.${name}.description' for the description, e.g. in the add-on overview dialog (not necessary for English) +
  • +
  • + 'addons.${name}.<scriptname>' for each script since it will be the menu title. +
  • +
+ + +
+ + + + + +
+ + + + + + + + + +

+ List of files and/or directories to remove on uninstall +

+ + +
+ + +
+ + + + + + + + + +

+ An add-on may contain multiple scripts. The node text defines the script name (e.g. insertInlineImage.groovy). The name must have a suffix of a supported script language like .groovy or .js and may only consist of letters and digits. The script properties have to be configured via attributes: +

+

+ +

+

+ * menuLocation: <locationkey> +

+

+    - Defines the menu location, defaults a sub menu 'main_menu_scripting/addons.${name}'. +

+

+    - Use developer tool menuItemInfo to inspect menu location keys. +

+

+    - This attribute is mandatory +

+

+ +

+

+ * menuTitleKey: <key> +

+

+    - The menu item title will be looked up under the translation key <key> - don't forget to define its translation. +

+

+    - This attribute is mandatory +

+

+ +

+

+ * executionMode: <mode> +

+

+    - The execution mode as described in the Freeplane wiki (http://freeplane.sourceforge.net/wiki/index.php/Scripting) +

+

+    - ON_SINGLE_NODE: Execute the script once. The node variable is set to the selected node. +

+

+    - ON_SELECTED_NODE: Execute the script n times for n selected nodes, once for each node. +

+

+    - ON_SELECTED_NODE_RECURSIVELY: Execute the script on every selected node and recursively on all of its children. +

+

+    - In doubt use ON_SINGLE_NODE. +

+

+    - This attribute is mandatory +

+

+ +

+

+ * keyboardShortcut: <shortcut> +

+

+    - Optional: keyboard combination / accelerator for this script, e.g. control alt I +

+

+    - Use lowercase letters for modifiers and uppercase for letters. Use no + signs. +

+

+    - The available key names are listed at http://download.oracle.com/javase/1.4.2/docs/api/java/awt/event/KeyEvent.html#VK_0 +

+

+      In the list only entries with a 'VK_' prefix count. Omit the prefix in the shortcut definition. +

+

+ +

+

+ * Permissions that the script(s) require, each either false or true: +

+

+    - execute_scripts_without_asking +

+

+    - execute_scripts_without_file_restriction: permission to read files +

+

+    - execute_scripts_without_write_restriction: permission to create/change/delete files +

+

+    - execute_scripts_without_exec_restriction: permission to execute other programs +

+

+    - execute_scripts_without_network_restriction: permission to access the network +

+

+   Notes: +

+

+   - The set of permissions is fixed. +

+

+   - Don't change the attribute names, don't omit one. +

+

+   - Set the values either to true or to false +

+

+   - In any case set execute_scripts_without_asking to true unless you want to annoy users. +

+ + +
+ + + + + + + + + + + + +
+ + + + + + + + + +

+ An add-on may contain any number of nodes containing binary files (normally .jar files) to be added to the add-on's classpath. +

+

+ +

+

+  - The immediate child nodes contain the name of the file, e.g. 'mysql-connector-java-5.1.25.jar'). Put the file into a 'lib' subdirectory of the add-on base directory. +

+

+ +

+

+  - The child nodes of these nodes contain the actual files. +

+

+ +

+

+  - Any lib file will be extracted in <installationbase>/<addonname>/lib. +

+

+ +

+

+  - The files will be processed in the sequence as seen in the map. +

+ + +
+
+ + + + + + + + + +

+ An add-on may contain any number of nodes containing zip files. +

+

+ +

+

+  - The immediate child nodes contain a description of the zip. The devtools script releaseAddOn.groovy allows automatic zip creation if the name of this node matches a directory in the current directory. +

+

+ +

+

+  - The child nodes of these nodes contain the actual zip files. +

+

+ +

+

+  - Any zip file will be extracted in the <installationbase>. Currently, <installationbase> is always Freeplane's <userhome>, e.g. ~/.freeplane/1.3. +

+

+ +

+

+  - The files will be processed in the sequence as seen in the map. +

+ + +
+
+ + + + + + + + + +

+ An add-on may define any number of images as child nodes of the images node. The actual image data has to be placed as base64 encoded binary data into the text of a subnode. +

+

+ The images are saved to the ${installationbase}/resources/images directory. +

+

+ +

+

+ The following images should be present: +

+
    +
  • + ${name}-icon.png, like oldicons-theme-icon.png. This will be used in the app-on overview. +
  • +
  • + ${name}-screenshot-1.png, like oldicons-theme-screenshot-1.png. This will be used in the app-on details dialog. Further images can be included but they are not used yet. +
  • +
+

+ Images can be added automatically by releaseAddOn.groovy or must be uploaded into the map via the script Tools->Scripts->Insert Binary since they have to be (base64) encoded as simple strings. +

+ + +
+
+
+
diff --git a/scripts/Open.groovy b/scripts/Open.groovy new file mode 100644 index 0000000..cbdf0a4 --- /dev/null +++ b/scripts/Open.groovy @@ -0,0 +1,236 @@ +// @ExecutionModes({ON_SELECTED_NODE}) + +/* + + Open.groovy + + It launches the associated application to open or browse the resource using the java.awt.Desktop class (see https://docs.oracle.com/javase/8/docs/api/java/awt/Desktop). It handles files and directories by their path (being absolute or relative to the directory containing the map file) and URIs. + + */ + +package giulioscattolin.freeplane.desktopactions + +import java.awt.Desktop +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import org.freeplane.plugin.script.FreeplaneScriptBaseClass + +class Open extends FreeplaneScriptBaseClass { + def run() { + // Define flags for action support. + def isOpenActionSupportedFlag = false + def isBrowseActionSupportedFlag = false + + // Obtain the desktop instance. + def desktop = obtainDesktopInstance() + // If desktop is an invalid reference.. + if (desktop == null) { + // Quit. + return + } + + // If the OPEN action is supported.. + if (isOpenActionSupported(desktop)) { + // Activate the appropriate flag. + isOpenActionSupportedFlag = true + + // If the resource has been opened successfully using the content of the selected node as an absolute path.. + if (openResourceUsingAbsolutePath(node.text, desktop)) { + // Quit. + return + } + + // If the resource has been opened successfully using the content of the selected node as a relative path to the directory containing the map file.. + if (openResourceUsingPathRelativeToMapFileDirectory(node.text, desktop)) { + // Quit. + return + } + } + // If the BROWSE action is supported.. + if (isBrowseActionSupported(desktop)) { + // Activate the appropriate flag. + isBrowseActionSupportedFlag = true + + // If the resource has been opened successfully using the content of the selected node as an URI.. + if (openResourceUsingURI(node.text, desktop)) { + // Quit. + return + } + } + + if (isOpenActionSupportedFlag && isBrowseActionSupportedFlag) { + // There are no more options, the resource won't be opened. + // Inform the user. + setStatusInfo("The resource cannot be opened because the node does not contain the path of an existent file / directory or a valid URI.") + // Finally, quit. + } + + // The user should have already been informed, quit. + } + + def obtainDesktopInstance() { + // If the Desktop object is supported.. + if (Desktop.isDesktopSupported()) { + // Return the Desktop instance. + return Desktop.getDesktop() + } else { + // Inform the user. + setStatusInfo("The resource cannot be opened because the Desktop class is not supported on the current platform.") + // Finally, report error. + return null + } + } + + def isOpenActionSupported(desktop) { + // If the OPEN action is supported.. + if (desktop.isSupported(Desktop.Action.OPEN)) { + // Return true + return true + } else { + // Inform the user. + setStatusInfo("The resource cannot be opened because the OPEN action is not supported on the current platform.") + // Finally, return false. + return false + } + } + + def isBrowseActionSupported(desktop) { + // If the BROWSE action is supported.. + if (desktop.isSupported(Desktop.Action.BROWSE)) { + // Return true + return true + } else { + // Inform the user. + setStatusInfo("The resource cannot be opened because the BROWSE action is not supported on the current platform.") + // Finally, return false. + return false + } + } + + def openResourceUsingAbsolutePath(nodeText, desktop) { + // Supposing the content of the selected node is the absolute path of a resource, create a Path instance from its string representation. + def path = Paths.get(nodeText) + // Obtain the associated file instance. + def file = path.toFile() + // If the file exists.. + if (file.exists()) { + // Inform the user. + setStatusInfo("Opening the resource using its absolute path..") + + // If the file is opened successfully.. + if (openFile(file, desktop)) { + // Report success. + return true + } else { + // Report insuccess. + return false + } + } else { + // Report insuccess. + return false + } + } + + def openResourceUsingPathRelativeToMapFileDirectory(nodeText, desktop) { + // Supposing the content of the selected node is the path of a resource relative to the directory containing the map file, obtain the parent directory as a file. + def parentDirectory = node.map.file.getParentFile() + // Create the appropriate File instance. + File file = new File(parentDirectory, node.text) + // If the file exists.. + if (file.exists()) { + // Inform the user. + setStatusInfo("Opening the resource using the path relative to map file directory..") + + // If the file is opened successfully.. + if (openFile(file, desktop)) { + // Report success. + return true + } else { + // Report insuccess. + return false + } + } else { + // Report insuccess. + return false + } + } + + def openFile(file, desktop) { + // Try to open the file.. + try { + desktop.open(file) + } catch (IOException e) { + // If the specified file has no associated application or the associated application fails to be launched, inform the user. + setStatusInfo("The resource cannot be opened because it has no associated application or the associated application failed to be launched.") + + // Report insuccess. + return false + } catch (SecurityException e) { + // If a security manager exists and its SecurityManager.checkRead (java.lang.String) method denies read access to the file, or it denies the AWTPermission("showWindowWithoutWarningBanner") permission, or the calling thread is not allowed to create a subprocess. + setStatusInfo("The resource cannot be opened because a security exception has been thrown.") + + // Report insuccess. + return false + } + + // Inform the user. + setStatusInfo("The resource has been successfully opened!") + // Report success. + return true + } + + def openResourceUsingURI(nodeText, desktop) { + // Supposing the content of the selected node is the URI of the resource, tries to create an URI instance. + try { + def uri = new URI(nodeText) + + // If the uri is browsed successfully.. + if (browseUri(uri, desktop)) { + // Report success. + return true + } + } catch (URISyntaxException e) { + // The given string violates RFC 2396. + // Ignore and continue. + } + + // Report insuccess. + return false + } + + def browseUri(uri, desktop) { + // Try to open the file.. + try { + desktop.browse(uri) + } catch (IOException e) { + // If the user default browser is not found, or it fails to be launched, or the default handler application failed to be launched, inform the user. + setStatusInfo("The resource cannot be opened because the user default browser is not found, or it fails to be launched, or the default handler application failed to be launched.") + + // Report insuccess. + return false + } catch (SecurityException e) { + // If a security manager exists and its SecurityManager.checkRead (java.lang.String) method denies read access to the file, or it denies the AWTPermission("showWindowWithoutWarningBanner") permission, or the calling thread is not allowed to create a subprocess. + setStatusInfo("The resource cannot be opened because a security exception has been thrown.") + + // Report insuccess. + return false + } catch (IllegalArgumentException e) { + // If the necessary permissions are not available and the URI can not be converted to a URL. + setStatusInfo("The resource cannot be opened because the necessary permissions are not available and the URI can not be converted to an URL.") + + // Report insuccess. + return false + } + + // Inform the user. + setStatusInfo("The resource has been successfully browsed!") + // Report success. + return true + } + + def setStatusInfo(text) { + c.setStatusInfo(text) + } +} + diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..cc76b4f --- /dev/null +++ b/version.properties @@ -0,0 +1,3 @@ +version=v0.1 +downloadUrl=https://github.com/giulioscattolin/freeplane-desktop-actions-addon/desktopActions-v0.1.addon.mm +freeplaneVersionFrom=1.7.2