Java – Crear un editor de texto

En este tutorial crearemos un editor básico multi-plataforma para documentos de texto plano. Se llamará TextPad Demo y se utilizará la biblioteca gráfica Swing por ser la biblioteca predeterminada en Java, además de flexible y portable. En el desarrollo puede sernos de utilidad un IDE para facilitarnos algunas tareas, pero en la creación de la interface gráfica de usuario (GUI) no se utilizará el editor gráfico, todo el código se escribirá directamente a mano ya sea en el IDE o en el editor de preferencia.

Requisitos:

  • Tener instalado el JDK (Java Development Kit): son las herramientas de desarrollo junto al entorno de ejecución. A la fecha actual el último JDK es el JSE 6.0 – Update 26.
  • Conocimientos básicos de Java.
  • En caso de no utilizar un IDE, se necesita una cierta comprensión de como trabajar con las herramientas de desarrollo del JDK desde la línea de comandos para construir el proyecto. Eso no se explica en este tutorial.

Introducción


Se le denomina texto plano al texto que contiene únicamente caracteres y no contiene información que defina formatos como lo son la Negrita, Cursiva, Subrayado, etc. o el estilo de letra como lo es Arial, Times, Courier, etc. En prácticamente todos los sistemas operativos la extensión de archivo .TXT es por convención la preferida para archivos de texto plano, sin embargo se pueden utilizar otras. Algunas extensiones populares que también son de texto plano son .INF, .DAT, etc.

Aunque el editor trabajará internamente solo con texto plano, el área de edición del editor podrá configurarse visualmente con un fuente y color a gusto del usuario.

A continuación se lista la mayoría de las clases e interfaces que necesitaremos para crear este editor de texto.

Las clases que definen los contenedores y componentes principales de Swing que conformaran la interface gráfica:

  • javax.swing.JFrame – Este es la ventana principal del editor.
  • javax.swing.JMenuBar, JPopupMenu, JMenu, JItem, JCheckBoxMenuItem – Con estos crearemos los menúes de opciones.
  • javax.swing.JToolBar, JButton – Con estos crearemos la barra de herramientas.
  • javax.swing.JTextArea – Este es el área de edición para texto plano donde se visualiza el documento.
  • javax.swing.JPanel, JLabel – Con estos crearemos la barra de estado.
  • javax.swing.JFileChooser – Este es un cuadro de dialogo que permite elegir archivos del sistema.

Las clases e interfaces que le otorgaran funcionalidad al editor:

  • java.awt.event.WindowAdapter, WindowEvent – Control de eventos de la ventana principal.
  • java.awt.event.ActionListener, ActionEvent – Control de eventos sobre menúes y botones.
  • java.awt.event.MouseAdapter, MouseEvent – Control de eventos del ratón.
  • java.awt.print.Printable, PrinterJob, PageFormat – Impresión del documento.
  • javax.swing.event.CaretListener, CaretEvent – Control de eventos sobre el cursor.
  • javax.swing.undo.UndoManager – Administrador de edición.
  • javax.swing.event.UndoableEditListener, UndoableEditEvent – Control de cambios en el documento.
  • java.io.File, FileReader, FileWriter, BufferedReader, BufferedWriter – Manejo de archivos.

Nota: es buena idea leer la documentación de cada clase.

En resumen, las posibilidades y la funcionalidad de este editor:

  • Cargar un documento en el área de edición.
  • Guardar el documento en un archivo.
  • Imprimir el documento.
  • Deshacer/Rehacer cambios en el documento.
  • Acciones típicas sobre texto: Cortar, Copiar, Pegar, Seleccionar todo, Buscar, llevar el cursor a una línea especifica.
  • Opciones visuales: ajuste de línea, ver y fijar barra de herramientas, ver barra de estado, fuente de letra, color de letra, color de fondo.

Algunas imágenes del aspecto del editor en diferentes entornos:

Windows XP

Windows Seven

Debian Squeeze 6 – GNOME 2


Slackware 13 – KDE 4

Nota: faltaría una captura en Mac.

El proyecto TextPad


El proyecto TextPad contendrá los siguientes paquetes:

Paquete: textpademo

Aquí se encuentran las clases del proyecto. Las cuatro principales definen un archivo de código fuente cada una:

  • ActionPerformer.java
  • JFontChooser.java
  • PrintAction.java
  • TPEditor.java

Nota: dentro de algunos archivos de código fuente también hay otras clases adicionales, por ejemplo EventHandler es una clase interna en TPEditor, estas clases luego de compiladas quedan almacenadas en archivos .CLASS separados.

Paquete: res

Aquí se encuentran los recursos del proyecto. Estos son diez archivos de imagen de tipo .PNG (dimensión: 32 x 32) para la interface gráfica:

  • tp_copy.png
  • tp_cut.png
  • tp_new.png
  • tp_open.png
  • tp_paste.png
  • tp_print.png
  • tp_redo.png
  • tp_save.png
  • tp_saveas.png
  • tp_undo.png

– – – – – – – – – – – – – – – – – –

Para aquellos que utilizan algún IDE, tener una estructura de proyecto bien organizada no supone ningún problema ni perdida de tiempo ya que cualquier IDE crea automáticamente una estructura de directorios adecuada y el proyecto comienza a tomar forma a medida que se le van agregando paquetes y clases. Además el IDE puede realizar la compilación del proyecto y el empaquetado en tarros .JAR con unos pocos clicks.

En NetBeans el proyecto se ve de la siguiente forma desde la pestaña “Projects“:


En Eclipse el proyecto se ve de la siguiente forma desde la pestaña “Package Explorer“:


En IntelliJ el proyecto se ve de la siguiente forma desde la pestaña “Project“:

En el caso de aquellos que no están utilizando un IDE, pueden recurrir a utilizar alguna herramienta de gestión de proyectos como Ant con el fin de automatizar y facilitar el proceso de compilado, empaquetado, y etc., simplemente escribiendo scripts XML.

La estructura principal de los directorios del proyecto es:

...\TextPad\src\textpademo\
...\TextPad\src\res\ 

Donde los directorios textpademo\ y res\ definen los paquetes del proyecto.

En Windows la ubicación completa del proyecto podría ser:

c:\Users\User\Java\Proyectos\TextPad\src\textpademo\
c:\Users\User\Java\Proyectos\TextPad\src\res\

En Linux la ubicación completa del proyecto podría ser:

/home/user/Java/Proyectos/TextPad/src/textpademo/
/home/user/Java/Proyectos/TextPad/src/res/

El siguiente esquema es una propuesta ideal para la estructura del proyecto:

[TextPad/]
├── [src/]
│   ├── [res/]
│   │   ├── tp_copy.png
│   │   ├── tp_cut.png
│   │   ├── tp_new.png
│   │   ├── tp_open.png
│   │   ├── tp_paste.png
│   │   ├── tp_print.png
│   │   ├── tp_redo.png
│   │   ├── tp_save.png
│   │   ├── tp_saveas.png
│   │   └── tp_undo.png
│   └── [textpademo/]
│       ├── ActionPerformer.java
│       ├── JFontChooser.java
│       ├── PrintAction.java
│       └── TPEditor.java
├── [build/]
│       └── [classes/]
├── [doc/]
├── [test/]
└── build.xml

Creando el editor


Ahora escribiremos el código del programa, clase por clase y método por método. Podemos observar aquí un sencillo diagrama de flujo que describe la ejecución a través de las clases principales del proyecto:

La propia clase TPEditor podría encargarse de manejar los eventos, sin embargo decidí manejarlos a todos en una clase especial y desde esta llamar a otra para ejecutar las operaciones requeridas. Más allá de que aquí todo, o casi todo, es un objeto, el diseño presentado no es especialmente OO (orientado a objetos), sino más bien simple y concreto.

Nota: El código fuente completo se encuentra al final.

Archivo TPEditor.java

El archivo de código fuente TPEditor.java contiene la clase publica TPEditor que se encarga de construir la interface gráfica de usuario (GUI), en esta clase se encuentra el método void main(String[] args) que define el punto de entrada del programa.

Dentro de la clase TPEditor se encuentra la clase interna EventHandler, a la cual se le deja la tarea de manejar todos los eventos que ocurran sobre los componentes de la GUI construida.

Comencemos con el esqueleto de este archivo, se definen las clases, métodos (sin contenido), y variables:

package textpademo;    //se define el paquete donde debe estar este archivo

import java.awt.BorderLayout;    //importamos todo lo que se utilizará
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.BadLocationException;
import javax.swing.undo.UndoManager;

public class TPEditor {    //clase publica TPEditor

    private JFrame jFrame;            //instancia de JFrame (ventana principal)
    private JMenuBar jMenuBar;        //instancia de JMenuBar (barra de menú)
    private JToolBar jToolBar;        //instancia de JToolBar (barra de herramientas)
    private JTextArea jTextArea;      //instancia de JTextArea (area de edición)
    private JPopupMenu jPopupMenu;    //instancia de JPopupMenu (menú emergente)
    private JPanel statusBar;         //instancia de JPanel (barra de estado)

    private JCheckBoxMenuItem itemLineWrap;         //instancias de algunos items de menú que necesitan ser accesibles
    private JCheckBoxMenuItem itemShowToolBar;
    private JCheckBoxMenuItem itemFixedToolBar;
    private JCheckBoxMenuItem itemShowStatusBar;
    private JMenuItem mbItemUndo;
    private JMenuItem mbItemRedo;
    private JMenuItem mpItemUndo;
    private JMenuItem mpItemRedo;

    private JButton buttonUndo;    //instancias de algunos botones que necesitan ser accesibles
    private JButton buttonRedo;

    private JLabel sbFilePath;    //etiqueta que muestra la ubicación del archivo actual
    private JLabel sbFileSize;    //etiqueta que muestra el tamaño del archivo actual
    private JLabel sbCaretPos;    //etiqueta que muestra la posición del cursor en el área de edición

    private boolean hasChanged = false;    //el estado del documento actual, no modificado por defecto
    private File currentFile = null;       //el archivo actual, ninguno por defecto

    private final EventHandler eventHandler;          //instancia de EventHandler (la clase que maneja eventos)
    private final ActionPerformer actionPerformer;    //instancia de ActionPerformer (la clase que ejecuta acciones)
    private final UndoManager undoManager;            //instancia de UndoManager (administrador de edición)

    public static void main(String[] args) {    //punto de entrada del programa
    }

    public TPEditor() {    //constructor de la clase TPEditor
    }

    private void buildMenuBar() {    //construye la barra de menú
    }

    private void buildToolBar() {    //construye la barra de herramientas
    }

    private void buildTextArea() {    //construye el área de edición
    }

    private void buildStatusBar() {    //construye la barra de estado
    }

    private void buildPopupMenu() {    //construye el menú emergente
    }

    private void showPopupMenu(MouseEvent e) {    //despliega el menú emergente
    }

    void updateControls() {    //actualiza algunos componentes de la GUI
    }

    EventHandler getEventHandler() {    //retorna la instancia de EventHandler (la clase que maneja eventos)
    }

    UndoManager getUndoManager() {    //retorna la instancia de UndoManager (administrador de edición)
    }

    boolean documentHasChanged() {    //retorna el estado de la variable hasChanged
    }

    void setDocumentChanged(boolean hasChanged) {    //establece el estado de la variable hasChanged
    }

    JTextArea getJTextArea() {    //retorna la instancia de JTextArea (área de edición)
    }

    JFrame getJFrame() {    //retorna la instancia de JFrame (ventana principal)
    }

    File getCurrentFile() {    //retorna el valor de la variable currentFile
    }

    void setCurrentFile(File currentFile) {    //establece el valor de la variable currentFile
    }

    JLabel getJLabelFilePath() {    //retorna la instancia de la etiqueta sbFilePath
    }

    JLabel getJLabelFileSize() {    //retorna la instancia de la etiqueta sbFileSize
    }

    /* la clase EventHandler extiende e implementa las clases e interfaces necesarias para
    atender y manejar los eventos sobre la GUI principal del editor */
    class EventHandler extends MouseAdapter implements ActionListener,
                                                       CaretListener,
                                                       UndoableEditListener {

        @Override
        public void actionPerformed(ActionEvent ae) {    //implemento de la interface ActionListener
        }

        @Override
        public void caretUpdate(CaretEvent ce) {    //implemento de la interface CaretListener
        }

        @Override
        public void undoableEditHappened(UndoableEditEvent uee) {    //implemento de la interface UndoableEditListener
        }

        @Override
        public void mousePressed(MouseEvent me) {    //herencia de la clase MouseAdapter
        }

        @Override
        public void mouseReleased(MouseEvent me) {    //herencia de la clase MouseAdapter
        }
    }
}

A partir de aquí iremos completando el cuerpo de los métodos.

Comenzamos con el método void main(String[] args), este es el punto de entrada del programa, aquí es donde todo lo relacionado con nuestro código comienza. Construimos una instancia de la clase TPEditor, la cual se encargará de construir la GUI, el detalle es que nos aseguramos que esto suceda desde el principio en el EDT (Event Dispatch Thread). Es de buena practica comenzar las aplicaciones Swing de esta forma, aunque es más importante cuando se trata de programas más complejos que este.

...
public static void main(String[] args) {
    //construye la GUI en el EDT (Event Dispatch Thread)
    javax.swing.SwingUtilities.invokeLater(new Runnable() {

        @Override
        public void run() {
            new TPEditor().jFrame.setVisible(true);    //hace visible la GUI creada por la clase TPEditor
        }
    });
}
...

Cuando el método void main(String[] args) construye la clase TPEditor, el correspondiente constructor de esta clase realiza su principal tarea de crear la GUI con todos los componente necesarios antes de que se haga visible para el usuario, y también se preparan y configuran otras clases que luego tendrán un papel fundamental.

