/jEdit/tags/jedit-4-5-pre1/doc/users-guide/plugin-implement.xml
XML | 1250 lines | 1027 code | 207 blank | 16 comment | 0 complexity | 21f5d6d6a09114153c2b06f452e1a5b7 MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, Apache-2.0, LGPL-2.0, LGPL-3.0, GPL-2.0, CC-BY-SA-3.0, LGPL-2.1, GPL-3.0, MPL-2.0-no-copyleft-exception, IPL-1.0
- <?xml version="1.0" encoding="UTF-8"?>
- <chapter id="plugin-implement">
- <!-- jEdit buffer-local properties: -->
- <!-- :xml.root=users-guide.xml: -->
- <!-- :maxLineLen=80:wrap=soft: -->
- <title>Implementing a Simple Plugin</title>
- <!-- jEdit 4 Plugin Guide, (C) 2001, 2002 John Gellene -->
- <!-- jEdit 4.5 Plugin Guide, (C) 2005, 2011 Alan Ezust -->
- <para>There are many applications for the leading operating systems that
- provide a <quote>scratch-pad</quote> or <quote>sticky note</quote> facility
- for the desktop display. A similar type of facility operating within the
- jEdit display would be a convenience. The use of dockable windows would
- allow the notepad to be displayed or hidden with a single mouse click or
- keypress (if a keyboard shortcut were defined). The contents of the notepad
- could be saved at program exit (or, if earlier, deactivation of the plugin)
- and retrieved at program startup or plugin activation.</para>
- <para>We will keep the capabilities of this plugin modest, but a few other
- features would be worthwhile. The user should be able to write the contents
- of the notepad to storage on demand. It should also be possible to choose
- the name and location of the file that will be used to hold the notepad
- text. This would allow the user to load other files into the notepad
- display. The path of the notepad file should be displayed in the plugin
- window, but will give the user the option to hide the file name. Finally,
- there should be an action by which a single click or keypress would cause
- the contents of the notepad to be written to the new text buffer for further
- processing.</para>
- <para>The full source code for QuickNotepad is contained in jEdit's source
- code distribution. We will provide excerpts in this discussion where it is
- helpful to illustrate specific points. You are invited to obtain the source
- code for further study or to use as a starting point for your own
- plugin.</para>
- <section id="plugin-load">
- <title><indexterm>
- <primary>Plugin API</primary>
- <secondary>loading at startup</secondary>
- </indexterm> How Plugins are Loaded</title>
- <para>We will discuss the implementation of the
- <application>QuickNotepad</application> plugin, along with the jEdit
- APIs it makes use of. But first, we describe how plugins are
- loaded.</para>
- <para>As part of its startup routine, jEdit's <function>main</function>
- method calls various methods to load and initialize plugins.</para>
- <para>Additionally, plugins using the jEdit 4.2 plugin API can be loaded
- and unloaded at any time. This is a great help when developing your own
- plugins -- there is no need to restart the editor after making changes
- (see <xref linkend="plugin-implement-reloading" /> ).</para>
- <para>Plugins are loaded from files with the <filename>.jar</filename>
- filename extension located in the <filename>jars</filename>
- subdirectories of the jEdit installation and user settings directories
- (see <xref linkend="settings-directory" />).</para>
- <para>For each JAR archive file it finds, jEdit scans its entries and
- performs the following tasks:</para>
- <itemizedlist>
- <listitem>
- <para>Adds to a collection maintained by jEdit a new object of
- type <ulink url="../api/org/gjt/sp/jedit/PluginJAR.html">
- <classname>PluginJAR</classname></ulink>. This is a data
- structure holding the name of the JAR archive file, a reference
- to the <ulink url="../api/org/gjt/sp/jedit/JARClassLoader.html">
- <classname>JARClassLoader</classname></ulink>, and a collection
- of plugins found in the archive file.</para>
- </listitem>
- <listitem>
- <para>Loads any properties defined in files ending with the
- extension <filename>.props</filename> that are contained in the
- archive. See <xref
- linkend="plugin-implement-properties" />.</para>
- </listitem>
- <listitem>
- <para>Reads action definitions from any file named
- <filename>actions.xml</filename> in the archive (the file need
- not be at the top level). See <xref
- linkend="plugin-implement-actions" />.</para>
- </listitem>
- <listitem>
- <para>Parses and loads the contents of any file named
- <filename>dockables.xml</filename> in the archive (the file need
- not be at the top level). This file contains BeanShell code for
- creating docking or floating windows that will contain the
- visible components of the plugin. Not all plugins define
- dockable windows, but those that do need a
- <filename>dockables.xml</filename> file. See <xref
- linkend="plugin-implement-dockables" />.</para>
- </listitem>
- <listitem>
- <para>Checks for a class name with a name ending with
- <filename>Plugin.class</filename>.</para>
- <para>Such a class is known as a <firstterm>plugin core
- class</firstterm> and must extend jEdit's abstract <ulink
- url="../api/org/gjt/sp/jedit/EditPlugin.html">
- <classname>EditPlugin</classname></ulink> class.</para>
- <para>The initialization routine checks the plugin's properties
- to see if it is subject to any dependencies. For example, a
- plugin may require that the version of the Java runtime
- environment or of jEdit itself be equal to or above some
- threshold version. A plugin can also require the presence of
- another plugin.</para>
- <para>If any dependency is not satisfied, the loader marks the
- plugin as <quote>broken</quote> and logs an error
- message.</para>
- </listitem>
- </itemizedlist>
- <para>After scanning the plugin JAR file and loading any resources, a
- new instance of the plugin core class is created and added to the
- collection maintained by the appropriate <ulink
- url="../api/org/gjt/sp/jedit/PluginJAR.html">
- <classname>PluginJAR</classname></ulink>. jEdit then calls the
- <function>start()</function> method of the plugin core class. The
- <function>start()</function> method can perform initialization of the
- object's data members. Because this method is defined as an empty
- <quote>no-op</quote> in the <ulink
- url="../api/org/gjt/sp/jedit/EditPlugin.html">
- <classname>EditPlugin</classname></ulink> abstract class, a plugin need
- not provide an implementation if no unique initialization is
- required.</para>
- </section>
- <section id="plugin-implement-quicknotepadplugin">
- <title>The QuickNotepadPlugin Class</title>
- <para>The major issues encountered when writing a plugin core class
- arise from the developer's decisions on what features the plugin will
- make available. These issues have implications for other plugin elements
- as well.</para>
- <itemizedlist>
- <listitem>
- <para>Will the plugin provide for actions that the user can
- trigger using jEdit's menu items, toolbar buttons and keyboard
- shortcuts?</para>
- </listitem>
- <listitem>
- <para>Will the plugin have its own visible interface?</para>
- </listitem>
- <listitem>
- <para>Will the plugin have settings that the user can
- configure?</para>
- </listitem>
- <listitem>
- <para>Will the plugin respond to any messages reflecting changes
- in the host application's state?</para>
- </listitem>
- <listitem>
- <para>Should the plugin do something special when it gets
- focus?</para>
- </listitem>
- </itemizedlist>
- <para>Recall that the plugin core class must extend <ulink
- url="../api/org/gjt/sp/jedit/EditPlugin.html">
- <classname>EditPlugin</classname></ulink>. In QuickNotepad's plugin core
- class, there are no special initialization or shutdown chores to
- perform, so we will not need a <function>start()</function> or
- <function>stop()</function> method.</para>
- <para>The resulting plugin core class is lightweight and straightforward
- to implement:</para>
- <itemizedlist>
- <listitem>
- <informalexample>
- <programlisting>public class QuickNotepadPlugin extends EditPlugin {
- public static final String NAME = "quicknotepad";
- public static final String OPTION_PREFIX = "options.quicknotepad.";
- }
- </programlisting>
- </informalexample>
- <para>The class has been simplified since 4.1, and all we
- defined here were a couple of <classname>String</classname> data
- members to enforce consistent syntax for the name of properties
- we will use throughout the plugin.</para>
- </listitem>
- <listitem>
- <para>These names are used in <filename>actions.xml</filename>
- for each of the menu choices. This file is discussed in more
- detail in <xref linkend="plugin-implement-actions" />. Each
- action is a beanshell script.</para>
- <informalexample>
- <programlisting>
- <!DOCTYPE ACTIONS SYSTEM "actions.dtd">
- <ACTIONS>
- <ACTION NAME="quicknotepad.choose-file">
- <CODE>
- wm.addDockableWindow(QuickNotepadPlugin.NAME);
- wm.getDockableWindow(QuickNotepadPlugin.NAME).chooseFile();
- </CODE>
- </ACTION>
- <ACTION NAME="quicknotepad.save-file">
- <CODE>
- wm.addDockableWindow(QuickNotepadPlugin.NAME);
- wm.getDockableWindow(QuickNotepadPlugin.NAME).saveFile();
- </CODE>
- </ACTION>
- <ACTION NAME="quicknotepad.copy-to-buffer">
- <CODE>
- wm.addDockableWindow(QuickNotepadPlugin.NAME);
- wm.getDockableWindow(QuickNotepadPlugin.NAME).copyToBuffer();
- </CODE>
- </ACTION>
- </ACTIONS>
- </programlisting>
- </informalexample>
- </listitem>
- <listitem>
- <para>The names also come up in the properties file,
- <filename>QuickNotePad.props</filename> file. The properties
- define option panes and strings used by the plugin. It is
- explained in more detail in <xref
- linkend="plugin-implement-properties" /> and the <ulink
- url="../api/org/gjt/sp/jedit/EditPlugin.html">
- <classname>EditPlugin</classname></ulink> API docs.</para>
- <informalexample>
- <programlisting>
- # jEdit only needs to load the plugin the first time the user accesses it
- # the presence of this property also tells jEdit the plugin is using the new API
- plugin.QuickNotepadPlugin.activate=defer
- # These two properties are required for all plugins
- plugin.QuickNotepadPlugin.name=QuickNotepad
- plugin.QuickNotepadPlugin.author=John Gellene
- # version number == jEdit version number
- plugin.QuickNotepadPlugin.version=4.4
- # online help
- plugin.QuickNotepadPlugin.docs=index.html
- # we only have one dependency, jEdit 4.4.1
- plugin.QuickNotepadPlugin.depend.0=jedit 04.04.99.01
- # plugin menu
- plugin.QuickNotepadPlugin.menu=quicknotepad \
- - \
- quicknotepad.choose-file \
- quicknotepad.save-file \
- quicknotepad.copy-to-buffer
- # action labels for actions supplied by dockables.xml
- quicknotepad.label=QuickNotepad
- # action labels for actions supplied by actions.xml
- quicknotepad.choose-file.label=Choose notepad file
- quicknotepad.save-file.label=Save notepad file
- quicknotepad.copy-to-buffer.label=Copy notepad to buffer
- # plugin option pane
- plugin.QuickNotepadPlugin.option-pane=quicknotepad
- # Option pane activation BeanShell snippet
- options.quicknotepad.code=new QuickNotepadOptionPane();
- # Option pane labels
- options.quicknotepad.label=QuickNotepad
- options.quicknotepad.file=File:
- options.quicknotepad.choose-file=Choose
- options.quicknotepad.choose-file.title=Choose a notepad file
- options.quicknotepad.choose-font=Font:
- options.quicknotepad.show-filepath.title=Display notepad file path
- # window title
- quicknotepad.title=QuickNotepad
- # window toolbar buttons
- quicknotepad.choose-file.icon=Open.png
- quicknotepad.save-file.icon=Save.png
- quicknotepad.copy-to-buffer.icon=CopyToBuffer.png
- # default settings
- options.quicknotepad.show-filepath=true
- options.quicknotepad.font=Monospaced
- options.quicknotepad.fontstyle=0
- options.quicknotepad.fontsize=14
- # Setting not defined but supplied for completeness
- options.quicknotepad.filepath=
- </programlisting>
- </informalexample>
- </listitem>
- </itemizedlist>
- </section>
- <section id="plugin-implement-properties">
- <title>The Property File</title>
- <para>jEdit maintains a list of <quote>properties</quote>, which are
- name/value pairs used to store human-readable strings, user settings,
- and various other forms of meta-data. During startup, jEdit loads the
- default set of properties, followed by plugin properties stored in
- plugin JAR files, finally followed by user properties.</para>
- <para>Some properties are used by the plugin API itself. Others are
- accessed by the plugin using methods in the <ulink
- url="../api/org/gjt/sp/jedit/jEdit.html">
- <classname>jEdit</classname></ulink> class. Others are accessed by the
- scripts used by plugin packagers <footnote>
- <para>See the <guimenuitem>Macros/Properties/Create Plugin
- Announcement</guimenuitem> macro for an example.</para>
- </footnote>.</para>
- <para>Property files contained in plugin JARs must end with the filename
- extension <filename>.props</filename>, and have a very simple syntax,
- which the following example illustrates:</para>
- <informalexample>
- <programlisting># Lines starting with '#' are ignored.
- name=value
- another.name=another value
- long.property=Long property value, split over \
- several lines
- escape.property=Newlines and tabs can be inserted \
- using the \t and \n escapes
- backslash.property=A backslash can be inserted by writing \\.</programlisting>
- </informalexample>
- <para>Now we look at a fragment from the
- <filename>QuickNotepad.props</filename> file <footnote>
- <para>Examine the actual file for a more complete example</para>
- </footnote> which contains properties for the QuickNotepad plugin.
- The first type of property data is information about the plugin itself;
- these are the only properties that must be specified in order for the
- plugin to load:</para>
- <informalexample>
- <programlisting># general plugin information
- plugin.QuickNotepadPlugin.activate=defer
- plugin.QuickNotepadPlugin.name=QuickNotepad
- plugin.QuickNotepadPlugin.author=John Gellene
- plugin.QuickNotepadPlugin.version=4.03
- plugin.QuickNotepadPlugin.docs=QuickNotepad.html
- # depends on jEdit 4.4.1
- plugin.QuickNotepadPlugin.depend.0=jedit 04.04.99.01
- plugin.QuickNotepadPlugin.description=A demo jEdit plugin that provides a notepad dockable.
- plugin.QuickNotepadPlugin.longdescription=description.html
- </programlisting>
- </informalexample>
- <para>These properties are each described in detail in the documentation
- for the <ulink url="../api/org/gjt/sp/jedit/EditPlugin.html">
- <classname>EditPlugin</classname></ulink> class and do not require
- further discussion here.</para>
- <para>Next in the file comes a property that sets the title of the
- plugin's dockable window. Dockable windows are discussed in detail in
- <xref linkend="plugin-implement-dockables" />.</para>
- <informalexample>
- <programlisting># dockable window name
- quicknotepad.title=QuickNotepad</programlisting>
- </informalexample>
- <para>Next, we see menu item labels for the plugin's actions. All of
- these but the first are defined in <literal>actions.xml</literal> file,
- and that is because the dockable itself has its own actions. Actions are
- discussed further in <xref linkend="plugin-implement-actions" />.</para>
- <informalexample>
- <programlisting># action labels
- # Dockable label
- quicknotepad.label=QuickNotepad
- # Additional strings extracted from the plugin java source
- quicknotepad.choose-file.label=Choose notepad file
- quicknotepad.save-file.label=Save notepad file
- quicknotepad.copy-to-buffer.label=Copy notepad to buffer
- </programlisting>
- </informalexample>
- <para>Next, the plugin's menu is defined. See <xref
- linkend="plugin-implement-quicknotepadplugin" />.</para>
- <informalexample>
- <programlisting># application menu items
- quicknotepad.menu.label=QuickNotepad
- quicknotepad.menu=quicknotepad - quicknotepad.choose-file \
- quicknotepad.save-file quicknotepad.copy-to-buffer</programlisting>
- </informalexample>
- <para>We have created a small toolbar as a component of QuickNotepad, so
- file names for the button icons follow:</para>
- <informalexample>
- <programlisting># plugin toolbar buttons
- quicknotepad.choose-file.icon=Open.png
- quicknotepad.save-file.icon=Save.png
- quicknotepad.copy-to-buffer.icon=Edit.png</programlisting>
- </informalexample>
- <para>The menu item labels corresponding to these icons will also serve
- as tooltip text.</para>
- <para>Finally, the properties file set forth the labels and settings
- used by the option pane:</para>
- <informalexample>
- <programlisting># Option pane labels
- options.quicknotepad.label=QuickNotepad
- options.quicknotepad.file=File:
- options.quicknotepad.choose-file=Choose
- options.quicknotepad.choose-file.title=Choose a notepad file
- options.quicknotepad.choose-font=Font:
- options.quicknotepad.show-filepath.title=Display notepad file path
- # Initial default font settings
- options.quicknotepad.show-filepath=true
- options.quicknotepad.font=Monospaced
- options.quicknotepad.fontstyle=0
- options.quicknotepad.fontsize=14
- # Setting not defined but supplied for completeness
- options.quicknotepad.filepath=</programlisting>
- </informalexample>
- <tip>
- <title>PropertySideKick</title>
- <para>There is a SideKick for Property files, provided in the
- JavaSideKick plugin. This gives you a compact and sorted tree view
- of property files.</para>
- </tip>
- </section>
- <section id="plugin-implement-editbus">
- <title>The EditBus</title>
- <para>jEdit (and some plugins) generate several kinds of messages to
- alert plugins and other components of jedit-specific events. The message
- classes, all derived from <ulink
- url="../api/org/gjt/sp/jedit/EBMessage.html">
- <classname>EBMessage</classname></ulink> cover the opening and closing
- of the application, changes in the status of buffers and views, changes
- in user settings, as well as changes in the state of other program
- features. A full list of messages can be found in the <ulink
- url="../api/org/gjt/sp/jedit/msg/package-summary.html">org.gjt.sp.jedit.msg</ulink>
- package.</para>
- <para>For example, the ViewUpdate messages are all related to the jEdit
- View, or the top-level window. If the user creates multiple Views, a
- plugin may need to know when they are created or destroyed, so it would
- monitor ViewUpdate messages.</para>
- <para>BufferUpdate messages are all related to jEdit buffers. They let
- plugins know when a buffer has become dirty, when it is about to be
- closed, after it is closed, created, loaded, or saved. Each of these
- messages are described in further detail in the API docs.</para>
- <para>As another example, The Navigator plugin monitors a newly added
- (to jEdit 4.3) EBMessage of the kind <ulink
- url="../api/org/gjt/sp/jedit/BufferChanging.html">BufferChanging</ulink>.
- The BufferChanging event provides Navigator enough advance notice to
- save the TextArea's caret just before the current EditPane changes its
- active Buffer. The <literal>BufferChanged</literal> event, another
- <literal>EditPaneUpdate</literal> message, is thrown shortly afterward.
- This is not used by Navigator, but it is used by SideKick to determine
- when it is time to reparse the buffer.</para>
- <para>Plugins register <ulink
- url="../api/org/gjt/sp/jedit/EBComponent.html">
- <function>EBComponent</function></ulink> instances with the <ulink
- url="../api/org/gjt/sp/jedit/EditBus.html">
- <classname>EditBus</classname></ulink> to receive messages reflecting
- changes in jEdit's state.</para>
- <para><ulink url="../api/org/gjt/sp/jedit/EBComponent.html">
- <function>EBComponent</function></ulink>s are added and removed with the
- <ulink
- url="../api/org/gjt/sp/jedit/EditBus.html#addToBus(org.gjt.sp.jedit.EBComponent)">
- <function>EditBus.addToBus()</function></ulink> and <ulink
- url="../api/org/gjt/sp/jedit/EditBus.html#removeFromBus(org.gjt.sp.jedit.EBComponent)">
- <function>EditBus.removeFromBus()</function></ulink> methods.</para>
- <para>Typically, the <ulink
- url="../api/org/gjt/sp/jedit/EBComponent.html#handleMessage(org.gjt.sp.jedit.EBMessage)">
- <function>EBComponent.handleMessage()</function></ulink> method is
- implemented with one or more <function>if</function> blocks that test
- whether the message is an instance of a derived message class in which
- the component has an interest.</para>
- <programlisting>if(msg instanceof BufferUpdate) {
- // a buffer's state has changed!
- }
- else if(msg instanceof ViewUpdate) {
- // a view's state has changed!
- }
- // ... and so on</programlisting>
- <para>If a plugin core class will respond to EditBus messages, it can be
- derived from <ulink url="../api/org/gjt/sp/jedit/EBPlugin.html">
- <classname>EBPlugin</classname></ulink>, in which case no explicit
- <function>addToBus()</function> call is necessary. Otherwise, <ulink
- url="../api/org/gjt/sp/jedit/EditPlugin.html">
- <classname>EditPlugin</classname></ulink> will suffice as a plugin base
- class. Note that QuickNotepad uses the latter.</para>
- <tip>
- <title>Using the Activity Log to see the EditBus</title>
- <para> To determine precisely which EditBus messages are being sent by
- jEdit or the plugins, start up jEdit with an additional argument,
- <literal>-log=5</literal>. You can set an even lower log level to see
- further details (the default is 7). With a log level of 5 or lower, the
- Activity Log will include [notice]s, which will show us exactly which
- EditBus message is sent and when. See <xref linkend="activity-log" />
- for more details. </para> </tip>
- </section>
- <section id="plugin-implement-actions">
- <title>The Actions Catalog</title>
- <para>Actions define procedures that can be bound to a menu item, a
- toolbar button or a keyboard shortcut. Most plugin Actions <footnote>
- <para>Some plugins, such as Sidekick, Console, and
- ProjectViewer, create pure Java EditAction-derived Actions,
- based which services are available, or which files are found in
- a certain path. However, this is an advanced topic you can
- explore further in the source and API docs of those
- plugins.</para>
- </footnote> are short scripts written in BeanShell, jEdit's macro
- scripting language. These scripts either direct the action themselves,
- delegate to a method in one of the plugin's classes that encapsulates
- the action, or do a little of both. The scripts are usually short;
- elaborate action protocols are usually contained in compiled code,
- rather than an interpreted macro script, to speed execution.</para>
- <para>Actions are defined by creating an XML file entitled
- <filename>actions.xml</filename> and placing it in the plugin JAR
- file.</para>
- <para>The <filename>actions.xml</filename> file from the
- <application>QuickNotepad</application> plugin looks as follows:</para>
- <informalexample>
- <programlisting><ACTIONS>
- <ACTION NAME="quicknotepad.choose-file">
- <CODE>
- wm.addDockableWindow(QuickNotepadPlugin.NAME);
- wm.getDockableWindow(QuickNotepadPlugin.NAME).chooseFile();
- </CODE>
- </ACTION>
- <ACTION NAME="quicknotepad.save-file">
- <CODE>
- wm.addDockableWindow(QuickNotepadPlugin.NAME);
- wm.getDockableWindow(QuickNotepadPlugin.NAME).saveFile();
- </CODE>
- </ACTION>
- <ACTION NAME="quicknotepad.copy-to-buffer">
- <CODE>
- wm.addDockableWindow(QuickNotepadPlugin.NAME);
- wm.getDockableWindow(QuickNotepadPlugin.NAME).copyToBuffer();
- </CODE>
- </ACTION>
- </ACTIONS></programlisting>
- </informalexample>
- <para>This file defines three actions. They each use a built-in variable
- <literal>wm</literal>, which refers to the current view's <ulink
- url="../api/org/gjt/sp/jedit/gui/DockableWindowManager.html">
- <classname>DockableWindowManager</classname></ulink>. Whenever you need
- to obtain a reference to the current dockable, or create a new one, this
- is the class to use. We use the method <filename>addDockable() followed
- by getDockable()</filename> to create if necessary, and then bring up
- the QuickNotepad plugin dockable. This will be docked or floating,
- depending on how it was last used.</para>
- <para>When an action is invoked, the BeanShell scripts address the
- plugin through static methods, or if instance data is needed, the
- current <ulink url="../api/org/gjt/sp/jedit/View.html">
- <classname>View</classname></ulink>, its <ulink
- url="../api/org/gjt/sp/jedit/gui/DockableWindowManager.html">
- <classname>DockableWindowManager</classname></ulink>, and the plugin
- object return by the <filename>getDockable()</filename> method.</para>
- <para>If you are unfamiliar with BeanShell code, you may nevertheless
- notice that the code statements bear a strong resemblance to Java code,
- with one exception: the variable <varname>view</varname> is never
- assigned any value.</para>
- <para>For complete answers to this and other BeanShell mysteries, see
- <xref linkend="writing-macros-part" />; two observations will suffice
- here. First, the variable <varname>view</varname> is predefined by
- jEdit's implementation of BeanShell to refer to the current
- <classname>View</classname> object. Second, the BeanShell scripting
- language is based upon Java syntax, but allows variables to be typed at
- run time, so explicit types for variables need not be declared.</para>
- <para>A formal description of each element of the
- <filename>actions.xml</filename> file can be found in the documentation
- of the <ulink url="../api/org/gjt/sp/jedit/ActionSet.html">
- <classname>ActionSet</classname></ulink> class.</para>
- </section>
- <section id="plugin-implement-dockables">
- <title>The dockables.xml Window Catalog</title>
- <para>A Dockable is a window that can float like a dialog, or dock into
- jEdit's docking area. Each dockable needs a label (for display in menus,
- and on small buttons) and a title (for display in the floating window's
- title bar).</para>
- <para>The jEdit plugin API uses BeanShell to create the top-level
- visible container of a plugin's interface. The BeanShell code is
- contained in a file named <filename>dockables.xml</filename>. It usually
- is quite short, providing only a single BeanShell expression used to
- create a visible plugin window.</para>
- <para>The following example from the QuickNotepad plugin illustrates the
- requirements of the data file:</para>
- <informalexample>
- <programlisting><?xml version="1.0"?>
- <!DOCTYPE DOCKABLES SYSTEM "dockables.dtd">
- <DOCKABLES>
- <DOCKABLE NAME="quicknotepad">
- new QuickNotepad(view, position);
- </DOCKABLE>
- </DOCKABLES></programlisting>
- </informalexample>
- <para>In this example, the <classname><DOCKABLE></classname>
- element has a single attribute, the dockable window's identifier. This
- attribute is used to key a property where the window title is stored;
- see <xref linkend="plugin-implement-properties" />.</para>
- <para>For each dockable, jedit defines an action with the same name.
- This means you do not need to define an explicit action to create your
- dockable - in fact, jEdit defines three actions: "toggle", "get" and
- "new floating instance" for each.</para>
- <para>The contents of the <classname><DOCKABLE></classname>
- element itself is a BeanShell expression that constructs a new
- <classname>QuickNotepad</classname> object. The <varname>view</varname>
- and <varname>position</varname> are predefined by the plugin API as the
- view in which the plugin window will reside, and the docking position of
- the plugin. You can use <varname>position</varname> to customize the
- layout of your plugin depending on whether it appears on the sides, or
- the top/bottom, or as a floating dockable.</para>
- <para>A formal description of each element of the
- <filename>dockables.xml</filename> file can be found in the
- documentation of the <ulink
- url="../api/org/gjt/sp/jedit/gui/DockableWindowManager.html">
- <classname>DockableWindowManager</classname></ulink> class. This class
- also contains the public interface you should use for getting, showing,
- hiding, and other interactions with the plugin's top-level
- windows.</para>
- </section>
- <section id="plugin-implement-services">
- <title>The services.xml file</title>
- <para>A "service" is a mechanism by which one plugin can work with other
- plugins and avoid a bidirectional build-dependency. For example, the XML
- plugin "depends" on Sidekick, but in fact, it is SideKick which creates
- and operates on an object (a <literal>SideKickParser</literal>, in fact)
- defined in the XML plugin. In a way, the dependency is
- bidirectional.</para>
- <para>Similarly, the AntFarm plugin defines but does not instantiate a
- <literal>Shell</literal> object. It is the Console plugin which creates
- a specific shell for each available service. SideKick and Console use
- the ServiceManager to search for services offered by other
- plugins.</para>
- <para>Here is an example of a service from the XML plugin, which extends
- Sidekick:</para>
- <informalexample>
- <programlisting>
- <!DOCTYPE SERVICES SYSTEM "services.dtd">
- <SERVICES>
- <SERVICE CLASS="sidekick.SideKickParser" NAME="xml">
- new xml.parser.SAXParserImpl();
- </SERVICE>
- </SERVICES>
- </programlisting>
- </informalexample>
- <para>The object it returns tells Sidekick how it can parse files of a
- specific type. The API docs for SideKickParser indicate exactly which
- methods must be implemented in a plugin which offers this service. It
- should be enough information to let Sidekick, which has its own
- dockable, display the tree information in its own view.</para>
- <para>For more information about services, refer to the <ulink
- url="../api/org/gjt/sp/jedit/ServiceManager.html">ServiceManager</ulink>
- class API documentation. There, you can find out what the tags and
- attributes mean, as well as how to register and use services.</para>
- </section>
- <section id="plugin-implement-quicknotepad">
- <title>The QuickNotepad Class</title>
- <para>Here is where most of the features of the plugin will be
- implemented. To work with the dockable window API, the top level window
- will be a <classname>JPanel</classname>. The visible components reflect
- a simple layout. Inside the top-level panel we will place a scroll pane
- with a text area. Above the scroll pane we will place a panel containing
- a small tool bar and a label displaying the path of the current notepad
- file.</para>
- <para>We have identified three user actions that need implementation
- here: <function>chooseFile()</function>,
- <function>saveFile()</function>, and
- <function>copyToBuffer()</function>. As noted earlier, we also want the
- text area to change its appearance in immediate response to a change in
- user options settings. In order to do that, the window class must
- respond to a <classname>PropertiesChanged</classname> message from the
- EditBus.</para>
- <!-- <para>
- We could have the plugin core class receive and delegate
- <classname>PropertiesChanged</classname> messages to the window class.
- However, this would require the plugin core class to hold a reference
- to either the plugin window class or the visible window class and to
- update that reference when the user activates or deactivates the
- plugin. It is simpler to have the plugin window class subscribe to the
- EditBus directly; many plugins take this approach. This means that
- <classname>QuickNotepad</classname> must implement the
- <classname>EBComponent</classname> interface.
- </para> -->
- <para>Unlike the <classname>EBPlugin</classname> class, the
- <classname>EBComponent</classname> interface does not deal with the
- component's actual subscribing and unsubscribing to the EditBus. To
- accomplish this, we use a pair of methods inherited from the Java
- platform's <classname>JComponent</classname> class that are called when
- the window is made visible, and when it is hidden. These two methods,
- <function>addNotify()</function> and
- <function>removeNotify()</function>, are overridden to add and remove
- the visible window from the list of EditBus subscribers.</para>
- <para>We will provide for two minor features when the notepad is
- displayed in the floating window. First, when a floating plugin window
- is created, we will give the notepad text area input focus. Second, when
- the notepad if floating and has input focus, we will have the
- <keycap>Escape</keycap> key dismiss the notepad window. An
- <classname>AncestorListener</classname> and a
- <classname>KeyListener</classname> will implement these details.</para>
- <para>Here is the listing for the data members, the constructor, and the
- implementation of the <classname>EBComponent</classname>
- interface:</para>
- <informalexample>
- <programlisting>public class QuickNotepad extends JPanel
- implements EBComponent
- {
- private String filename;
- private String defaultFilename;
- private View view;
- private boolean floating;
- private QuickNotepadTextArea textArea;
- private QuickNotepadToolPanel toolPanel;
- //
- // Constructor
- //
- public QuickNotepad(View view, String position)
- {
- super(new BorderLayout());
- this.view = view;
- this.floating = position.equals(
- DockableWindowManager.FLOATING);
- this.filename = jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX
- + "filepath");
- if(this.filename == null || this.filename.length() == 0)
- {
- this.filename = new String(jEdit.getSettingsDirectory()
- + File.separator + "qn.txt");
- jEdit.setProperty(QuickNotepadPlugin.OPTION_PREFIX
- + "filepath",this.filename);
- }
- this.defaultFilename = new String(this.filename);
- this.toolPanel = new QuickNotepadToolPanel(this);
- add(BorderLayout.NORTH, this.toolPanel);
- if(floating)
- this.setPreferredSize(new Dimension(500, 250));
- textArea = new QuickNotepadTextArea();
- textArea.setFont(QuickNotepadOptionPane.makeFont());
- textArea.addKeyListener(new KeyHandler());
- textArea.addAncestorListener(new AncestorHandler());
- JScrollPane pane = new JScrollPane(textArea);
- add(BorderLayout.CENTER, pane);
- readFile();
- }
- //
- // Attribute methods
- //
- // for toolBar display
- public String getFilename()
- {
- return filename;
- }
- //
- // EBComponent implementation
- //
- public void handleMessage(EBMessage message)
- {
- if (message instanceof PropertiesChanged)
- {
- propertiesChanged();
- }
- }
- private void propertiesChanged()
- {
- String propertyFilename = jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX + "filepath");
- if(!defaultFilename.equals(propertyFilename))
- {
- saveFile();
- toolPanel.propertiesChanged();
- defaultFilename = propertyFilename.clone();
- filename = defaultFilename.clone();
- readFile();
- }
- Font newFont = QuickNotepadOptionPane.makeFont();
- if(!newFont.equals(textArea.getFont()))
- {
- textArea.setFont(newFont);
- textArea.invalidate();
- }
- }
- // These JComponent methods provide the appropriate points
- // to subscribe and unsubscribe this object to the EditBus
- public void addNotify()
- {
- super.addNotify();
- EditBus.addToBus(this);
- }
- public void removeNotify()
- {
- saveFile();
- super.removeNotify();
- EditBus.removeFromBus(this);
- }
- ...
- }</programlisting>
- </informalexample>
- <para>This listing refers to a
- <classname>QuickNotebookTextArea</classname> object. It is currently
- implemented as a <classname>JTextArea</classname> with word wrap and tab
- sizes hard-coded. Placing the object in a separate class will simply
- future modifications.</para>
- </section>
- <section id="plugin-implement-quicknotepadtoolbar">
- <title>The QuickNotepadToolBar Class</title>
- <para>There is nothing remarkable about the toolbar panel that is placed
- inside the <classname>QuickNotepad</classname> object. The constructor
- shows the continued use of items from the plugin's properties
- file.</para>
- <informalexample>
- <programlisting>public class QuickNotepadToolPanel extends JPanel
- {
- private QuickNotepad pad;
- private JLabel label;
- public QuickNotepadToolPanel(QuickNotepad qnpad)
- {
- pad = qnpad;
- JToolBar toolBar = new JToolBar();
- toolBar.setFloatable(false);
- toolBar.add(makeCustomButton("quicknotepad.choose-file",
- new ActionListener() {
- public void actionPerformed(ActionEvent evt) {
- QuickNotepadToolPanel.this.pad.chooseFile();
- }
- }));
- toolBar.add(makeCustomButton("quicknotepad.save-file",
- new ActionListener() {
- public void actionPerformed(ActionEvent evt) {
- QuickNotepadToolPanel.this.pad.saveFile();
- }
- }));
- toolBar.add(makeCustomButton("quicknotepad.copy-to-buffer",
- new ActionListener() {
- public void actionPerformed(ActionEvent evt) {
- QuickNotepadToolPanel.this.pad.copyToBuffer();
- }
- }));
- label = new JLabel(pad.getFilename(),
- SwingConstants.RIGHT);
- label.setForeground(Color.black);
- label.setVisible(jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX
- + "show-filepath").equals("true"));
- this.setLayout(new BorderLayout(10, 0));
- this.add(BorderLayout.WEST, toolBar);
- this.add(BorderLayout.CENTER, label);
- this.setBorder(BorderFactory.createEmptyBorder(0, 0, 3, 10));
- }
- ...
- }</programlisting>
- </informalexample>
- <para>The method <classname>makeCustomButton()</classname> provides
- uniform attributes for the three toolbar buttons corresponding to three
- of the plugin's use actions. The menu titles for the user actions serve
- double duty as tooltip text for the buttons. There is also a
- <function>propertiesChanged()</function> method for the toolbar that
- sets the text and visibility of the label containing the notepad file
- path.</para>
- </section>
- <section id="plugin-implement-options">
- <title>The QuickNotepadOptionPane Class</title>
- <para>Using the default implementation provided by
- <classname>AbstractOptionPane</classname> reduces the preparation of an
- option pane to two principal tasks: writing a
- <function>_init()</function> method to layout and initialize the pane,
- and writing a <function>_save()</function> method to commit any settings
- changed by user input. If a button on the option pane should trigger
- another dialog, such as a <classname>JFileChooser</classname> or jEdit's
- own enhanced <classname>VFSFileChooserDialog</classname>, the option
- pane will also have to implement the
- <classname>ActionListener</classname> interface to display additional
- components.</para>
- <para>The QuickNotepad plugin has only three options to set: the path
- name of the file that will store the notepad text, the visibility of the
- path name on the tool bar, and the notepad's display font. Using the
- shortcut methods of the plugin API, the implementation of
- <function>_init()</function> looks like this:</para>
- <informalexample>
- <programlisting>public class QuickNotepadOptionPane extends AbstractOptionPane
- implements ActionListener
- {
- private JTextField pathName;
- private JButton pickPath;
- private FontSelector font;
- ...
- public void _init()
- {
- showPath = new JCheckBox(jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX
- + "show-filepath.title"),
- jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX + "show-filepath")
- .equals("true"));
- addComponent(showPath);
- pathName = new JTextField(jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX
- + "filepath"));
- JButton pickPath = new JButton(jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX
- + "choose-file"));
- pickPath.addActionListener(this);
- JPanel pathPanel = new JPanel(new BorderLayout(0, 0));
- pathPanel.add(pathName, BorderLayout.CENTER);
- pathPanel.add(pickPath, BorderLayout.EAST);
- addComponent(jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX + "file"),
- pathPanel);
- font = new FontSelector(makeFont());
- addComponent(jEdit.getProperty(
- QuickNotepadPlugin.OPTION_PREFIX + "choose-font"),
- font);
- }
- ...
- }</programlisting>
- </informalexample>
- <para>Here we adopt the vertical arrangement offered by use of the
- <function>addComponent()</function> method with one embellishment. We
- want the first <quote>row</quote> of the option pane to contain a text
- field with the current notepad file path and a button that will trigger
- a file chooser dialog when pressed. To place both of them on the same
- line (along with an identifying label for the file option), we create a
- <classname>JPanel</classname> to contain both components and pass the
- configured panel to <function>addComponent()</function>.</para>
- <para>The <function>_init()</function> method uses properties from the
- plugin's property file to provide the names of label for the components
- placed in the option pane. It also uses a property whose name begins
- with <function>PROPERTY_PREFIX</function> as a persistent data item -
- the path of the current notepad file. The elements of the notepad's font
- are also extracted from properties using a static method of the option
- pane class.</para>
- <para>The <function>_save()</function> method extracts data from the
- user input components and assigns them to the plugin's properties. The
- implementation is straightforward:</para>
- <informalexample>
- <programlisting>public void _save()
- {
- jEdit.setProperty(QuickNotepadPlugin.OPTION_PREFIX
- + "filepath", pathName.getText());
- Font _font = font.getFont();
- jEdit.setProperty(QuickNotepadPlugin.OPTION_PREFIX
- + "font", _font.getFamily());
- jEdit.setProperty(QuickNotepadPlugin.OPTION_PREFIX
- + "fontsize", String.valueOf(_font.getSize()));
- jEdit.setProperty(QuickNotepadPlugin.OPTION_PREFIX
- + "fontstyle", String.valueOf(_font.getStyle()));
- jEdit.setProperty(QuickNotepadPlugin.OPTION_PREFIX
- + "show-filepath", String.valueOf(showPath.isSelected()));
- }</programlisting>
- </informalexample>
- <para>The class has only two other methods, one to display a file
- chooser dialog in response to user action, and the other to construct a
- <classname>Font</classname> object from the plugin's font properties.
- They do not require discussion here.</para>
- </section>
- <section id="plugin-implement-docs">
- <title>Plugin Documentation</title>
- <para>While not required by the plugin API, a help file is an essential
- element of any plugin written for public release. A single web page is
- often all that is required. There are no specific requirements on
- layout, but because of the design of jEdit's help viewer, the use of
- frames should be avoided. Topics that would be useful include the
- following:</para>
- <itemizedlist>
- <listitem>
- <para>a description of the purpose of the plugin;</para>
- </listitem>
- <listitem>
- <para>an explanation of the type of input the user can supply
- through its visible interface (such as mouse action or text
- entry in controls);</para>
- </listitem>
- <listitem>
- <para>a listing of available user actions that can be taken when
- the plugin does not have input focus;</para>
- </listitem>
- <listitem>
- <para>a summary of configuration options;</para>
- </listitem>
- <listitem>
- <para>information on development of the plugin (such as a change
- log, a list of <quote>to do</quote> items, and contact
- information for the plugin's author); and</para>
- </listitem>
- <listitem>
- <para>licensing information, including acknowledgments for any
- library software used by the plugin.</para>
- </listitem>
- </itemizedlist>
- <para>The location of the plugin's help file is stored in the
- <literal>plugin.QuickNotepad.docs</literal> property; see <xref
- linkend="plugin-implement-properties" />.</para>
- </section>
- <section id="plugin-implement-building">
- <title>The build.xml Ant build file</title>
- <para>We have already outlined the contents of the user action catalog,
- the properties file and the documentation file in our earlier
- discussion. The final step is to compile the source file and build the
- archive file that will hold the class files and the plugin's other
- resources.</para>
- <para>Publicly released plugins include with their source a makefile in
- XML format for the <application>Ant</application> utility. The format
- for this file requires few changes from plugin to plugin. Here is a
- version of <filename>build.xml</filename> that could be used by
- QuickNotepad:</para>
- <informalexample>
- <programlisting>
- <project name="QuickNotepad"
- default="build">
- <description>
- This is an ant build.xml file for building the QuickNotepad plugin for jEdit.
- </description>
- <property name="user-doc.xml"
- value="users-guide.xml" />
- <property file="build.properties" />
- <property file="../build.properties" />
- <property name="build.support"
- value="../../../build-support" />
- <import file="${build.support}/plugin-build.xml" />
- </project>
- </programlisting>
- </informalexample>
- <para>This build file imports another modular build file,
- <literal>plugin-build.xml</literal> from the <literal>build-support</literal>
- project. It is available as a package you can check out from subversion, or found online in the <ulink
- url="https://jedit.svn.sourceforge.net/svnroot/jedit/build-support/trunk/">jEdit's
- SVN repository</ulink>. It contains the common build steps used to build the core jEdit plugins, and some example <literal>build.properties.sample</literal> files which you can adapt for use with your development environment.</para>
- <para>Customizing this build file for a different plugin will likely
- only require three changes to build.xml file:</para>
- <itemizedlist>
- <listitem>
- <para>the name of the project</para>
- </listitem>
- <listitem>
- <para>the dependencies of the plugin</para>
- </listitem>
- <listitem>
- <para>The extra files that need to be copied into the
- jar.</para>
- </listitem>
- </itemizedlist>
- <para>Because this build file and the one of most plugins import a
- <literal>build.properties</literal> file from the current and the parent
- directories, it is possible to build most of jEdit's plugins in a
- uniform way by setting the common properties in a single
- <literal>build.properties</literal> file, placed in the plugin source's
- parent directory. </para>
- <tip> <para>For a full discussion of the <filename>Ant</filename> file
- format and command syntax, you should consult the <ulink
- url="http://jakarta.apache.org/ant/manual/index.html">Ant
- documentation</ulink>, also available through jEdit's help system if you
- installed the Ant Plugin. When editing Ant build files, the XML plugin
- gives you completion tips for both elements <emphasis>and</emphasis>
- attributes. The Console plugin provides you with an ANT button which you
- can bind to keyboard actions. In addition, there are the AntFarm and
- Antelope plugins which also proivde you with alternate means to execute
- Ant targets through the Console.</para> </tip> </section>
- <section id="plugin-implement-reloading">
- <title>Reloading the Plugin</title>
- <para>Once you have compiled your plugin, you will need to test its
- behavior when it is reloaded. Follow these steps to reload your plugin
- without restarting jEdit:</para>
- <itemizedlist>
- <listitem>
- <para>From the Plugins menu open the Plugin Manager.</para>
- </listitem>
- <listitem>
- <para>On the Manage tab uncheck Hide libraries. This will allow
- you to see plugins that are not loaded.</para>
- </listitem>
- <listitem>
- <para>Recheck the plugin to reload it.</para>
- </listitem>
- </itemizedlist>
- <tip>
- <para>The Activator plugin provides a very convenient (dockable) way
- to test the activating and reloading behavior of your plugin. Be
- sure to test your plugin's reloading behavior with both the
- Activator and the Reloader tabs.</para>
- </tip>
- <para>If you have reached this point in the text, you are probably
- serious about writing a plugin for jEdit. Good luck with your efforts,
- and thank you for contributing to the jEdit project.</para>
- </section>
- <section id="plugin-debugging" >
- <title> Tips for debugging plugins </title>
- <bridgehead> BeanShell </bridgehead>
- <para> jEdit includes a Beanshell interface into its currently running JVM at all times. You can access it a variety of ways, but one way is from <literal>Plugins - Console - Shells - BeanShell</literal>. From here, you can interactively inspect the values of any object in memory, call any of its member functions, or create new instances of any class that is currently loaded by jEdit or any of its plugins. All this, without setting any breakpoints!
- </para>
- <para> If you're too lazy to type each Beanshell statement interactively, you can also create debugging code snippets as macros and invoke them from <literal>utilities - beanshell - evaluate selection</literal>, or <literal>Macros - Misc - Evaluate Buffer in Beanshell</literal>, or place the file in your own macros directory and bind it to its own keyboard shortcut. </para>
- <bridgehead> Other useful tips </bridgehead>
- <para> This section is new but will be expanded shortly. Please post suggestions to the <literal>jedit-devel</literal> mailing list. </para>
- </section>
- </chapter>