JAPI: How to develop Swing applications with JAPI
This part of the guide demonstrates how to develop a Swing application using the JAPI library from scratch.
Our example application shall be a simple text editor without any advanced features. Examples are good if examples are easy to understand, so we want to keep things simple.
Step 1: Identify Use Cases
The first step is to identify the use cases of the application. The following use cases are involved:
-
File related use cases
- Creating a new empty text
- Opening an existing text file
- Saving a text file
- Saving a text file under a new name
- Quitting the application
-
Editing related use cases
- Inserting text
- Overwriting text
- Deleting text
- Cutting text to the clipboard
- Copying text to the clipboard
- Pasting text from the clipboard
The use cases Inserting text, Overwriting text and Deleting text are already handled automatically by the Swing component JTextPane which we will to use for our text editor application. A JTextPane can also be attached to a DefaultEditorKit which handles Cutting text to the clipboard, Copying text to the clipboard and Pasting text from the clipboard.
Step 2: Defining the package
Our package will be net.sf.japi.examples.editor
.
Our main class will be net.sf.japi.examples.editor.Editor
.
The first version will look like this:
package net.sf,japi.examples.editor; /** A Text Editor application. * This is an example for developing an application with JAPI. * @author Christian Hujer */ public class Editor { /** Main program. * @param args command line arguments (currently ignored) */ public static void main(final String... args) { new Editor(); } } // class Editor
Step 3: Menu Concept
We will define the menu structure:
-
File
- New
- Open
- Save
- SaveAs
- Quit
-
Edit
- Cut
- Copy
- Paste
Step 4: Defining the menu
In Swing, a complete application menu is defined as a JMenuBar which contains JMenus, these JMenus contain JMenuItems as entries or other JMenus as sub menus.
In JAPI, creating a complete application menu is a one liner in the Java code.
The complete menu is defined in a properties file named action.properties
.
editor.menuBar=file edit
This line defines that the menu bar consists of two menus named file
and edit
.
The names file
and edit
are only internally used.
file.menu=fileNew fileOpen fileSave fileSaveAs fileQuit file.text=File file.mnemonic=F
These lines define that the menu file is labelled "File", with "F" as mnemonic, and that the file action is a menu consisting of other actions.
These other actions are fileNew
, fileOpen
, fileSave
, fileSaveAs
and fileQuit
.
fileNew.text=New fileNew.mnemonic=N fileNew.accel=ctrl pressed N fileNew.shortdescription=Creates a new file
These lines define that the action "fileNew" is a menu item labelled "New", with "N" as mnemonic, "Ctrl+N" as keyboard shortcut and "Creates a new file" as tooltip text. The other actions and menus are defined analoguous.
It's also possible to store "normal" properties in this property file. Let's store a property for the frame title.
frame.title=Editor (JAPI usage example)
Step 5: Using the defined menu
package net.sf.japi.examples.editor; import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE; import javax.swing.JFrame; import net.sf.japi.swing.ActionFactory; /** A Text Editor application. * This is an example for developing an application with JAPI. * @author Christian Hujer */ public class Editor { /** Action Factory. */ private static final ActionFactory ACTION_FACTORY = getFactory("net.sf.japi.examples.editor"); /** Main program. * @param args command line arguments (currently ignored) */ public static void main(final String... args) { new Editor(); } /** Application frame. */ private JFrame frame = new JFrame(ACTION_FACTORY.getString("frame.title")); /** Editor component. */ private JTextPane textPane = new JTextPane(); /** FileChooser for opening and saving files. */ private JFileChooser fileChooser = new JFileChooser(); /** Create the Editor. */ public Editor() { frame.setJMenuBar(ACTION_FACTORY.createMenuBar(true, "editor", this)); frame.add(ACTION_FACTORY.createToolBar(this, "editor"), NORTH); frame.add(new JScrollPane(textPane)); frame.pack(); frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE); frame.setVisible(true); } } // class Editor
As you see, programming the menu is just a one liner. But how is code attached to the actions attached to the menu items? See next step.
Step 6: Attaching Code to the actions
The name of an action is at the same time the name of the method that's invoked using Reflection.
Such action methods must be public instance methods with empty parameter list and void return type.
These methods may declare any Exception you want.
Reflection will be used to find, independently of declared exceptions, a declarative exception handler for any exception thrown by the action method.
The action methods should be annotated as @ActionMethod
.
In future this will be used for a tool that performs a static code analysis and reports action methods not covered with action definitions and action definitions not covered with action methods.
Step 6.1: Implementing fileNew
/** Action method. * @used */ @ActionMethod public void fileNew() { textPane.setText(""); file = null; }
Step 6.2: Implementing fileOpen
/** Action method. * @used */ @ActionMethod public void fileOpen() throws FileNotFoundException, IOException { if (fileChooser.showOpenDialog(frame) == APPROVE_OPTION) { final StringBuilder newText = new StringBuilder(); final File newFile = fileChooser.getSelectedFile(); final FileReader in = new FileReader(newFile); try { final char[] buf = new char[4096]; for (int charsRead; (charsRead = in.read(buf)) != -1; ) { newText.append(buf, 0, charsRead); } textPane.setText(newText.toString()); file = newFile; } finally { in.close(); } } }
Step 7: Adding precreated actions
For the edit actions Cut, Copy and Paste it would be nice if they were consistent with those provided by the editor kit.
In fact it would be even better if we would use the Actions defined by the editor kit.
That's of course also possible with ActionFactory
.
You can register any number of ActionProvider
s with an ActionFactory
.
An ActionProvider
is used to lookup actions using their names (keys) before ActionFactory
will create its own.
All we need to do is have our Editor implement ActionProvider
and make sure it provides the right Actions.
/** The supported editor action names and their corresponding kit action names. */ private static final Map<String, String> editorActionNames = new HashMap<String, String>(); static { editorActionNames.put("editCut", DefaultEditorKit.cutAction); editorActionNames.put("editCopy", DefaultEditorKit.copyAction); editorActionNames.put("editPaste", DefaultEditorKit.pasteAction); editorActionNames.put("editSelectAll", DefaultEditorKit.selectAllAction); }; /** {@inheritDoc} */ @Nullable public Action getAction(final String key) { for (final Action action : textPane.getActions()) { final String realKey = editorActionNames.get(key); if (realKey != null && realKey.equals(action.getValue(Action.NAME))) { ACTION_FACTORY.initAction(true, action, key); return action; } } return null; }
Complete source
Java Source: net/sf/japi/examples/editor/Editor.java
package net.sf.japi.examples.editor;
import static java.awt.BorderLayout.NORTH;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import javax.swing.Action;
import javax.swing.JFileChooser;
import static javax.swing.JFileChooser.APPROVE_OPTION;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE;
import javax.swing.text.DefaultEditorKit;
import org.jetbrains.annotations.Nullable;
import net.sf.japi.swing.ActionFactory;
import static net.sf.japi.swing.ActionFactory.getFactory;
import net.sf.japi.swing.ActionMethod;
import net.sf.japi.swing.ActionProvider;
public class Editor implements ActionProvider {
/** Action Factory. */
private static final ActionFactory ACTION_FACTORY = getFactory("net.sf.japi.examples.editor");
/** The supported editor action names and their corresponding kit action names. */
private static final Map<String, String> editorActionNames = new HashMap<String, String>();
static {
editorActionNames.put("editCut", DefaultEditorKit.cutAction);
editorActionNames.put("editCopy", DefaultEditorKit.copyAction);
editorActionNames.put("editPaste", DefaultEditorKit.pasteAction);
editorActionNames.put("editSelectAll", DefaultEditorKit.selectAllAction);
};
/** Application frame. */
private final JFrame frame = new JFrame(ACTION_FACTORY.getString("frame.title"));
/** Editor component. */
private final JTextPane textPane = new JTextPane();
/** FileChooser for opening and saving files. */
private final JFileChooser fileChooser = new JFileChooser();
/** Currently opened file.
* Maybe <code>null</code> in case the current document was not already saved.
*/
private File file;
/** Create the Editor. */
public Editor() {
ACTION_FACTORY.addActionProvider(this);
frame.setJMenuBar(ACTION_FACTORY.createMenuBar(true, "editor", this));
frame.add(ACTION_FACTORY.createToolBar(this, "editor"), NORTH);
frame.add(new JScrollPane(textPane));
frame.pack();
frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
/** {@inheritDoc} */
@Nullable public Action getAction(final String key) {
for (final Action action : textPane.getActions()) {
final String realKey = editorActionNames.get(key);
if (realKey != null && realKey.equals(action.getValue(Action.NAME))) {
ACTION_FACTORY.initAction(true, action, key);
return action;
}
}
return null;
}
/** Action method.
* @used
*/
@ActionMethod public void fileNew() {
textPane.setText("");
file = null;
}
/** Action method.
* @used
*/
@ActionMethod public void fileOpen() throws FileNotFoundException, IOException {
if (fileChooser.showOpenDialog(frame) == APPROVE_OPTION) {
final StringBuilder newText = new StringBuilder();
final File newFile = fileChooser.getSelectedFile();
final FileReader in = new FileReader(newFile);
try {
final char[] buf = new char[4096];
for (int charsRead; (charsRead = in.read(buf)) != -1; ) {
newText.append(buf, 0, charsRead);
}
textPane.setText(newText.toString());
file = newFile;
} finally {
in.close();
}
}
}
/** Action method.
* @used
*/
@ActionMethod public void fileQuit() {
if (ACTION_FACTORY.showQuestionDialog(frame, "reallyQuit")) {
frame.dispose();
}
}
/** Action method.
* @used
*/
@ActionMethod public void fileSave() throws IOException {
if (file == null) {
fileSaveAs();
} else {
final FileWriter out = new FileWriter(file);
try {
out.write(textPane.getText());
} finally {
out.close();
}
}
}
/** Action method.
* @used
*/
@ActionMethod public void fileSaveAs() throws IOException {
if (fileChooser.showSaveDialog(frame) == APPROVE_OPTION) {
final File newFile = fileChooser.getSelectedFile();
final FileWriter out = new FileWriter(newFile);
try {
out.write(textPane.getText());
file = newFile;
} finally {
out.close();
}
}
}
/** Main program.
* @param args command line arguments
*/
public static void main(final String... args) {
new Editor();
}
} // class Editor
Action Properties: net/sf/japi/examples/editor/action.properties
# Action properties for Editor, non-localizable information. # This file MUST be ISO-8859-1 ActionFactory.additionalBundles=net.sf.japi.examples.editor.messages editor.menubar=file edit editor.toolbar=fileNew fileOpen fileSave fileSaveAs - editCut editCopy editPaste file.menu=fileNew fileOpen fileSave fileSaveAs fileQuit edit.menu=editCut editCopy editPaste fileNew.icon=general/New16 fileOpen.icon=general/Open16 fileOpen.exception.java.io.FileNotFoundException.messagetype=ERROR_MESSAGE fileOpen.exception.java.io.IOException.messagetype=ERROR_MESSAGE fileSave.icon=general/Save16 fileSave.exception.java.io.IOException.messagetype=ERROR_MESSAGE fileSaveAs.icon=general/SaveAs16 fileSaveAs.exception.java.io.IOException.messagetype=ERROR_MESSAGE editCut.icon=general/Cut16 editCopy.icon=general/Copy16 editPaste.icon=general/Paste16
Message Properties: net/sf/japi/examples/editor/messages.properties
# Action properties for Editor, localizable information. # This file MUST be ISO-8859-1 # frame.title=Text Editor (JAPI usage example) file.text=File file.mnemonic=F fileNew.text=New fileNew.mnemonic=N fileNew.accel=ctrl pressed N fileNew.shortdescription=Creates a new empty text document. fileOpen.text=Open... fileOpen.mnemonic=O fileOpen.accel=ctrl pressed O fileOpen.shortdescription=Opens an existing text document. fileOpen.exception.java.io.FileNotFoundException.title=File not found fileOpen.exception.java.io.FileNotFoundException.message=<html>The file couldn''t be opened. Reason:<br>{0} fileOpen.exception.java.io.IOException.title=I/O error fileOpen.exception.java.io.IOException.message=<html>I/O error while reading. Reason:<br>{0} fileSave.text=Save fileSave.mnemonic=S fileSave.accel=ctrl pressed S fileSave.shortdescription=Saves the current document. fileSave.exception.java.io.IOException.title=I/O error fileSave.exception.java.io.IOException.message=<html>I/O error while writing. Reason:<br>{0} fileSaveAs.text=Save As... fileSaveAs.mnemonic=A fileSaveAs.accel=ctrl shift pressed S fileSaveAs.shortdescription=Saves the current document in a new filename. fileSaveAs.exception.java.io.IOException.title=I/O error fileSaveAs.exception.java.io.IOException.message=<html>I/O error while writing. Reason:<br>{0} fileQuit.text=Quit fileQuit.mnemonic=Q fileQuit.accel=ctrl pressed Q fileQuit.shortdescription=Quits the text editor. edit.text=Edit edit.mnemonic=E editCut.text=Cut editCut.mnemonic=C editCut.accel=ctrl pressed X editCut.shortdescription=Cuts the current selection to the clipboard editCopy.text=Copy editCopy.mnemonic=O editCopy.accel=ctrl pressed C editCopy.shortdescription=Copies the current selection to the clipboard editPaste.text=Paste editPaste.mnemonic=P editPaste.accel=ctrl pressed V editPaste.shortdescription=Pastes over the current selection from the clipboard editSelectAll.text=Select All editSelectAll.mnemonic=A editSelectAll.accel=ctrl pressed A editSelectAll.shortdescription=Selects the entire editor content reallyQuit.title=Really Quit? reallyQuit.message=Do you really want to quit the editor?