Lo primero es asegurar un LookAndFeel adecuado para el sistema subyacente, de esta forma nuestro editor encajara con el aspecto de las demás aplicaciones del sistema. Luego creamos un JFrame que será la ventana principal del editor y se le asigna una clase anónima que se encargara de manejar el evento de cierre, todos los restantes eventos serán manejados por EventHandler.

Aquí se inicializan clases importantes, se construye una instancia de la clase EventHandler que manejara los eventos, una instancia de la clase ActionPerformer que ejecutará las operaciones necesarias y una instancia de la clase UndoManager que administrará las ediciones realizadas en el documento permitiendo deshacer o rehacer cambios.

Se ejecuta una sucesión de métodos encargados de crear las diferentes partes del editor: el área de edición, una barra de menú, una barra de herramientas, y una barra de estado. En esta sucesión el método que tiene relevancia en el orden es void buildTextArea() el cual debe de ejecutarse primero, el motivo se explica más adelante.

Todas las partes del editor se colocan dentro del contenedor principal del JFrame, en su correspondiente orientación.

...
public TPEditor() {
    try {    //LookAndFeel nativo
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    } catch (Exception ex) {
        System.err.println(ex);
    }

    //construye un JFrame con título
    jFrame = new JFrame("TextPad Demo - Sin Título");
    jFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

    //asigna un manejador de eventos para el cierre del JFrame
    jFrame.addWindowListener(new WindowAdapter() {

        @Override
        public void windowClosing(WindowEvent we) {
            actionPerformer.actionExit();    //invoca el método actionExit()
        }
    });

    eventHandler = new EventHandler();              //construye una instancia de EventHandler
    actionPerformer = new ActionPerformer(this);    //construye una instancia de ActionPerformer
    undoManager = new UndoManager();                //construye una instancia de UndoManager
    undoManager.setLimit(50);                       //le asigna un límite al buffer de ediciones

    buildTextArea();     //construye el área de edición, es importante que esta sea la primera parte en construirse
    buildMenuBar();      //construye la barra de menú
    buildToolBar();      //construye la barra de herramientas
    buildStatusBar();    //construye la barra de estado
    buildPopupMenu();    //construye el menú emergente

    jFrame.setJMenuBar(jMenuBar);                              //designa la barra de menú del JFrame
    Container c = jFrame.getContentPane();                     //obtiene el contenedor principal
    c.add(jToolBar, BorderLayout.NORTH);                       //añade la barra de herramientas, orientación NORTE del contendor
    c.add(new JScrollPane(jTextArea), BorderLayout.CENTER);    //añade el área de edición en el CENTRO
    c.add(statusBar, BorderLayout.SOUTH);                      //añade la barra de estado, orientación SUR

    //configura el JFrame con un tamaño inicial proporcionado con respecto a la pantalla
    Dimension pantalla = Toolkit.getDefaultToolkit().getScreenSize();
    jFrame.setSize(pantalla.width / 2, pantalla.height / 2);

    //centra el JFrame en pantalla
    jFrame.setLocationRelativeTo(null);
}
...

El método void buildTextArea() es el encargado de construir el área de edición donde el usuario trabaja con sus documentos. El componente ideal para documentos de texto plano es el JTextArea, a este se le asigna el manejador de eventos necesario para conocer la posición del cursor, detectar eventos del ratón, detectar cambios en el documento, y etc.

El motivo por el cual esta parte del editor es la primera en construirse se debe a que en el sistema operativo Windows (y en otros también) se le asigna por defecto al JTextArea combinaciones de teclas asociadas a las operaciones típicas: Cortar (CTRL + X), Copiar (CTRL + C) y Pegar (CTRL + V). Como la intención en este editor es crear una barra de menú en la cual los items Cortar, Copiar y Pegar tengan asignados esas mismas combinaciones de teclas hay que asegurarse de tener el control sobre estas operaciones, para eso se remueven las combinaciones CTRL + X, CTRL + C y CTRL + V de la configuración predeterminada cuando recién se crea el JTextArea y antes de que se construya la barra de menú. Si no realizáramos esto, posiblemente al presionar por ejemplo CTRL + X no estaríamos invocando nuestro propio método. Si bien lo anterior no supone un problema critico para este programa, en proyectos más complejos podría ser algo a tener en cuenta.

...
private void buildTextArea() {
    jTextArea = new JTextArea();    //construye un JTextArea

    //se configura por defecto para que se ajusten las líneas al tamaño del área de texto ...
    jTextArea.setLineWrap(true);
    //... y que se respete la integridad de las palabras en el ajuste
    jTextArea.setWrapStyleWord(true);

    //asigna el manejador de eventos para el cursor
    jTextArea.addCaretListener(eventHandler);
    //asigna el manejador de eventos para el ratón
    jTextArea.addMouseListener(eventHandler);
    //asigna el manejador de eventos para registrar los cambios sobre el documento
    jTextArea.getDocument().addUndoableEditListener(eventHandler);

    //remueve las posibles combinaciones de teclas asociadas por defecto con el JTextArea
    jTextArea.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), "none");    //remueve CTRL + X ("Cortar")
    jTextArea.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), "none");    //remueve CTRL + C ("Copiar")
    jTextArea.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), "none");    //remueve CTRL + V ("Pegar")
}
...

El método void buildMenuBar() es el encargado de construir la barra de menú la cual tiene cuatro menúes propiamente dichos. Cada menú tiene una cierta cantidad de elementos (items), a la mayoría de estos se les atribuye una combinación de teclas para facilitar su acceso con el teclado, por ejemplo al item “Abrir” se accede con CTRL + O. A todos los items se les asigna un nombre de comando, esta es una forma de identificarlos para facilitar la tarea del manejador de eventos. También, a todos los items se les asigna el manejador de eventos, el cual siempre es el mismo, y para evitar escribir el mismo código correspondiente a cada item creado se utiliza al final un bucle que itera sobre todos los componentes de la barra de menú y le asigna el manejador de eventos siempre que no sea un simple separador.

Nota: en la barra de menú no todo componente es un item, también existen separadores. Si se le asigna un manejador de eventos a un separador se genera una excepción.

...
private void buildMenuBar() {
    jMenuBar = new JMenuBar();    //construye un JMenuBar

    //construye el menú "Archivo", a continuación se construyen los items para este menú
    JMenu menuFile = new JMenu("Archivo");

    //construye el item "Nuevo"
    JMenuItem itemNew = new JMenuItem("Nuevo");
    //le asigna una combinación de teclas
    itemNew.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, KeyEvent.CTRL_MASK));
    //le asigna un nombre de comando
    itemNew.setActionCommand("cmd_new");

    JMenuItem itemOpen = new JMenuItem("Abrir");
    itemOpen.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_MASK));
    itemOpen.setActionCommand("cmd_open");

    JMenuItem itemSave = new JMenuItem("Guardar");
    itemSave.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK));
    itemSave.setActionCommand("cmd_save");

    JMenuItem itemSaveAs = new JMenuItem("Guardar como...");
    itemSaveAs.setActionCommand("cmd_saveas");
    itemSaveAs.addActionListener(eventHandler);

    JMenuItem itemPrint = new JMenuItem("Imprimir");
    itemPrint.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_MASK));
    itemPrint.setActionCommand("cmd_print");

    JMenuItem itemExit = new JMenuItem("Salir");
    itemExit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK));
    itemExit.setActionCommand("cmd_exit");

    menuFile.add(itemNew);    //se añaden los items al menú "Archivo"
    menuFile.add(itemOpen);
    menuFile.add(itemSave);
    menuFile.add(itemSaveAs);
    menuFile.addSeparator();
    menuFile.add(itemPrint);
    menuFile.addSeparator();
    menuFile.add(itemExit);
    //----------------------------------------------

    //construye el menú "Editar", a continuación se construyen los items para este menú
    JMenu menuEdit = new JMenu("Editar");

    mbItemUndo = new JMenuItem("Deshacer");
    mbItemUndo.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
    mbItemUndo.setEnabled(false);
    mbItemUndo.setActionCommand("cmd_undo");

    mbItemRedo = new JMenuItem("Rehacer");
    mbItemRedo.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
    mbItemRedo.setEnabled(false);
    mbItemRedo.setActionCommand("cmd_redo");

    JMenuItem itemCut = new JMenuItem("Cortar");
    itemCut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK));
    itemCut.setActionCommand("cmd_cut");

    JMenuItem itemCopy = new JMenuItem("Copiar");
    itemCopy.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK));
    itemCopy.setActionCommand("cmd_copy");

    JMenuItem itemPaste = new JMenuItem("Pegar");
    itemPaste.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK));
    itemPaste.setActionCommand("cmd_paste");

    JMenuItem itemSearch = new JMenuItem("Buscar");
    itemSearch.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, ActionEvent.CTRL_MASK));
    itemSearch.setActionCommand("cmd_search");

    JMenuItem itemSearchNext = new JMenuItem("Buscar siguiente");
    itemSearchNext.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0));
    itemSearchNext.setActionCommand("cmd_searchnext");

    JMenuItem itemGotoLine = new JMenuItem("Ir a la línea...");
    itemGotoLine.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, ActionEvent.CTRL_MASK));
    itemGotoLine.setActionCommand("cmd_gotoline");

    JMenuItem itemSelectAll = new JMenuItem("Seleccionar todo");
    itemSelectAll.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, ActionEvent.CTRL_MASK));
    itemSelectAll.setActionCommand("cmd_selectall");

    menuEdit.add(mbItemUndo);    //se añaden los items al menú "Editar"
    menuEdit.add(mbItemRedo);
    menuEdit.addSeparator();     //añade separadores entre algunos items
    menuEdit.add(itemCut);
    menuEdit.add(itemCopy);
    menuEdit.add(itemPaste);
    menuEdit.addSeparator();
    menuEdit.add(itemSearch);
    menuEdit.add(itemSearchNext);
    menuEdit.add(itemGotoLine);
    menuEdit.addSeparator();
    menuEdit.add(itemSelectAll);
    //----------------------------------------------

    //construye el menú "Opciones", a continuación se construyen los items para este menú
    JMenu menuTools = new JMenu("Opciones");

    itemLineWrap = new JCheckBoxMenuItem("Ajuste de línea");
    itemLineWrap.setSelected(true);
    itemLineWrap.setActionCommand("cmd_linewrap");

    itemShowToolBar = new JCheckBoxMenuItem("Ver barra de herramientas");
    itemShowToolBar.setSelected(true);
    itemShowToolBar.setActionCommand("cmd_showtoolbar");

    itemFixedToolBar = new JCheckBoxMenuItem("Fijar barra de herramientas");
    itemFixedToolBar.setSelected(true);
    itemFixedToolBar.setActionCommand("cmd_fixedtoolbar");

    itemShowStatusBar = new JCheckBoxMenuItem("Ver barra de estado");
    itemShowStatusBar.setSelected(true);
    itemShowStatusBar.setActionCommand("cmd_showstatusbar");

    JMenuItem itemFont = new JMenuItem("Fuente de letra");
    itemFont.setActionCommand("cmd_font");

    JMenuItem itemFontColor = new JMenuItem("Color de letra");
    itemFontColor.setActionCommand("cmd_fontcolor");

    JMenuItem itemBackgroundColor = new JMenuItem("Color de fondo");
    itemBackgroundColor.setActionCommand("cmd_backgroundcolor");

    menuTools.add(itemLineWrap);    //se añaden los items al menú "Opciones"
    menuTools.add(itemShowToolBar);
    menuTools.add(itemFixedToolBar);
    menuTools.add(itemShowStatusBar);
    menuTools.addSeparator();
    menuTools.add(itemFont);
    menuTools.add(itemFontColor);
    menuTools.add(itemBackgroundColor);

    //construye el menú "Ayuda", a continuación se construye el único item para este menú
    JMenu menuHelp = new JMenu("Ayuda");

    JMenuItem itemAbout = new JMenuItem("Acerca de");
    itemAbout.setActionCommand("cmd_about");

    menuHelp.add(itemAbout);     //se añade el único item al menú "Ayuda"
    //----------------------------------------------

    jMenuBar.add(menuFile);    //se añaden los menúes construidos a la barra de menú
    jMenuBar.add(Box.createHorizontalStrut(5));    //añade espacios entre cada menú
    jMenuBar.add(menuEdit);
    jMenuBar.add(Box.createHorizontalStrut(5));
    jMenuBar.add(menuTools);
    jMenuBar.add(Box.createHorizontalStrut(5));
    jMenuBar.add(menuHelp);

    /* itera sobre todos los componentes de la barra de menú, se les asigna el mismo
    manejador de eventos a todos excepto a los separadores */
    for (Component c1 : jMenuBar.getComponents()) {
        //si el componente es un menú
        if (c1.getClass().equals(javax.swing.JMenu.class)) {
            //itera sobre los componentes del menú
            for (Component c2 : ((JMenu) c1).getMenuComponents()) {
                //si el componente no es un separador
                if (!c2.getClass().equals(javax.swing.JPopupMenu.Separator.class)) {
                    ((JMenuItem) c2).addActionListener(eventHandler);
                }
            }
        }
    }
}
...

El método void buildToolBar() es el encargado de construir la barra de herramientas. Esta formada por diez botones de imagen que representan las operaciones mas notables que puede realizar el editor: Nuevo, Abrir, Guardar, Guardar como, Imprimir, Deshacer, Rehacer, Cortar, Copiar y Pegar. A todos estos botones se les asigna un nombre de comando; esta es una forma de identificarlos para facilitar la tarea del manejador de eventos, un icono de imagen; archivo .PNG de 32×32 pixeles ubicado en los recursos del proyecto, una etiqueta flotante; texto de descripción visible cuando el ratón esta encima del botón. También, a todos los botones se les asigna el manejador de eventos, el cual siempre es el mismo, y para evitar escribir el mismo código correspondiente a cada botón creado se utiliza al final un bucle que itera sobre todos los componentes de la barra de herramientas y le asigna el manejador de eventos siempre que se trate de un botón.

Nota: en la barra de herramientas no todo componente es un botón, también existen separadores. Si se le asigna un manejador de eventos a un separador se genera una excepción.

...
private void buildToolBar() {
    jToolBar = new JToolBar();       //construye un JToolBar
    jToolBar.setFloatable(false);    //se configura por defecto como barra fija

    //construye el botón "Nuevo"
    JButton buttonNew = new JButton();
    //le asigna una etiqueta flotante
    buttonNew.setToolTipText("Nuevo");
    //le asigna una imagen ubicada en los recursos del proyecto
    buttonNew.setIcon(new ImageIcon(getClass().getResource("/res/tp_new.png")));
    //le asigna un nombre de comando
    buttonNew.setActionCommand("cmd_new");

    JButton buttonOpen = new JButton();
    buttonOpen.setToolTipText("Abrir");
    buttonOpen.setIcon(new ImageIcon(getClass().getResource("/res/tp_open.png")));
    buttonOpen.setActionCommand("cmd_open");

    JButton buttonSave = new JButton();
    buttonSave.setToolTipText("Guardar");
    buttonSave.setIcon(new ImageIcon(getClass().getResource("/res/tp_save.png")));
    buttonSave.setActionCommand("cmd_save");

    JButton buttonSaveAs = new JButton();
    buttonSaveAs.setToolTipText("Guardar como...");
    buttonSaveAs.setIcon(new ImageIcon(getClass().getResource("/res/tp_saveas.png")));
    buttonSaveAs.setActionCommand("cmd_saveas");

    JButton buttonPrint = new JButton();
    buttonPrint.setToolTipText("Imprimir");
    buttonPrint.setIcon(new ImageIcon(getClass().getResource("/res/tp_print.png")));
    buttonPrint.setActionCommand("cmd_print");

    buttonUndo = new JButton();
    buttonUndo.setEnabled(false);
    buttonUndo.setToolTipText("Deshacer");
    buttonUndo.setIcon(new ImageIcon(getClass().getResource("/res/tp_undo.png")));
    buttonUndo.setActionCommand("cmd_undo");

    buttonRedo = new JButton();
    buttonRedo.setEnabled(false);
    buttonRedo.setToolTipText("Rehacer");
    buttonRedo.setIcon(new ImageIcon(getClass().getResource("/res/tp_redo.png")));
    buttonRedo.setActionCommand("cmd_redo");

    JButton buttonCut = new JButton();
    buttonCut.setToolTipText("Cortar");
    buttonCut.setIcon(new ImageIcon(getClass().getResource("/res/tp_cut.png")));
    buttonCut.setActionCommand("cmd_cut");

    JButton buttonCopy = new JButton();
    buttonCopy.setToolTipText("Copiar");
    buttonCopy.setIcon(new ImageIcon(getClass().getResource("/res/tp_copy.png")));
    buttonCopy.setActionCommand("cmd_copy");

    JButton buttonPaste = new JButton();
    buttonPaste.setToolTipText("Pegar");
    buttonPaste.setIcon(new ImageIcon(getClass().getResource("/res/tp_paste.png")));
    buttonPaste.setActionCommand("cmd_paste");

    jToolBar.add(buttonNew);    //se añaden los botones construidos a la barra de herramientas
    jToolBar.add(buttonOpen);
    jToolBar.add(buttonSave);
    jToolBar.add(buttonSaveAs);
    jToolBar.addSeparator();    //añade separadores entre algunos botones
    jToolBar.add(buttonPrint);
    jToolBar.addSeparator();
    jToolBar.add(buttonUndo);
    jToolBar.add(buttonRedo);
    jToolBar.addSeparator();
    jToolBar.add(buttonCut);
    jToolBar.add(buttonCopy);
    jToolBar.add(buttonPaste);

    /* itera sobre todos los componentes de la barra de herramientas, se les asigna el
    mismo margen y el mismo manejador de eventos unicamente a los botones */
    for (Component c : jToolBar.getComponents()) {
        //si el componente es un botón
        if (c.getClass().equals(javax.swing.JButton.class)) {
            JButton jb = (JButton) c;
            jb.setMargin(new Insets(0, 0, 0, 0));
            jb.addActionListener(eventHandler);
        }
    }
}
...

El método void buildStatusBar() es el encargado de construir la barra de estado. En esta se muestra alguna información sobre el archivo\documento actual. Como no existe una barra de estado en el set estándar de componentes Swing, y para no tener que utilizar una librería externa escrita por terceros, se crea un panel al cual se le añaden las etiquetas necesarias para mostrar la información y con esto ya es suficiente para imitar una barra de estado decente.

...
private void buildStatusBar() {
    statusBar = new JPanel();    //construye un JPanel
    //se configura con un BoxLayout
    statusBar.setLayout(new BoxLayout(statusBar, BoxLayout.LINE_AXIS));
    //le añade un borde compuesto
    statusBar.setBorder(BorderFactory.createCompoundBorder(
                        BorderFactory.createLoweredBevelBorder(),
                        BorderFactory.createEmptyBorder(5, 5, 5, 5)));

    //construye la etiqueta para mostrar la ubicación del archivo actual
    sbFilePath = new JLabel("...");
    //construye la etiqueta para mostrar el tamaño del archivo actual
    sbFileSize = new JLabel("");
    //construye la etiqueta para mostrar la posición del cursor en el documento actual
    sbCaretPos = new JLabel("...");

    /* se añaden las etiquetas construidas al JPanel, el resultado es un panel
    similar a una barra de estado */
    statusBar.add(sbFilePath);
    statusBar.add(Box.createRigidArea(new Dimension(10, 0)));
    statusBar.add(sbFileSize);
    statusBar.add(Box.createRigidArea(new Dimension(10, 0)));
    statusBar.add(Box.createHorizontalGlue());
    statusBar.add(sbCaretPos);
}
...

El método void buildPopupMenu() es el encargado de construir el menú emergente que solo se hace visible con el click izquierdo del ratón sobre el área de edición. A todos los items se les asigna un nombre de comando, esta es una forma de identificarlos para facilitar la tarea del manejador de eventos. También, a todos los items se les asigna el manejador de eventos, el cual siempre es el mismo, y para evitar escribir el mismo código correspondiente a cada item creado se utiliza al final un bucle que itera sobre todos los componentes del menú emergente y le asigna el manejador de eventos siempre que no sea un simple separador.

Nota: en el menú emergente no todo componente es un item, también existen separadores. Si se le asigna un manejador de eventos a un separador se genera una excepción.

...
private void buildPopupMenu() {
    jPopupMenu = new JPopupMenu();    //se construye un JPopupMenu

    //construye el item "Deshacer"
    mpItemUndo = new JMenuItem("Deshacer");
    //se configura desactivado por defecto
    mpItemUndo.setEnabled(false);
    //le asigna un nombre de comando
    mpItemUndo.setActionCommand("cmd_undo");

    mpItemRedo = new JMenuItem("Rehacer");
    mpItemRedo.setEnabled(false);
    mpItemRedo.setActionCommand("cmd_redo");

    JMenuItem itemCut = new JMenuItem("Cortar");
    itemCut.setActionCommand("cmd_cut");

    JMenuItem itemCopy = new JMenuItem("Copiar");
    itemCopy.setActionCommand("cmd_copy");

    JMenuItem itemPaste = new JMenuItem("Pegar");
    itemPaste.setActionCommand("cmd_paste");

    JMenuItem itemGoto = new JMenuItem("Ir a...");
    itemGoto.setActionCommand("cmd_gotoline");

    JMenuItem itemSearch = new JMenuItem("Buscar");
    itemSearch.setActionCommand("cmd_search");

    JMenuItem itemSearchNext = new JMenuItem("Buscar siguiente");
    itemSearchNext.setActionCommand("cmd_searchnext");

    JMenuItem itemSelectAll = new JMenuItem("Seleccionar todo");
    itemSelectAll.setActionCommand("cmd_selectall");

    jPopupMenu.add(mpItemUndo);    //se añaden los items al menú emergente
    jPopupMenu.add(mpItemRedo);
    jPopupMenu.addSeparator();     //añade separadores entre algunos items
    jPopupMenu.add(itemCut);
    jPopupMenu.add(itemCopy);
    jPopupMenu.add(itemPaste);
    jPopupMenu.addSeparator();
    jPopupMenu.add(itemGoto);
    jPopupMenu.add(itemSearch);
    jPopupMenu.add(itemSearchNext);
    jPopupMenu.addSeparator();
    jPopupMenu.add(itemSelectAll);

    /* itera sobre todos los componentes del menú emergente, se les asigna el mismo
    manejador de eventos a todos excepto a los separadores */
    for (Component c : jPopupMenu.getComponents()) {
        //si el componente es un item
        if (c.getClass().equals(javax.swing.JMenuItem.class)) {
            ((JMenuItem) c).addActionListener(eventHandler);
        }
    }
}
...

Como el menú emergente solo se hace visible haciendo click sobre el área de edición, se necesita de un método que lo haga visible al detectar el correspondiente evento del ratón. Usualmente en la mayoría de los sistemas se tomaría un click izquierdo como el responsable de mostrar un menú de opciones, sin embargo esto no tiene porque ser siempre así. El manejador de eventos invocará al método void showPopupMenu() en la situación de un click del ratón, y este método averiguara si es el evento indicado para hacer visible el menú.

...
private void showPopupMenu(MouseEvent me) {
    if (me.isPopupTrigger() == true) {    //si el evento es el desencadenador del menú emergente
        //hace visible el menú emergente en las coordenadas actuales del ratón
        jPopupMenu.show(me.getComponent(), me.getX(), me.getY());
    }
}
...

Las opciones “Deshacer” y “Rehacer” están presentes en la barra de menú, la barra de herramientas y el menú emergente. Al inicio del programa y cuando se carga un nuevo documento estas opciones están desactivadas, y según el estado del documento actual estas opciones pueden activarse o desactivarse. El criterio para esto es manejado por el método void updateControls() el cual es invocado por el manejador de eventos cuando se detectan cambios en el área de edición, este método activa o desactiva estas opciones según lo que permita el administrador de edición basado en los límites del buffer.

...
void updateControls() {
    //averigua si se pueden deshacer los cambios en el documento actual
    boolean canUndo = undoManager.canUndo();
    //averigua si se pueden rehacer los cambios en el documento actual
    boolean canRedo = undoManager.canRedo();

    //activa o desactiva las opciones en la barra de menú
    mbItemUndo.setEnabled(canUndo);
    mbItemRedo.setEnabled(canRedo);

    //activa o desactiva las opciones en la barra de herramientas
    buttonUndo.setEnabled(canUndo);
    buttonRedo.setEnabled(canRedo);

    //activa o desactiva las opciones en el menú emergente
    mpItemUndo.setEnabled(canUndo);
    mpItemRedo.setEnabled(canRedo);
}
...

Algunos datos (instancias y variables) necesitan ser accedidos externamente por otras clases en el paquete textpademo. Para ello están los correspondientes getters y setters.

...
EventHandler getEventHandler() {    //retorna la instancia de EventHandler (la clase interna que maneja eventos)
    return eventHandler;
}

UndoManager getUndoManager() {    //retorna la instancia de UndoManager (administrador de edición)
    return undoManager;
}

boolean documentHasChanged() {    //retorna el estado del documento actual
    return hasChanged;
}

void setDocumentChanged(boolean hasChanged) {    //establece el estado del documento actual
    this.hasChanged = hasChanged;
}

JTextArea getJTextArea() {    //retorna la instancia de JTextArea (área de edición)
    return jTextArea;
}

JFrame getJFrame() {    //retorna la instancia de JFrame (ventana principal del editor)
    return jFrame;
}

File getCurrentFile() {    //retorna la instancia de File (el archivo actual)
    return currentFile;
}

void setCurrentFile(File currentFile) {    //establece el archivo actual
    this.currentFile = currentFile;
}

JLabel getJLabelFilePath() {    //retorna la instancia de la etiqueta sbFilePath
    return sbFilePath;
}

JLabel getJLabelFileSize() {    //retorna la instancia de la etiqueta sbFileSize
    return sbFileSize;
}
...

Hasta aquí hemos visto los métodos de la clase TPEditor, ahora continuamos con los métodos de la clase interna EventHander. En esta se sobrescriben e implementan una variedad de métodos necesarios para poder detectar los eventos en la GUI del editor y poder actuar en consecuencia llamando a la clase ActionPerformer, no obstante algunas operaciones serán ejecutadas aquí mismo.

Al implementar la interface java.awt.event.ActionListener se implementa el método void actionPerformed(), donde se averigua que botón o item de menú ah seleccionado el usuario y se realiza la operación correspondiente.

...
@Override
public void actionPerformed(ActionEvent ae) {
    String ac = ae.getActionCommand();    //se obtiene el nombre del comando ejecutado

    if (ac.equals("cmd_new") == true) {    //opción seleccionada: "Nuevo"
        actionPerformer.actionNew();
    } else if (ac.equals("cmd_open") == true) {    //opción seleccionada: "Abrir"
        actionPerformer.actionOpen();
    } else if (ac.equals("cmd_save") == true) {    //opción seleccionada: "Guardar"
        actionPerformer.actionSave();
    } else if (ac.equals("cmd_saveas") == true) {    //opción seleccionada: "Guardar como"
        actionPerformer.actionSaveAs();
    } else if (ac.equals("cmd_print") == true) {    //opción seleccionada: "Imprimir"
        actionPerformer.actionPrint();
    } else if (ac.equals("cmd_exit") == true) {    //opción seleccionada: "Salir"
        actionPerformer.actionExit();
    } else if (ac.equals("cmd_undo") == true) {    //opción seleccionada: "Deshacer"
        actionPerformer.actionUndo();
    } else if (ac.equals("cmd_redo") == true) {    //opción seleccionada: "Rehacer"
        actionPerformer.actionRedo();
    } else if (ac.equals("cmd_cut") == true) {    //opción seleccionada: "Cortar"
        //corta el texto seleccionado en el documento
        jTextArea.cut();
    } else if (ac.equals("cmd_copy") == true) {    //opción seleccionada: "Copiar"
        //copia el texto seleccionado en el documento
        jTextArea.copy();
    } else if (ac.equals("cmd_paste") == true) {    //opción seleccionada: "Pegar"
        //pega en el documento el texto del portapapeles
        jTextArea.paste();
    } else if (ac.equals("cmd_gotoline") == true) {    //opción seleccionada: "Ir a la línea..."
        actionPerformer.actionGoToLine();
    } else if (ac.equals("cmd_search") == true) {    //opción seleccionada: "Buscar"
        actionPerformer.actionSearch();
    } else if (ac.equals("cmd_searchnext") == true) {    //opción seleccionada: "Buscar siguiente"
        actionPerformer.actionSearchNext();
    } else if (ac.equals("cmd_selectall") == true) {    //opción seleccionada: "Seleccionar todo"
        jTextArea.selectAll();
    } else if (ac.equals("cmd_linewrap") == true) {    //opción seleccionada: "Ajuste de línea"
        //si esta propiedad esta activada se desactiva, o lo inverso
        jTextArea.setLineWrap(!jTextArea.getLineWrap());
        jTextArea.setWrapStyleWord(!jTextArea.getWrapStyleWord());
    } else if (ac.equals("cmd_showtoolbar") == true) {    //opción seleccionada: "Ver barra de herramientas"
        //si la barra de herramientas esta visible se oculta, o lo inverso
        jToolBar.setVisible(!jToolBar.isVisible());
    } else if (ac.equals("cmd_fixedtoolbar") == true) {    //opción seleccionada: "Fijar barra de herramientas"
        //si esta propiedad esta activada se desactiva, o lo inverso
        jToolBar.setFloatable(!jToolBar.isFloatable());
    } else if (ac.equals("cmd_showstatusbar") == true) {    //opción seleccionada: "Ver barra de estado"
        //si la barra de estado esta visible se oculta, o lo inverso
        statusBar.setVisible(!statusBar.isVisible());
    } else if (ac.equals("cmd_font") == true) {    //opción seleccionada: "Fuente de letra"
        actionPerformer.actionSelectFont();
    } else if (ac.equals("cmd_fontcolor") == true) {    //opción seleccionada: "Color de letra"
        actionPerformer.actionSelectFontColor();
    } else if (ac.equals("cmd_backgroundcolor") == true) {    //opción seleccionada: "Color de fondo"
        actionPerformer.actionSelectBackgroundColor();
    } else if (ac.equals("cmd_about") == true) {    //opción seleccionada: "Acerca de"
        //presenta un dialogo modal con alguna informacion
        JOptionPane.showMessageDialog(jFrame,
                                      "TextPad Demo por Dark[byte]",
                                      "Acerca de",
                                      JOptionPane.INFORMATION_MESSAGE);
    }
}
...

Al implementar la interface javax.swing.event.CaretListener se implementa el método void caretUpdate(), el cual es invocado cuando el cursor se mueve en el área de edición. Aquí mismo se realizan unos simples cálculos para mostrar en la barra de estado la información sobre la posición actual del cursor.

...
@Override
public void caretUpdate(CaretEvent e) {
    final int caretPos;  //valor de la posición del cursor sin inicializar
    int y = 1;           //valor de la línea inicialmente en 1
    int x = 1;           //valor de la columna inicialmente en 1

    try {
        //obtiene la posición del cursor con respecto al inicio del JTextArea (área de edición)
        caretPos = jTextArea.getCaretPosition();
        //sabiendo lo anterior se obtiene el valor de la línea actual (se cuenta desde 0)
        y = jTextArea.getLineOfOffset(caretPos);

        /** a la posición del cursor se le resta la posición del inicio de la línea para
           determinar el valor de la columna actual */
        x = caretPos - jTextArea.getLineStartOffset(y);

        //al valor de la línea actual se le suma 1 porque estas comienzan contándose desde 0
        y += 1;
    } catch (BadLocationException ex) {    //en caso de que ocurra una excepción
        System.err.println(ex);
    }

    /** muestra la información recolectada en la etiqueta sbCaretPos de la
       barra de estado, también se incluye el número total de lineas */
    sbCaretPos.setText("Líneas: " + jTextArea.getLineCount() + " - Y: " + y + " - X: " + x);
}
...

Al implementar la interface javax.swing.event.UndoableEditListener se implementa el método void undoableEditHappened(), el cual es invocado cuando el usuario realiza algún cambio en el área de edición, es decir sobre el documento. El administrador de edición guarda estos cambios en un buffer y permite al usuario deshacer o rehacer. Sin embargo el buffer fue limitado a 50 registros, por lo cual mas allá de ese valor no se guardaran los cambios.

Este evento es también tomado como referencia para cambiar el estado del documento actual representado por la variable hasChanged, esta al valer true significa que el documento actual no ah sido guardado, por lo que en caso de que el usuario intente cerrar abruptamente el editor se le ofrecerá guardar los cambios.

...
@Override
public void undoableEditHappened(UndoableEditEvent uee) {
    /* el cambio realizado en el área de edición se guarda en el buffer
       del administrador de edición */
    undoManager.addEdit(uee.getEdit());
    updateControls();     //actualiza el estado de las opciones "Deshacer" y "Rehacer"

    hasChanged = true;    //marca el documento como modificado
}
...

Al extender la clase java.awt.event.MouseAdapter se sobrescriben los métodos void mousePressed(MouseEvent me) y void mouseReleased(MouseEvent me), los cuales permiten detectar el click del ratón sobre el área de edición. Ambos invocan al método void showPopupMenu(), definido en la clase TPEditor, con el fin de hacer visible el menú emergente.

...
@Override
public void mousePressed(MouseEvent me) {
    showPopupMenu(me);
}

@Override
public void mouseReleased(MouseEvent me) {
    showPopupMenu(me);
}
...

Archivo ActionPerformer.java

El archivo de código fuente ActionPerformer.java contiene la clase publica ActionPerformer que se encarga de ejecutar la mayoría de las acciones solicitadas por el manejador de eventos, algunas acciones muy simples son ejecutadas en el propio manejador de eventos sin necesitar llegar hasta aquí y otras acciones muy complejas requieren de clases adicionales (por ejemplo PrintAction) que se explicarán más adelante.

La clase ActionPerformer también contiene una clase anónima interna que define un filtro de extensiones de archivo.

Comencemos con el esqueleto de este archivo, se definen las clases, métodos (sin contenido), y variables:

package textpademo;    //se define el paquete donde debe estar este archivo

import java.awt.Color;    //importamos todo lo que se utilizará
import java.awt.Font;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.regex.Pattern;
import javax.swing.JColorChooser;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;
import javax.swing.text.BadLocationException;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;

public class ActionPerformer {    //clase publica ActionPerformer

    private final TPEditor tpEditor;    //instancia de TPEditor (la clase principal)
    private String lastSearch = "";     //la última búsqueda de texto realizada, por defecto no contiene nada

    public ActionPerformer(TPEditor tpEditor) {    //constructor de la clase ActionPerformer
    }

    public void actionNew() {    //opción seleccionada: "Nuevo"
    }

    public void actionOpen() {    //opción seleccionada: "Abrir"
    }

    public void actionSave() {    //opción seleccionada: "Guardar"
    }

    public void actionSaveAs() {    //opción seleccionada: "Guardar como"
    }

    public void actionPrint() {    //opción seleccionada: "Imprimir"
    }

    public void actionExit() {    //opción seleccionada: "Salir"
    }

    public void actionUndo() {    //opción seleccionada: "Deshacer"
    }

    public void actionRedo() {    //opción seleccionada: "Rehacer"
    }

    public void actionSearch() {    //opción seleccionada: "Buscar"
    }

    public void actionSearchNext() {    //opción seleccionada: "Buscar siguiente"
    }

    public void actionGoToLine() {    //opción seleccionada: "Ir a la línea..."
    }

    public void actionSelectFont() {   //opción seleccionada: "Fuente de letra"
    }

    public void actionSelectFontColor() {    //opción seleccionada: "Color de letra"
    }

    public void actionSelectBackgroundColor() {    //opción seleccionada: "Color de fondo"
    }

    private static JFileChooser getJFileChooser() {    //retorna un JFileChooser
    }

    //clase anónima interna que define un filtro de extensiones
    private static FileFilter textFileFilter = new FileFilter() {
        public boolean accept(File f) {
        }

        public String getDescription() {
        }
    };

    private static String shortPathName(String longpath) {    //comprime una ruta de archivo muy larga
    }

    private static String roundFileSize(long length) {    //retorna el tamaño de un archivo redondeado
    }
}

Al instanciar esta clase el constructor recibe como argumento una instancia de la clase principal TPEditor y la guarda para poder acceder a datos y métodos de la misma.

...
public ActionPerformer(TPEditor tpEditor) {
    this.tpEditor = tpEditor;    //guarda la instancia de la clase TPEditor
}
...

El método void actionNew() es ejecutado cuando el usuario selecciona la opción “Nuevo“. El documento actual en el área de edición es reemplazado por uno nuevo vacío, lo cual no significa que se crea un archivo de este nuevo documento en el disco duro. Si el documento actual esta marcado como modificado, es decir la variable hasChanged vale true, entonces se le ofrece al usuario guardar los cambios físicamente antes de crear el nuevo documento. Básicamente lo que se hace aquí es reusar el área de edición para poder escribir otro documento.

...
public void actionNew() {
    if (tpEditor.documentHasChanged() == true) {    //si el documento esta marcado como modificado
        //se le ofrece al usuario guardar los cambios
        int option = JOptionPane.showConfirmDialog(tpEditor.getJFrame(), "¿Desea guardar los cambios?");

        switch (option) {
            case JOptionPane.YES_OPTION:       //si elige que si
                actionSaveFile();              //se guarda el archivo
                break;
            case JOptionPane.CANCEL_OPTION:    //si elige cancelar
                return;                        //se cancela esta operación
            //en otro caso se continúa con la operación y no se guarda el documento actual
        }
    }

    tpEditor.getJFrame().setTitle("TextPad Demo - Sin Título");    //nuevo título de la ventana

    //limpia el contenido del area de edición
    tpEditor.getJTextArea().setText("");
    //limpia el contenido de las etiquetas en la barra de estado
    tpEditor.getJLabelFilePath().setText("");
    tpEditor.getJLabelFileSize().setText("");

    tpEditor.getUndoManager().die();    //limpia el buffer del administrador de edición
    tpEditor.updateControls();          //actualiza el estado de las opciones "Deshacer" y "Rehacer"

    //el archivo asociado al documento actual se establece como null
    tpEditor.setCurrentFile(null);
    //marca el estado del documento como no modificado
    tpEditor.setDocumentChanged(false);
}
...

El método void actionOpen() es ejecutado cuando el usuario selecciona la opción “Abrir“. Se le presenta al usuario una ventana de dialogo donde elegir un archivo de texto presente en el sistema, se carga su contenido en el área de edición mostrando así un nuevo documento. Si el documento actual esta marcado como modificado, es decir la variable hasChanged vale true, entonces se le ofrece al usuario guardar los cambios antes de cargar el archivo. En esta operación podría ocurrir una excepción si el usuario no tuviera los permisos necesarios para abrir el archivo elegido.

...
public void actionOpen() {
    if (tpEditor.documentHasChanged() == true) {    //si el documento esta marcado como modificado
        //le ofrece al usuario guardar los cambios
        int option = JOptionPane.showConfirmDialog(tpEditor.getJFrame(), "¿Desea guardar los cambios?");

        switch (option) {
            case JOptionPane.YES_OPTION:     //si elige que si
                actionSaveFile();            //se guarda el archivo
                break;
            case JOptionPane.CANCEL_OPTION:  //si elige cancelar
                return;                      //se cancela esta operación
            //en otro caso se continúa con la operación y no se guarda el documento actual
        }
    }

    JFileChooser fc = getJFileChooser();    //obtiene un JFileChooser

    //se presenta un dialogo modal para que el usuario seleccione un archivo
    int state = fc.showOpenDialog(tpEditor.getJFrame());

    if (state == JFileChooser.APPROVE_OPTION) {    //si elige abrir el archivo
        File f = fc.getSelectedFile();    //obtiene el archivo seleccionado

        try {
            //abre un flujo de datos desde el archivo seleccionado
            BufferedReader br = new BufferedReader(new FileReader(f));
            tpEditor.getJTextArea().read(br, null);    //lee desde el flujo de datos hacia el área de edición
            br.close();    //cierra el flujo de datos

            //asigna el manejador de eventos para registrar los cambios en el nuevo documento actual
            tpEditor.getJTextArea().getDocument().addUndoableEditListener(tpEditor.getEventHandler());

            tpEditor.getUndoManager().die();    //limpia el buffer del administrador de edición
            tpEditor.updateControls();          //actualiza el estado de las opciones "Deshacer" y "Rehacer"

            //nuevo título de la ventana con el nombre del archivo abierto
            tpEditor.getJFrame().setTitle("TextPad Demo - " + f.getName());

            //muestra la ubicación del archivo abierto
            tpEditor.getJLabelFilePath().setText(shortPathName(f.getAbsolutePath()));
            //muestra el tamaño del archivo abierto
            tpEditor.getJLabelFileSize().setText(roundFileSize(f.length()));

            //establece el archivo abierto como el archivo actual
            tpEditor.setCurrentFile(f);
            //marca el estado del documento como no modificado
            tpEditor.setDocumentChanged(false);
        } catch (IOException ex) {    //en caso de que ocurra una excepción
            //presenta un dialogo modal con alguna información de la excepción
            JOptionPane.showMessageDialog(tpEditor.getJFrame(),
                                          ex.getMessage(),
                                          ex.toString(),
                                          JOptionPane.ERROR_MESSAGE);
        }
    }
}
...

El método void actionSave() es ejecutado cuando el usuario selecciona la opción “Guardar“. Se guarda el contenido del área de edición en el archivo actual. Al igual que en cualquier programa, el usuario utiliza esta opción normalmente cuando el documento ya fue guardado anteriormente, es decir ya existe un archivo asociado al documento actual, pero si este no fuera el caso entonces este método desvía la operación hacia el método void actionSaveAs() que se explica luego.

...
public void actionSave() {
    if (tpEditor.getCurrentFile() == null) {    //si no hay un archivo asociado al documento actual
        actionSaveAs();    //invoca el método actionSaveAs()
    } else if (tpEditor.documentHasChanged() == true) {    //si el documento esta marcado como modificado
        try {
            //abre un flujo de datos hacia el archivo asociado al documento actual
            BufferedWriter bw = new BufferedWriter(new FileWriter(tpEditor.getCurrentFile()));
            //escribe desde el flujo de datos hacia el archivo
            tpEditor.getJTextArea().write(bw);
            bw.close();    //cierra el flujo

            //marca el estado del documento como no modificado
            tpEditor.setDocumentChanged(false);
        } catch (IOException ex) {    //en caso de que ocurra una excepción
            //presenta un dialogo modal con alguna información de la excepción
            JOptionPane.showMessageDialog(tpEditor.getJFrame(),
                                          ex.getMessage(),
                                          ex.toString(),
                                          JOptionPane.ERROR_MESSAGE);
        }
    }
}
...

El método void actionSaveAs() es ejecutado cuando el usuario selecciona la opción “Guardar como“. Se le presenta al usuario una ventana de dialogo donde elegir en que ubicación prefiere guardar el documento actual. Al igual que en cualquier programa, el usuario utilizará esta opción normalmente cuando el documento no fue guardado anteriormente, es decir cuando todavía no existe un archivo asociado al documento actual.

...
public void actionSaveAs() {
    JFileChooser fc = getJFileChooser();    //se obtiene un JFileChooser

    //presenta un dialogo modal para que el usuario seleccione un archivo
    int state = fc.showSaveDialog(tpEditor.getJFrame());
    if (state == JFileChooser.APPROVE_OPTION) {    //si elige guardar en el archivo
        File f = fc.getSelectedFile();    //se obtiene el archivo seleccionado

        try {
            //abre un flujo de datos hacia el archivo asociado seleccionado
            BufferedWriter bw = new BufferedWriter(new FileWriter(f));
            //escribe desde el flujo de datos hacia el archivo
            tpEditor.getJTextArea().write(bw);
            bw.close();    //cierra el flujo

            //nuevo título de la ventana con el nombre del archivo guardado
            tpEditor.getJFrame().setTitle("TextPad Demo - " + f.getName());

            //muestra la ubicación del archivo guardado
            tpEditor.getJLabelFilePath().setText(shortPathName(f.getAbsolutePath()));
            //muestra el tamaño del archivo guardado
            tpEditor.getJLabelFileSize().setText(roundFileSize(f.length()));

            //establece el archivo guardado como el archivo actual
            tpEditor.setCurrentFile(f);
            //marca el estado del documento como no modificado
            tpEditor.setDocumentChanged(false);
        } catch (IOException ex) {    //en caso de que ocurra una excepción
            //presenta un dialogo modal con alguna información de la excepción
            JOptionPane.showMessageDialog(tpEditor.getJFrame(),
                                          ex.getMessage(),
                                          ex.toString(),
                                          JOptionPane.ERROR_MESSAGE);
        }
    }
}
...

El método void actionPrint() es ejecutado cuando el usuario selecciona la opción “Imprimir“. Si el documento actual no esta vacío, se invoca al método estático boolean print() de la clase PrintAction para presentar al usuario una ventana de dialogo donde configurar la impresión. Aquí participa nuestra clase PrintAction que implementa la interface Printable.

La comprobación para saber si el documento no esta vacío se podría implementar también en otras operaciones del editor. La impresión retorna un valor booleano en caso de éxito o lo contrario, pero en este caso se esta ignorando ya que no se hace nada útil con el mismo.

Nota: Desde J2SE 6 la alternativa mas sencilla y prudente es utilizar los nuevos métodos sobrecargados boolean print(...) definidos en la clase JTextComponent y disponibles también en sus subclases como por ejemplo en el JTextArea.

...
public void actionPrint() {
    boolean result = false;    //resultado de la impresión, por defecto es false

    //si el documento actual no esta vacío
    if (tpEditor.getJTextArea().getText().trim().equals("") == false) {
        //invoca nuestra la clase PrintAction para presentar el dialogo de impresión
        result = PrintAction.print(tpEditor.getJTextArea(), tpEditor.getJFrame());
    }
}
...

El método void actionExit() es ejecutado cuando el usuario selecciona la opción “Salir” o intenta cerrar la ventana. Si el documento actual esta marcado como modificado, es decir la variable hasChanged vale true, entonces se le ofrece al usuario guardar los cambios antes de finalizar el programa.

...
public void actionExit() {
    if (tpEditor.documentHasChanged() == true) {    //si el documento esta marcado como modificado
        //se le ofrece al usuario guardar los cambios
        int option = JOptionPane.showConfirmDialog(tpEditor.getJFrame(), "¿Desea guardar los cambios?");

        switch (option) {
            case JOptionPane.YES_OPTION:     //si elige que si
                actionSave();                //se guarda el archivo
                break;
            case JOptionPane.CANCEL_OPTION:  //si elige cancelar
                return;                      //se cancela esta operación
            //en otro caso se continúa con la operación y no se guarda el documento actual
        }
    }

    System.exit(0);    //se finaliza el programa con el código 0 (sin error)
}
...

El método void actionUndo() es ejecutado cuando el usuario selecciona la opción “Deshacer“. A través del administrador de edición se deshace el último cambio realizado en el documento actual.

...
public void actionUndo() {
    try {
        //deshace el último cambio realizado sobre el documento en el área de edición
        tpEditor.getUndoManager().undo();
    } catch (CannotUndoException ex) {    //en caso de que ocurra una excepción
        System.err.println(ex);
    }

    //actualiza el estado de las opciones "Deshacer" y "Rehacer"
    tpEditor.updateControls();
}
...

El método void actionRedo() es ejecutado cuando el usuario selecciona la opción “Rehacer“. A través del administrador de edición se rehace el último cambio realizado en el documento actual.

...
public void actionRedo() {
    try {
        //rehace el último cambio realizado sobre el documento en el área de edición
        tpEditor.getUndoManager().redo();
    } catch (CannotRedoException ex) {    //en caso de que ocurra una excepción
        System.err.println(ex);
    }

    //actualiza el estado de las opciones "Deshacer" y "Rehacer"
    tpEditor.updateControls();
}
...

El método void actionSearch() es ejecutado cuando el usuario selecciona la opción “Buscar“. Se le presenta al usuario una ventana de dialogo donde introducir el texto a buscar en el área de edición, si se encuentra se selecciona para resaltarlo en el área de edición, de lo contrario no sucede nada. Luego de una primera búsqueda se pueden realizar búsquedas subsecuentes con la opción “Buscar Siguiente” que se explica luego.

...
public void actionSearch() {
    //solicita al usuario que introduzca el texto a buscar
    String text = JOptionPane.showInputDialog(
            tpEditor.getJFrame(),
            "Texto:",
            "TextPad Demo - Buscar",
            JOptionPane.QUESTION_MESSAGE);

    if (text != null) {    //si se introdujo texto (puede ser una cadena vacía)
        String textAreaContent = tpEditor.getJTextArea().getText();    //obtiene todo el contenido del área de edición
        int pos = textAreaContent.indexOf(text);    //obtiene la posición de la primera ocurrencia del texto

        if (pos > -1) {    //si la posición es mayor a -1 significa que la búsqueda fue positiva
            //selecciona el texto en el área de edición para resaltarlo
            tpEditor.getJTextArea().select(pos, pos + text.length());
        }

        //establece el texto buscado como el texto de la última búsqueda realizada
        lastSearch = text;
    }
}
...

El método void actionSearchNext() es ejecutado cuando el usuario selecciona la opción “Buscar siguiente“. Realiza una búsqueda del texto guardado en la última búsqueda, es decir el contenido de lastSearch, si se encuentra se selecciona para resaltarlo en el área de edición, de lo contrario no sucede nada. Este método busca a partir de la posición actual del cursor en el área de edición. Si la última busqueda no contiene nada entonces este método desvía la operación hacia el método void actionSearch() que fue explicado antes.

...
public void actionSearchNext() {
    if (lastSearch.isEmpty() == false) {    //si la última búsqueda contiene texto
        String textAreaContent = tpEditor.getJTextArea().getText();    //se obtiene todo el contenido del área de edición
        int pos = tpEditor.getJTextArea().getCaretPosition();    //se obtiene la posición del cursor sobre el área de edición
        //buscando a partir desde la posición del cursor, se obtiene la posición de la primera ocurrencia del texto
        pos = textAreaContent.indexOf(lastSearch, pos);

        if (pos > -1) {    //si la posición es mayor a -1 significa que la búsqueda fue positiva
            //selecciona el texto en el área de edición para resaltarlo
            tpEditor.getJTextArea().select(pos, pos + lastSearch.length());
        }
    } else {    //si la última búsqueda no contiene nada
        actionSearch();    //invoca el método actionSearch()
    }
}
...

El método void actionGoToLine() es ejecutado cuando el usuario selecciona la opción “Ir a la línea…“. Se le presenta al usuario una ventana de dialogo donde introducir el número de línea, y si este un dato coherente, es decir se encuentra dentro de los limites del área de edición, se posiciona el cursor en el inicio de la línea solicitada.

...
public void actionGoToLine() {
    //solicita al usuario que introduzca el número de línea
    String line = JOptionPane.showInputDialog(
            tpEditor.getJFrame(),
            "Número:",
            "TextPad Demo - Ir a la línea...",
            JOptionPane.QUESTION_MESSAGE);

    if (line != null && line.length() > 0) {    //si se introdujo un dato
        try {
            int pos = Integer.parseInt(line);    //el dato introducido se convierte en entero

            //si el número de línea esta dentro de los límites del área de texto
            if (pos >= 0 && pos <= tpEditor.getJTextArea().getLineCount()) {
                //posiciona el cursor en el inicio de la línea
                tpEditor.getJTextArea().setCaretPosition(tpEditor.getJTextArea().getLineStartOffset(pos));
            }
        } catch (NumberFormatException ex) {    //en caso de que ocurran excepciones
            System.err.println(ex);
        } catch (BadLocationException ex) {
            System.err.println(ex);
        }
    }
}
...

El método void actionSelectFont() es ejecutado cuando el usuario selecciona la opción “Fuente de letra“. Se le presenta al usuario un dialogo donde elegir el fuente que prefiere para la letra en el área de edición. Aquí participa nuestra clase JFontChooser que extiende de JDialog.

...
public void actionSelectFont() {
    //presenta el dialogo de selección de fuentes
    Font font = JFontChooser.showDialog(tpEditor.getJFrame(),
                                        "TextPad Demo - Fuente de letra:",
                                        null);
    if (font != null) {    //si un fuente fue seleccionado
        //se establece como fuente del área de edición
        tpEditor.getJTextArea().setFont(font);
    }
}
...

El método void actionSelectFontColor() es ejecutado cuando el usuario selecciona la opción “Color de letra“. Se le presenta al usuario un dialogo donde elegir el color que prefiere para la letra en el área de edición.

...
public void actionSelectFontColor() {
    //presenta el dialogo de selección de colores
    Color color = JColorChooser.showDialog(tpEditor.getJFrame(),
                                           "TextPad Demo - Color de letra:",
                                           tpEditor.getJTextArea().getForeground());
    if (color != null) {    //si un color fue seleccionado
        //se establece como color del fuente y cursor
        tpEditor.getJTextArea().setForeground(color);
        tpEditor.getJTextArea().setCaretColor(color);
    }
}
...

El método void actionSelectBackgroundColor() es ejecutado cuando el usuario selecciona la opción “Color de fondo“. Se le presenta al usuario un dialogo donde elegir el color que prefiere para el fondo del área de edición.

...
public void actionSelectBackgroundColor() {
    //presenta el dialogo de selección de colores
    Color color = JColorChooser.showDialog(tpEditor.getJFrame(),
                                           "TextPad Demo - Color de fondo:",
                                           tpEditor.getJTextArea().getForeground());
    if (color != null) {    //si un color fue seleccionado
        //se establece como color de fondo
        tpEditor.getJTextArea().setBackground(color);
    }
}
...

El método JFileChooser getJFileChooser() es utilizado por otros métodos para obtener un JFileChooser construido y configurado listo para usar donde el usuario puede elegir un archivo del sistema. Se utiliza un filtro de extensiones para mostrar en principio solo los archivos de texto (idealmente los “.txt“), aunque el usuario puede cambiar el tipo de archivo y elegir cualquiera.

...
private static JFileChooser getJFileChooser() {
    JFileChooser fc = new JFileChooser();                     //construye un JFileChooser
    fc.setDialogTitle("TextPad Demo - Elige un archivo:");    //se le establece un título
    fc.setMultiSelectionEnabled(false);                       //desactiva la multi-selección
    fc.setFileFilter(textFileFilter);                         //aplica un filtro de extensiones
    return fc;    //retorna el JFileChooser
}
...

Aquí tenemos una clase anónima interna donde se sobrescriben los métodos necesarios de javax.swing.filechooser.FileFilter para aplicar un filtro de extensiones en el JFileChooser.

Nota: Desde J2SE 6, gracias a nuevas clases incorporadas en la API, esto podría realizarse sencillamente con: new FileNameExtensionFilter("Text Files", "txt").

...
private static FileFilter textFileFilter = new FileFilter() {
    public boolean accept(File f) {
        //acepta directorios y archivos de extensión .txt
        return f.isDirectory() || f.getName().toLowerCase().endsWith("txt");
    }

    public String getDescription() {
        //la descripción del tipo de archivo aceptado
        return "Text Files";
    }
};
...

El método String shortPathName(String longpath) es utilizado por otros métodos para obtener la ruta completa de la ubicación de un archivo en forma comprimida. Es un procedimiento sencillo donde cada nombre de directorio que tenga 10 o mas caracteres será reducido a sus 3 primeros caracteres añadiéndosele unos “…” que indican la reducción. El propósito es mostrar la ruta completa (en realidad comprimida) en la barra de estado.

...
private static String shortPathName(String longPath) {
    //construye un arreglo de cadenas, donde cada una es un nombre de directorio
    String[] tokens = longPath.split(Pattern.quote(File.separator));

    //construye un StringBuilder donde se añadira el resultado
    StringBuilder shortpath = new StringBuilder();

    //itera sobre el arreglo de cadenas
    for (int i = 0 ; i < tokens.length ; i++) {
        if (i == tokens.length - 1) {             //si la cadena actual es la última, es el nombre del archivo
            shortpath.append(tokens[i]);    //se añade al resultado sin separador
            break;                          //se termina el bucle
        } else if (tokens[i].length() >= 10) {    //si la cadena actual tiene 10 o más caracteres
            //se toman los primeros 3 caracteres y se añade al resultado con un separador
            shortpath.append(tokens[i].substring(0, 3)).append("...").append(File.separator);
        } else {                                  //si la cadena actual tiene menos de 10 caracteres
            //se añade al resultado con un separador
            shortpath.append(tokens[i]).append(File.separator);
        }
    }

    return shortpath.toString();    //retorna la cadena resultante
}
...

El método String roundFileSize(long length) es utilizado por otros métodos para obtener el tamaño del archivo actual redondeado en KiloBytes cuando este es mayor a 1024 bytes.

...
private static String roundFileSize(long length) {
    //retorna el tamaño del archivo redondeado
    return (length < 1024) ? length + " bytes" : (length / 1024) + " Kbytes";
}
...

Archivo PrintAction.java

El archivo de código fuente PrintAction.java contiene la clase publica PrintAction que se encarga de imprimir el documento presente en el área de edición. Se implementa la interface java.awt.print.Printable con la cual se sigue un modelo de impresión secuencial de las paginas renderizadas del documento, comenzando con la pagina 0.

Dentro de PrintAction tenemos la clase interna PrintingMessageBox que extiende java.swing.JDialog, se utiliza para presentarle al usuario una ventana de dialogo modal que muestra la información de la pagina que se esta imprimiendo y una opción para permitir abortar la operación.

Comencemos con el esqueleto de este archivo, se definen las clases, métodos (sin contenido), y variables:

package textpademo;    //se define el paquete donde debe estar este archivo

import java.awt.BorderLayout;    //importamos todo lo que se utilizará
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.WindowConstants;

public class PrintAction implements Printable {    //clase publica que implementa la interface Printable

    private final JTextArea jTextArea;    //área de edición donde se encuentra el documento actual
    private JDialog dialog;               //dialogo de estado, muestra el estado de la impresión
    private int[] pageBreaks;             //arreglo de quiebres de página
    private String[] textLines;           //arreglo de líneas de texto
    private int currentPage = -1;         //página actual impresa, por defecto inicilizada en -1
    private boolean result = false;       //resultado de la impresión, por defecto es negativo

    public PrintAction(JComponent jComponent) {    //constructor de la clase PrintAction
    }

    //método estático conveniente para inicializar la clase PrintAction
    public static boolean print(JComponent jComponent, Frame owner) {
    }

    //presenta el dialogo de impresión e inicia la impresión del documento
    public boolean printDialog(Frame owner) {
    }

    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {    //implemento de la interface Printable
    }

    private void updateStatus(int pageIndex) {    //actualiza el estado de la impresión en el dialogo de estado
    }

    //clase que extiende JDialog, construye un dialogo modal
    private class PrintingMessageBox extends JDialog {

        private JLabel lbStatusMsg;    //etiqueta que muestra el estado de impresión

        public PrintingMessageBox(Frame owner, final PrinterJob pj) {    //constructor de la clase PrintingMessageBox
        }

        public void setStatusMsg(String statusMsg){    //establece el texto de la etiqueta lbStatusMsg
        }
    }
}

Al instanciar la clase PrintAction su constructor recibe como argumento una instancia del componente JTextArea (área de edición) y la guarda.

...
public PrintAction(JComponent jComponent) {
    this.jTextArea = (JTextArea) jComponent;    //guarda la instancia del área de edición
}
...

El método estático boolean print(JComponent jComponent, Frame owner) es invocado directamente desde la clase ActionPerformer. Construye e inicia la clase PrintAction para realizar la impresión del documento presente en el área de edición. Retorna true en caso de éxito o false en caso contrario.

...
public static boolean print(JComponent jComponent, Frame owner) {
    PrintAction pa = new PrintAction(jComponent);   //construye una instancia de PrintAction
    return pa.printDialog(owner);                   //inicia la impresión y retorna un valor booleano
}
...

El método boolean printDialog(Frame owner) realiza la impresión del documento. Se le presenta al usuario una ventana de dialogo donde configurar las propiedades de impresión, esta luego es iniciada en un hilo externo al EDT (Event Dispatch Thread) y durante su transcurso se presenta otro dialogo mostrando el estado para informar al usuario.

...
public boolean printDialog(Frame owner) {
    //construye un trabajo de impresión
    final PrinterJob pj = PrinterJob.getPrinterJob();
    //construye un conjunto de atributos para la impresión
    final PrintRequestAttributeSet pras = new HashPrintRequestAttributeSet();
    //establece a la clase PrintAction como responsable de renderizar las páginas del documento
    pj.setPrintable(this);

    boolean option = pj.printDialog(pras);    //presenta un dialogo de impresión

    if (option == true) {    //si el usuario acepta
        //construye el dialogo modal de estado sobre la ventana padre
        dialog = new PrintingMessageBox(owner, pj);

        //crea un nuevo hilo para que se ocupe de la impresión
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    pj.print();                        //inicia la impresión
                    PrintAction.this.result = true;    //resultado positivo
                } catch (PrinterException ex) {        //en caso de que ocurra una excepción
                    System.err.println(ex);
                }

                dialog.setVisible(false);    //oculta el dialogo de estado
            }
        }).start();    //inicia el hilo de impresión

        dialog.setVisible(true);     //hace visible el dialogo de estado
    }

    return PrintAction.this.result;    //retorna el resultado de la impresión
}
...

El método int print(Graphics g, PageFormat pf, int pageIndex) implementado de la interface Printable es invocado secuencialmente por el sistema de impresión para solicitar cada pagina a imprimir. La primera ves que este método es invocado se calculan los quiebres de pagina (pageBreaks) necesarios para imprimir todo el documento, cada uno de estos corresponde a cada pagina que será solicitada en la cual cabe una determinada cantidad de líneas.

Esta implementación trata el contenido del área de edición como lo que es, simple texto plano, por lo tanto se ignora el fuente o los colores.

...
public int print(Graphics g, PageFormat pf, int pageIndex) {
    Graphics2D g2d = (Graphics2D) g;                      //conversión de gráficos simples a gráficos 2D
    g2d.setFont(new Font("Serif", Font.PLAIN, 10));       //establece un fuente para todo el texto
    int lineHeight = g2d.getFontMetrics().getHeight();    //obtiene la altura del fuente

    if (pageBreaks == null) {    //si los quiebres de página no fueron calculados
        //construye un arreglo con las líneas de texto presentes en el área de edición
        textLines = jTextArea.getText().split("\n");
        //calcula el número de líneas que caben en cada página
        int linesPerPage = (int) (pf.getImageableHeight() / lineHeight);
        //calcula el número de quiebres de página necesarios para imprimir todo el documento
        int numBreaks = (textLines.length - 1) / linesPerPage;
        //construye un arreglo con los quiebres de página 
        pageBreaks = new int[numBreaks];
        for (int i = 0 ; i < numBreaks ; i++) {
            //se calcula la posición para cada quiebre de página
            pageBreaks[i] = (i + 1) * linesPerPage;
        }
    }

    //si el índice de página solicitado es menor o igual que la cantidad de quiebres total
    if (pageIndex <= pageBreaks.length) {
        /* establece una igualdad entre el origen del espacio gráfico (x:0,y:0) y el origen 
        del área imprimible definido por el formato de página */
        g2d.translate(pf.getImageableX(), pf.getImageableY());

        int y = 0;    //coordenada "y", inicializada en 0 (principio de página)
        //obtiene la primera línea para la página actual
        int startLine = (pageIndex == 0) ? 0 : pageBreaks[pageIndex - 1];
        //obtiene la última línea para la página actual
        int endLine = (pageIndex == pageBreaks.length) ? textLines.length : pageBreaks[pageIndex];

        //itera sobre las líneas que forman parte de la página actual
        for (int line = startLine ; line < endLine ; line++) {
            y += lineHeight;                          //aumenta la coordenada "y" para cada línea
            g2d.drawString(textLines[line], 0, y);    //imprime la linea en las coordenadas actuales
        }

        updateStatus(pageIndex);    //actualiza el estado de impresión

        return PAGE_EXISTS;     //la página solicitada será impresa
    } else {
        return NO_SUCH_PAGE;    //la pagina solicitada no es valida
    }
}
...

El método void updateStatus(int pageIndex) es invocado por int print(Graphics g, PageFormat pf, int pageIndex) en el transcurso de la impresión para actualizar el estado de la impresión en el dialogo de estado, donde se muestra la página que se esta imprimiendo actualmente.

Es normal que el método int print(Graphics g, PageFormat pf, int pageIndex) sea invocado por el sistema de impresión más de una ves para una misma página, lo que significa que la actualización del estado se podría ejecutar innecesariamente. Por ese motivo antes de actualizar se comprueba que la página actual sea diferente al índice de la página impresa, de lo contrario no hay nada que actualizar.

Conforme a la metodología de Swing, el acceso desde un hilo externo a la GUI del dialogo de estado el cual se encuentra en el EDT (Event Dispatch Thread), se realiza de forma segura a través del método void invokeLater(Runnable doRun).

...
private void updateStatus(int pageIndex) {
    if (pageIndex != currentPage) {
        currentPage++;    //incrementa la página actual    

        //acceso seguro al EDT (Event Dispatch Thread) para actualizar la GUI
        javax.swing.SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                //actualiza la información de la etiqueta lbStatusMsg
                ((PrintingMessageBox) dialog).setStatusMsg("Imprimiendo página " + (currentPage + 1) + " ...");
            }
        });
    }
}
...

Hasta aquí hemos visto los métodos de la clase PrintAction, ahora continuamos con los métodos de la clase interna PrintingMessageBox. Esta extiende javax.swing.JDialog para presentarle al usuario una sencilla ventana modal de dialogo donde se muestra el estado actual de la impresión y también un botón que permite cancelar la impresión en curso.

En el constructor se realiza todo lo necesario para crear esta ventana.

...
public PrintingMessageBox(Frame owner, final PrinterJob pj) {
    /** invoca el constructor de la superclase para establecer la ventana padre, el título
    de la ventana, y que será una ventana modal */
    super(owner, "Impresión", true);
    setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

    //construye y configura la etiqueta que muestra el estado
    lbStatusMsg = new JLabel("Iniciando ...");
    lbStatusMsg.setPreferredSize(new Dimension(200, 30));
    lbStatusMsg.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

    //construye el botón de cancelar
    JButton buttonCancel = new JButton("Cancelar");
    JPanel jp = new JPanel();
    jp.add(buttonCancel);

    //asigna un manejador de eventos para el botón de cancelar
    buttonCancel.addActionListener(new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            pj.cancel();          //cancela el trabajo de impresión
            setVisible(false);    //oculta esta ventana
        }
    });

    getContentPane().add(lbStatusMsg, BorderLayout.CENTER);    //añade la etiqueta en el CENTRO
    getContentPane().add(jp, BorderLayout.SOUTH);              //añade el botón, orientación SUR

    //asigna un manejador de eventos para cuando la ventana pierde la visibilidad
    this.addComponentListener(new ComponentAdapter() {

        @Override
        public void componentHidden(ComponentEvent e) {
            Window w = (Window) e.getComponent();    //convierte el componente afectado en una ventana
            w.dispose();                             //destruye la ventana
        }
    });

    setResizable(false);             //no se permite redimensionar la ventana
    pack();                          //se le da el tamaño preferido
    setLocationRelativeTo(owner);    //centra la ventana sobre el editor de texto
}
...

Además del constructor, el otro método presente en esta clase interna es void setStatusMsg(String statusMsg). Este es invocado desde la clase exterior PrintAction para actualizar el estado de impresión.

...
public void setStatusMsg(String statusMsg) {
    lbStatusMsg.setText(statusMsg);    //establece el texto de la etiqueta lbStatusMsg
}
...

Archivo JFontChooser.java

Como la API estándar de Java no ofrece algún componente ya hecho para que el usuario pueda elegir un fuente, similar a como javax.swing.JColorChooser permite elegir un color, tenemos aquí una sencilla implementación de JComponent y JDialog para crear dicho componente.

El archivo de código fuente JFontChooser.java contiene la clase publica JFontChooser que se encarga de presentarle al usuario una ventana de dialogo donde elegir el fuente para el área de texto. Se extiende la clase javax.swing.JComponent de la cual deben de extender todos los componentes de Swing que no sean contenedores.

La clase FontChooserDialog que extiende javax.swing.JDialog es invocada por la clase JFontChooser para construir una ventana de dialogo modal, la mayoría de los eventos en esta ventana son manejados por la clase interna EventHandler (similar a como realizamos anteriormente con la ventana principal del editor).

El aspecto de este componente:

Comencemos con el esqueleto de este archivo, se definen las clases, métodos (sin contenido), y variables:

package textpademo;    //se define el paquete donde debe estar este archivo

import java.awt.Color;    //importamos todo lo que se utilizará
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Arrays;
import java.util.Comparator;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.Position;

public class JFontChooser extends JComponent {    //clase publica que extiende JComponent

    private final Font initialFont;    //fuente inicial
    private Font font;                 //fuente seleccionado por el usuario

    public JFontChooser(Font initialFont) {    //constructor de la clase JFontChooser
    }

    //método estático conveniente para inicializar la clase JFontChooser
    public static Font showDialog(Frame owner, String title, Font initialFont) {
    }

    public Font getInitialFont() {    //retorna el fuente inicial
    }

    public Font getSelectedFont() {    //retorna el fuente seleccionado
    }

    public void setSelectedFont(Font font) {    //establece el fuente seleccionado
    }
}

class FontChooserDialog extends JDialog {    //clase que extiende JDialog

    private JTextField textFieldNames;     //campo de texto para el nombre del fuente
    private JTextField textFieldStyles;    //campo de texto para el estilo del fuente
    private JTextField textFieldSizes;     //campo de texto para el tamaño del fuente
    private JList listFontNames;     //lista para nombres de fuente
    private JList listFontStyles;    //lista para estilos de fuente
    private JList listFontSizes;     //lista para tamaños de fuente
    private JLabel textExample;    //etiqueta que muestra un ejemplo del fuente seleccionado

    //arreglo con nombres de fuente disponibles
    private static final String[] FONT_NAMES = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
    //arreglo con estilos de fuente
    private static final String[] FONT_STYLES = {
        "Normal", "Bold", "Italic", "Bold Italic"
    };
    //arreglo con tamaños de fuente
    private static final String[] FONT_SIZES = {
        "8", "9", "10", "11", "12", "13", "14", "16", "18", "20", "24", "28", "32", "48", "72"
    };

    //constructor de la clase FontChooserDialog, construye un dialogo modal
    public FontChooserDialog(Frame owner, String title, final JFontChooser jFontChooser) {
    }

    //clase interna que extiende KeyAdapter e implementa ListSelectionListener y Comparator
    class EventHandler extends KeyAdapter implements Comparator,
                                                     ListSelectionListener {
        @Override
        public void keyReleased(KeyEvent ke) {    //herencia de la clase KeyAdapter
        }

        @Override
        public int compare(String string1, String string2) {    //implemento de la interface Comparator
        }

        @Override
        public void valueChanged(ListSelectionEvent lse) {    //implemento de la interface ListSelectionListener
        }
    }

    public Font getSelectedFont() {    //retorna el fuente seleccionado
    }
}

Al instanciar la clase JFontChooser su constructor recibe como argumento el fuente inicial y lo guarda. Este será el primer fuente seleccionado al hacerse visible el dialogo.

...
public JFontChooser(Font initialFont) {
    this.initialFont = initialFont;    //guarda el fuente inicial
}
...

El método estático Font showDialog(Frame owner, String title, Font initialFont) es invocado directamente desde la clase ActionPerformer. Construye e inicia la clase JFontChooser para presentar al usuario el dialogo de selección de fuente. Retorna el fuente seleccionado.

...
public static Font showDialog(Frame owner, String title, Font initialFont) {
    JFontChooser fontChooser = new JFontChooser(initialFont);    //construye una instancia de JFontChooser

    //construye el dialogo de selección de fuente sobre la ventana padre
    JDialog dialog = new FontChooserDialog(owner, title, fontChooser);
    dialog.setVisible(true);    //hace visible el dialogo

    return fontChooser.getSelectedFont();    //retorna el fuente seleccionado
}
...

Los datos de la clase JFontChooser son accesibles desde el exterior. Aquí están los correspondientes getters y setters.

...
public Font getInitialFont() {    //retorna el fuente inicial
    return initialFont;
}

public Font getSelectedFont() {    //retorna el fuente seleccionado
    return font;
}

public void setSelectedFont(Font font) {    //establece el fuente seleccionado
    this.font = font;
}
...

Hasta aquí hemos visto los métodos de la clase JFontChooser, ahora continuamos con los métodos de la clase FontChooserDialog. Esta extiende JDialog para presentarle al usuario una ventana modal de dialogo donde elegir un fuente.

En el constructor se realiza todo lo necesario para crear esta ventana. La mayoría de los eventos se dejan a cargo de la clase interna EventHandler.

Nota: no confundir esta clase interna EventHandler con la que está dentro de la clase TPEditor. Las eh nombrado igual pero pertenecen a clases diferentes.

...
public FontChooserDialog(Frame owner, String title, final JFontChooser jFontChooser) {
    /** invoca el constructor de la superclase para establecer la ventana padre, el título
    de la ventana, y que será una ventana modal */
    super(owner, title, true);

    final EventHandler eventHandler = new EventHandler();    //construye una instancia de EventHandler
    JScrollPane jScrollPane;

    JPanel cp = (JPanel) getContentPane();                        //obtiene el panel de contenido principal
    cp.setLayout(new GridBagLayout());                            //establece un GridBagLayout
    cp.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));    //establece un borde de espacio

    //construye un conjunto de limitaciones para los componentes del GridBagLayout
    GridBagConstraints gbc = new GridBagConstraints();

    JLabel label1 = new JLabel("Name:");    //construye la etiqueta "Name:"
    gbc.insets = new Insets(0, 5, 0, 5);
    gbc.anchor = GridBagConstraints.FIRST_LINE_START;
    gbc.weightx = 0.5;
    gbc.gridx = 1;
    gbc.gridy = 1;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(label1, gbc);    //añade la etiqueta, coordenadas X:1 - Y:1

    JLabel label2 = new JLabel("Style:");    //construye la etiqueta "Style:"
    gbc.gridx = 2;
    gbc.gridy = 1;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(label2, gbc);    //añade la etiqueta, coordenadas X:2 - Y:1

    JLabel label3 = new JLabel("Size:");    //construye la etiqueta "Size:"
    gbc.gridx = 3;
    gbc.gridy = 1;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(label3, gbc);    //añade la etiqueta, coordenadas X:3 - Y:1

    textFieldNames = new JTextField("");    //construye el campo de texto para el nombre del fuente
    int fixedWidth = textFieldNames.getPreferredSize().width;
    Dimension fixedSize = new Dimension(fixedWidth, 20);
    textFieldNames.setMinimumSize(fixedSize);
    textFieldNames.setMaximumSize(fixedSize);
    textFieldNames.setPreferredSize(fixedSize);
    textFieldNames.addKeyListener(eventHandler);    //asigna el manejador de eventos para el teclado
    gbc.fill = GridBagConstraints.HORIZONTAL;
    gbc.gridx = 1;
    gbc.gridy = 2;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(textFieldNames, gbc);    //añade el campo de texto, coordenadas X:1 - Y:2

    textFieldStyles = new JTextField("");    //construye el campo de texto para el estilo del fuente
    fixedWidth = textFieldStyles.getPreferredSize().width;
    fixedSize = new Dimension(fixedWidth, 20);
    textFieldStyles.setMinimumSize(fixedSize);
    textFieldStyles.setMaximumSize(fixedSize);
    textFieldStyles.setPreferredSize(fixedSize);
    textFieldStyles.addKeyListener(eventHandler);    //asigna el manejador de eventos para el teclado
    gbc.fill = GridBagConstraints.HORIZONTAL;
    gbc.gridx = 2;
    gbc.gridy = 2;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(textFieldStyles, gbc);    //añade el campo de texto, coordenadas X:2 - Y:2

    textFieldSizes = new JTextField("");    //construye el campo de texto para el tamaño del fuente
    fixedWidth = textFieldSizes.getPreferredSize().width;
    fixedSize = new Dimension(fixedWidth, 20);
    textFieldSizes.setMinimumSize(fixedSize);
    textFieldSizes.setMaximumSize(fixedSize);
    textFieldSizes.setPreferredSize(fixedSize);
    textFieldSizes.addKeyListener(eventHandler);    //asigna el manejador de eventos para el teclado
    gbc.fill = GridBagConstraints.HORIZONTAL;
    gbc.gridx = 3;
    gbc.gridy = 2;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(textFieldSizes, gbc);    //añade el campo de texto, coordenadas X:3 - Y:2

    listFontNames = new JList(FONT_NAMES);    //construye la lista para nombres de fuente
    jScrollPane = new JScrollPane(listFontNames);
    String fontName = jFontChooser.getInitialFont().getName();
    listFontNames.setSelectedValue(fontName, true);    //selecciona el nombre de fuente inicial
    textFieldNames.setText(fontName);
    listFontNames.addListSelectionListener(eventHandler);
    gbc.gridx = 1;
    gbc.gridy = 3;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(jScrollPane, gbc);    //añade la lista, coordenadas X:1 - Y:3

    listFontStyles = new JList(FONT_STYLES);    //construye la lista para estilos de fuente
    jScrollPane = new JScrollPane(listFontStyles);
    int fontSyle = jFontChooser.getInitialFont().getStyle();
    listFontStyles.setSelectedIndex(fontSyle);    //selecciona el estilo de fuente inicial
    textFieldStyles.setText(listFontStyles.getSelectedValue().toString());
    listFontStyles.addListSelectionListener(eventHandler);
    gbc.gridx = 2;
    gbc.gridy = 3;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(jScrollPane, gbc);    //añade la lista, coordenadas X:2 - Y:3

    listFontSizes = new JList(FONT_SIZES);    //construye la lista para tamaños de fuente
    jScrollPane = new JScrollPane(listFontSizes);
    String fontSize = String.valueOf(jFontChooser.getInitialFont().getSize());
    listFontSizes.setSelectedValue(fontSize, true);    //selecciona el tamaño de fuente inicial
    textFieldSizes.setText(fontSize);
    listFontSizes.addListSelectionListener(eventHandler);
    gbc.gridx = 3;
    gbc.gridy = 3;
    gbc.gridwidth = 1;
    gbc.gridheight = 1;
    cp.add(jScrollPane, gbc);    //añade la lista, coordenadas X:3 - Y:3

    textExample = new JLabel("AaBaCcDdEeFfGgHhJj");        //contruye la etiqueta para mostrar un ejemplo del fuente seleccionado
    textExample.setHorizontalAlignment(JLabel.CENTER);
    textExample.setFont(jFontChooser.getInitialFont());    //establece el fuente inicial
    textExample.setOpaque(true);
    textExample.setBackground(Color.WHITE);
    textExample.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder("Texto de ejemplo"),
                                                             BorderFactory.createEmptyBorder(10, 10, 10, 10)));
    int fixedHeight = textExample.getPreferredSize().height;
    fixedSize = new Dimension(100, fixedHeight);
    textExample.setMinimumSize(fixedSize);
    textExample.setMaximumSize(fixedSize);
    textExample.setPreferredSize(fixedSize);
    gbc.gridx = 1;
    gbc.gridy = 4;
    gbc.gridwidth = 3;
    gbc.gridheight = 1;
    cp.add(textExample, gbc);    //añade la etiqueta, coordenadas X:1 - Y:4

    JPanel jp = new JPanel();                        //construye un panel para los botones
    JButton buttonOk = new JButton("Ok");            //construye el botón de aceptar
    JButton buttonCancel = new JButton("Cancel");    //construye el botón de cancelar
    jp.add(buttonOk);                                //añade los botones al panel
    jp.add(buttonCancel);
    gbc.anchor = GridBagConstraints.CENTER;
    gbc.gridx = 1;
    gbc.gridy = 5;
    cp.add(jp, gbc);    //añade el panel, coordenadas X:1 - Y:5

    //asigna un manejador de eventos para el botón de cancelar
    buttonOk.addActionListener(new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            jFontChooser.setSelectedFont(getSelectedFont());
            //fontChooser.font = getSelectedFont();
            setVisible(false);
        }
    });

    //asigna un manejador de eventos para el botón de cancelar
    buttonCancel.addActionListener(new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            setVisible(false);    //oculta esta ventana
        }
    });

    //asigna un manejador de eventos para el cierre del JDialog
    this.addWindowListener(new WindowAdapter() {

        @Override
        public void windowClosing(WindowEvent e) {
            Window w = e.getWindow();    //convierte el componente afectado en una ventana
            w.setVisible(false);         //oculta esta ventana
        }
    });

    //asigna un manejador de eventos para cuando la ventana pierde la visibilidad
    this.addComponentListener(new ComponentAdapter() {

        @Override
        public void componentHidden(ComponentEvent e) {
            Window w = (Window) e.getComponent();    //convierte el componente afectado en una ventana
            w.dispose();                             //destruye la ventana
        }
    });

    setResizable(false);              //no se permite redimensionar la ventana
    pack();                           //se le da el tamaño preferido
    setLocationRelativeTo(owner);     //la ventana se centra sobre el editor de texto
}
...

El método Font getSelectedFont() retorna un fuente como resultado de la combinación de un tipo, estilo y tamaño de fuente seleccionados en las listas del dialogo.

...
public Font getSelectedFont() {
    try {
        //retorna el fuente seleccionado en el dialogo
        return new Font(String.valueOf(listFontNames.getSelectedValue()), listFontStyles.getSelectedIndex(),
                        Integer.parseInt(String.valueOf(listFontSizes.getSelectedValue())));
    } catch (NumberFormatException nfe) {    //en caso de que ocurra una excepción
        System.err.println(nfe);
    }

    return null;    //retorna null
}
...

Hasta aquí hemos visto los métodos de la clase FontChooserDialog, ahora continuamos con los métodos de la clase interna EventHandler. En esta se sobrescriben e implementan métodos necesarios para detectar algunos eventos en la GUI del dialogo y poder actuar en consecuencia.

Al extender la clase java.awt.event.KeyAdapter se sobrescribe el método void keyReleased(KeyEvent ke), donde se detecta cuando el usuario libera una tecla sobre los campos de texto. Esto sirve para poder buscar en la lista coincidencias con lo que el usuario escribe, y entonces así ir desplazando la barra vertical manteniendo visible el texto en cuestión.

...
@Override
public void keyReleased(KeyEvent ke) {
    //obtiene el origen del evento, y lo convierte en un campo de texto
    final JTextField eventTField = (JTextField) ke.getSource();
    final String text = eventTField.getText();    //obtiene el contenido del campo de texto

    //averigua en que campo de texto se ah ejecutado el evento
    if (eventTField == textFieldNames) {    //si el campo de texto es textFieldNames
        //obtiene el índice del proximo elemento en la lista que coincida con el contenido del campo de texto
        int index = listFontNames.getNextMatch(text, 0, Position.Bias.Forward);

        if (index > -1) {    //si el índice es mayor que -1
            /* realiza una búsqueda binaria sobre el arreglo de nombres de fuente, se utiliza
            esta clase como comparador implementando la interface Comparator */
            if (Arrays.binarySearch(FONT_NAMES, text, this) > -1) {    //si el resultado es mayor que -1
                listFontNames.setSelectedIndex(index);    //selecciona el índice
            }

            listFontNames.ensureIndexIsVisible(index);    //hace el índice visible en la lista
        }
    } else if (eventTField == textFieldStyles) {        //si el campo de texto es textFieldStyles
        //obtiene el índice del proximo elemento en la lista que coincida con el contenido del campo de texto
        int index = listFontStyles.getNextMatch(text, 0, Position.Bias.Forward);

        if (index > -1) {    //si el índice es mayor que -1
            //itera sobre los elementos en el arreglo de estilos de fuente
            for (int i = 0 ; i < FONT_STYLES.length ; i++){
                //si el contenido del campo de texto es igual al elemento actual
                if (text.equalsIgnoreCase(FONT_STYLES[i]) == true) {
                    listFontStyles.setSelectedIndex(index);    //selecciona el índice
                }
            }

            listFontStyles.ensureIndexIsVisible(index);    //hace el índice visible en la lista
        }
    } else if (eventTField == textFieldSizes) {    //si el campo de texto es textFieldSizes
        //obtiene el índice del proximo elemento en la lista que coincida con el contenido del campo de texto
        int index = listFontSizes.getNextMatch(text, 0, Position.Bias.Forward);

        if (index > -1) {    //si el índice es mayor que -1
            //itera sobre los elementos en el arreglo de tamaños de fuente
            for (int i = 0 ; i < FONT_SIZES.length ; i++) {
                //si el contenido del campo de texto es igual al elemento actual
                if (text.equalsIgnoreCase(FONT_SIZES[i]) == true) {
                    listFontSizes.setSelectedIndex(index);    //selecciona el índice
                }
            }

            listFontSizes.ensureIndexIsVisible(index);    //hace el índice visible en la lista
        }
    }

    //establece el fuente seleccionado en la etiqueta de muestra
    textExample.setFont(getSelectedFont());
}
...

Al implementar la interface java.util.Comparator se implementa el método int compare(Object string1, Object string2), con el cual se realiza una comparación lexicográfica insensible de mayúsculas sobre dos cadenas de texto. Con esta implementación hemos convertido la clase EventHandler en un comparador que es utilizado para la búsqueda binaria realizada en el método void keyReleased(KeyEvent ke) visto anteriormente.

...
@Override
public int compare(String string1, String string2) {
    //compara dos cadenas de texto ignorando mayúsculas
    return string1.compareToIgnoreCase(string2);
}
...

Al implementar la interface javax.swing.event.ListSelectionListener se implementa el método void valueChanged(ListSelectionEvent lse), en el cual se detecta cuando cambia el elemento seleccionado en alguna de las listas del dialogo. Cuando esto sucede se actualizan el contenido del campo de texto relacionado con el elemento seleccionado y la etiqueta de muestra con el fuente seleccionado.

...
@Override
public void valueChanged(ListSelectionEvent lse) {
    //averigua en que lista se ah ejecutado el evento
    if (lse.getSource() == listFontNames) {    //si la lista es listFontNames
        //establece el contenido del campo de texto textFieldNames
        textFieldNames.setText(String.valueOf(listFontNames.getSelectedValue()));
    } else if (lse.getSource() == listFontStyles) {    //si la lista es listFontStyles
        //establece el contenido del campo de texto textFieldStyles
        textFieldStyles.setText(String.valueOf(listFontStyles.getSelectedValue()));
    } else if (lse.getSource() == listFontSizes) {    //si la lista es listFontSizes
        //establece el contenido del campo de texto textFieldSizes
        textFieldSizes.setText(String.valueOf(listFontSizes.getSelectedValue()));
    }

    //establece el fuente seleccionado en la etiqueta de muestra
    textExample.setFont(getSelectedFont());
}
...

Aquí finaliza el código del proyecto.

Estas son las imágenes .PNG 32×32 que se utilizan como recursos en el proyecto:

Mejorar el editor


Algunas cosas que se podrían tener en cuenta para mejorar o incluir en el editor:

  • El editor actualmente puede manejar un solo documento porque el área de edición es directamente un JTextArea. Normalmente las buenas aplicaciones de escritorio son de tipo MDI (multiple-document interface), las cuales pueden manejar la edición de varios documentos simultáneamente. El área de edición se puede representar con un panel contenedor JDesktopPane y según lo requerido crear en su interior ventanas internas JInternalFrame cada una con su propio JTextArea. De esta forma se podría tener un editor MDI.
  • El JTextArea es un componente especifico para texto plano. Si se pretende un editor que soporte formatos compuestos como .HTML o .RFT se puede utilizar un componente de texto mas avanzado como JEditorPane o un JTextPane (que extiende el anterior). Con la ayuda de librerías adicionales se pueden manipular también otros formatos, por ejemplo .PDF con iTEXT.
  • En caso de tratar con formatos compuestos la impresión del documento debería de respetar diferentes fuentes, colores, imágenes o lo que fuese. Por ejemplo se puede calcular cuantas páginas se necesitan para representar todo el contenido de un componente JTextPane, y utilizar el método void paint(Graphics g) (que ofrecen los componentes Swing) para ir dibujando el contenido en un objeto de gráficos para cada página que será impresa. Desde J2SE 6 existen convenientemente un grupo de métodos sobrecargados boolean print(…) disponibles en los componentes de texto que extienden de JTextComponent, estos hacen mas sencillo el trabajo.
  • Utilizar hilos obreros adicionales para cargar o guardar archivos, principalmente si se piensa trabajar con archivos grandes. Desde J2SE 6 es una buena opción utilizar la clase javax.swing.SwingWorker para crear estos hilos.
  • El administrador de edición (Deshacer\Rehacer) debería de guardar modificaciones continuas en un solo registro y no carácter por carácter, por ejemplo si el usuario mantiene presionada la tecla “A” cada nueva letra A se registra individualmente, esto es poco eficiente y deja sin espacio el buffer fácilmente.
  • El sistema de búsqueda de texto podría ofrecer más opciones al usuario como ignorar mayúsculas, buscar solo palabras completas, buscar hacia atrás en el texto, utilizar expresiones regulares, etc.
  • Junto al sistema de búsqueda se puede incluir la opción de reemplazar texto.
  • Permitir al usuario elegir la codificación de caracteres.
  • Colocar una columna en un lado del área de edición mostrando el número de cada línea.
  • Internacionalización. Para que la GUI del programa este adecuada al idioma local del sistema.
  • Permitir seleccionar otros LookAndFeel. Los que Swing ofrece, o utilizar alguna librería externa para dar una gran variedad.

Código fuente:
* TextPad Demo

Documentación:
* Java™ Platform, Standard Edition 6 API Specification


, , ,

  1. #1 por José Ramón el noviembre 1, 2011 - 3:47 am

    ¡¡Hola, Darkbyte!!
    Te escribo desde España y he descargado el código fuente del editor, porque tengo que hacer una aplicación en Java Swing. No se trata de un editor de texto, sino de una aplicación que, basada en formularios y/o documentos XML, genera determinados documentos en dvi o pdf.
    Tu ejemplo me va a resultar muy útil, porque no sabía cómo estructurar la aplicación: cuántas clases escribir en cada fichero, cómo manejar los eventos, etc. Así que MUCHÍSIMAS GRACIAS.
    El lenguaje que más he usado ha sido el C (además, uso Debian Linux), aunque también sé algo de C++, Perl, Bash o Python. Sin embargo, nunca había tenido que diseñar una aplicación de escritorio. Por eso, te quería hacer algunas preguntas, no sobre tu código, que ya estudiaré a fondo, sino sobre Java:

    – ¿Cuánto tiempo te llevó escribir el código del editor de texto, tal como lo tienes acá? Yo pensaba presentar en febrero mi aplicación Java, pero quizás sea demasiado optimista, ¿o no? Se trata de mi Proyecto de Fin de Carrera (no sé cómo lo llaman ustedes). Estaría tres meses codificando, a razón de unas 50 horas por semana. ¿piensas que es una estimación realista? Antes de responder, lee la siguiente pregunta.

    – Además de lo que te expliqué, la aplicación también tiene que ser web, no solo de escritorio. Mi pregunta es: ¿tengo que cambiar el diseño de la aplicación por el hecho de que vaya a ser una aplicación web?

    Como veo que tienes experiencia en Java, te agradecería cualquier otra consideración o simplemente, el enlace a alguna página web donde me puedan informar con más detalle.

    MUCHAS GRACIAS de nuevo y……., seguro que menciono tu página web en mi lista de “Fuentes consultadas”.

    Un saludo,
    José Ramón

    • #2 por Dark[byte] el noviembre 1, 2011 - 4:01 pm

      Hola José.
      Como ya te distes cuenta esto es solo un editor de texto lo mas sencillo posible con funcionalidad básica diseñado para funcionar desde Java 5 en adelante, y claro que también da una idea general de como se hace una aplicación de escritorio en Swing. Si no recuerdo mal creo que lo escribí en dos días, pero si me preguntas cuanto te va a llevar tu proyecto solo puedo decirte que eso es relativo a tus conocimientos, destreza, voluntad, dedicación, y diferentes obstáculos que se te van presentando. Cuando yo escribí esto no hice nada que no hubiera echo antes porque es muy básico, entonces no enfrenté ninguna situación que me complicara y quitara tiempo, si ese no es tu caso entonces te llevará un poco mas de tiempo ya que tendrás que aprender y luego escribir e ir de a poco.
      En cuanto a tu estimación, creo que lo terminaras antes de Febrero. No te dejes engañar por la cantidad de lineas que Java requiere, la mayoría es solo código redundante, y con la cantidad de bibliotecas que hay disponibles te será aún más fácil.
      Una solución a tu necesidad de que también sea una aplicación web seria usar Java Web Start, esto es preferible a usar Applets y no requiere modificar el código, es comúnmente muy utilizado. Es una forma de ejecutar aplicaciones .JAR a través de la web, todo esto funciona a través de lo que se llama Java Network Launch Protocol (JNLP). Primero que nada el PC del usuario tiene que tener instalado como mínimo J2SE 1.4.2, luego para tener acceso a la aplicación el usuario hace click en un link en tu web y el navegador descarga un archivo .JNLP que es el encargado de definir la ejecución del .JAR que es luego descargado por el navegador. La próxima vez que el usuario haga click en el link se comprueba si hay una nueva versión de la aplicación, si la hay entonces el proceso de descarga sucede de nuevo, si no la hay entonces el navegador ejecuta directamente el .JAR que ya ah descargado anteriormente. Te dejo este link del tutorial oficial, te explica como se hace el archivo .JNLP y el enlace al mismo, además te muestra un ejemplo.
      Suerte y salu2!.

  2. #3 por Eduardo Fernazdez el octubre 25, 2012 - 9:10 pm

    ola dark byte como puedo hacer esta aplicasion pero en un JFrame ojala y me puedas proporcinar ayuda gracias porfavor

    • #4 por Dark[byte] el diciembre 14, 2012 - 9:43 pm

      Hola Eduardo:

      Esta aplicación utiliza un JFrame. No se si entiendo tu pregunta.

      Saludos.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: