/*
Applet Jaxe - Editeur XML en Java

Copyright (C) 2007 Observatoire de Paris-Meudon

Ce programme est un logiciel libre ; vous pouvez le redistribuer et/ou le modifier conformément aux dispositions de la Licence Publique Générale GNU, telle que publiée par la Free Software Foundation ; version 2 de la licence, ou encore (à votre choix) toute version ultérieure.

Ce programme est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de COMMERCIALISATION ou D'ADAPTATION A UN OBJET PARTICULIER. Pour plus de détail, voir la Licence Publique Générale GNU .

Vous devez avoir reçu un exemplaire de la Licence Publique Générale GNU en même temps que ce programme ; si ce n'est pas le cas, écrivez à la Free Software Foundation Inc., 675 Mass Ave, Cambridge, MA 02139, Etats-Unis.
*/

package jaxeapplet;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.ResourceBundle;

import javax.swing.*;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.*;
import javax.swing.text.DefaultEditorKit.CopyAction;
import javax.swing.text.DefaultEditorKit.CutAction;
import javax.swing.text.DefaultEditorKit.PasteAction;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.UndoManager;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.swabunga.spell.engine.SpellDictionary;
import com.swabunga.spell.event.SpellChecker;
import com.swabunga.spell.swing.JTextComponentSpellChecker;

import jaxe.*;
import jaxe.elements.JESwing;


/**
 * Fenêtre d'édition d'un document XML
 */
public class JaxeAppletFrame extends JFrame implements EcouteurMAJ {
    
    protected static boolean iconeValide = false;
    protected static ResourceBundle rb = JaxeResourceBundle.getRB();
    protected URL urlFichier;
    protected URL urlEnregistrement;
    protected JaxeDocument doc;
    protected JaxeTextPane textPane;
    protected JScrollPane paneScrollPane;
    protected JMenuBar barreInsertion;
    protected JTabbedPane sidepane ;
    protected ArbreXML arbrexml ;
    protected AllowedElementsPanel allowed;
    protected AttributePanel attpane;
    protected JSplitPane split;
    protected CaretListenerLabel caretListenerLabel;
    protected UndoAction monActionAnnuler;
    protected RedoAction monActionRetablir;
    protected boolean afficherSide = true;
    protected boolean afficherArbre = true;
    protected boolean afficherAllowed = true;
    protected boolean afficherAttributs = true;
    protected Action actionFermeture = null;
    //protected boolean aEnregistrer = false;
    protected AppletSourceFrame fenetreSource = null;
    protected AppletValidationFrame fenetreValidation = null;
    protected URL urlDictionnaire = null;
    protected URL urlPhonetique = null;
    
    
    /**
     * Constructeur utilisant une URL pour le document XML, avec un dictionnaire
     *
     * @param urlCfg  URL du fichier de config de Jaxe
     * @param urlFichier  URL du document XML
     * @param urlDictionnaire  URL du fichier .dico du dictionnaire
     * @param urlPhonetique  URL du fichier .phon du dictionnaire
     */
    public JaxeAppletFrame(final URL urlCfg, final URL urlFichier, final URL urlEnregistrement,
            final URL urlDictionnaire, final URL urlPhonetique, boolean nouveau) {
        super(urlFichier.getPath());
        this.urlFichier = urlFichier;
        this.urlEnregistrement = urlEnregistrement;
        this.urlDictionnaire = urlDictionnaire;
        this.urlPhonetique = urlPhonetique;
        if (nouveau) {
            try {
                doc = new JaxeDocument(new Config(urlCfg, true));
                textPane = new JaxeTextPane(doc, this, iconeValide);
                doc.nouveau();
                doc.furl = urlFichier;
            } catch (JaxeException ex) {
                JOptionPane.showMessageDialog(null, ex.getMessage(),
                    rb.getString("erreur.Erreur"), JOptionPane.ERROR_MESSAGE);
                doc = new JaxeDocument();
                textPane = new JaxeTextPane(doc, this, iconeValide);
            }
        } else {
            doc = new JaxeDocument();
            textPane = new JaxeTextPane(doc, this, iconeValide);
            doc.lire(urlFichier, urlCfg);
        }
        initGUI();
        if (!nouveau)
            sidepane.setSelectedComponent(arbrexml);
    }
    
    /**
     * Constructeur utilisant une URL pour le document XML
     *
     * @param urlCfg  URL du fichier de config de Jaxe
     * @param urlFichier  URL du document XML
     */
    public JaxeAppletFrame(final URL urlCfg, final URL urlFichier, final URL urlEnregistrement, boolean nouveau) {
        super(urlFichier.getPath());
        this.urlFichier = urlFichier;
        this.urlEnregistrement = urlEnregistrement;
        if (nouveau) {
            try {
                doc = new JaxeDocument(new Config(urlCfg, true));
                textPane = new JaxeTextPane(doc, this, iconeValide);
                doc.nouveau();
                doc.furl = urlFichier;
            } catch (JaxeException ex) {
                JOptionPane.showMessageDialog(null, ex.getMessage(),
                    rb.getString("erreur.Erreur"), JOptionPane.ERROR_MESSAGE);
                doc = new JaxeDocument();
                textPane = new JaxeTextPane(doc, this, iconeValide);
            }
        } else {
            doc = new JaxeDocument();
            textPane = new JaxeTextPane(doc, this, iconeValide);
            doc.lire(urlFichier, urlCfg);
        }
        initGUI();
        if (!nouveau)
            sidepane.setSelectedComponent(arbrexml);
    }
    
    /**
     * Constructeur utilisant un document DOM pour le document XML
     *
     * @param urlCfg  URL du fichier de config de Jaxe
     * @param documentDOM  Document DOM à éditer, null pour un nouveau document
     */
    public JaxeAppletFrame(final URL urlCfg, final Document documentDOM) {
        super("Jaxe");
        urlFichier = null;
        urlEnregistrement = null;
        if (documentDOM != null) {
            doc = new JaxeDocument();
            textPane = new JaxeTextPane(doc, this, iconeValide);
            doc.setDOMDoc(documentDOM, urlCfg);
        } else {
            try {
                doc = new JaxeDocument(new Config(urlCfg, true));
            } catch (JaxeException ex) {
                JOptionPane.showMessageDialog(null, ex.getMessage(),
                    rb.getString("erreur.Erreur"), JOptionPane.ERROR_MESSAGE);
                doc = new JaxeDocument();
            }
            textPane = new JaxeTextPane(doc, this, iconeValide);
            doc.nouveau();
        }
        initGUI();
        if (documentDOM != null)
            sidepane.setSelectedComponent(arbrexml);
    }
    
    /**
     * Renvoit le document DOM actuel
     */
    public Document getDocumentDOM() {
        return(doc.DOMdoc);
    }
    
    public URL getURLEnregistrement() {
        return (urlEnregistrement);
    }
    
    public AppletSourceFrame getSourceFrame() {
        return(fenetreSource);
    }
    
    public void setSourceFrame(final AppletSourceFrame sourceFrame) {
        fenetreSource = sourceFrame;
    }
    
    /**
     * Renvoit un Reader pour le code source
     */
    public Reader getReader() throws IOException {
        return(doc.getReader());
    }
    
    /**
     * Envoit le code source dans un Writer
     */
    public void sendToWriter(final Writer destination) {
        doc.sendToWriter(destination);
    }
    
    /**
     * Definit l'action appelée en cas de fermeture de la fenêtre
     */
    public void setActionFermeture(Action actionFermeture) {
        this.actionFermeture = actionFermeture;
    }
    
    protected void initGUI() {
        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(final WindowEvent e) {
                fermer();
            }
        });
        final JPanel contentPane = new JPanel(new BorderLayout());
        setContentPane(contentPane);
        //contentPane.add(textPane, BorderLayout.CENTER);
        caretListenerLabel = new CaretListenerLabel(" ", doc);
        contentPane.add(caretListenerLabel, BorderLayout.SOUTH);
        if (doc.cfg != null)
            setupMenuBar();
        //fixBuggySafari();
        textPane.ajouterEcouteurAnnulation(this);
        paneScrollPane = new JScrollPane(textPane);
        paneScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        paneScrollPane.setPreferredSize(new Dimension(500, 400));
        paneScrollPane.setMinimumSize(new Dimension(100, 50));
        modifierSide();
        textPane.addCaretListener(caretListenerLabel);
        
        final Dimension ecran = getToolkit().getScreenSize();
        int largeur = (ecran.width * 2) / 3;
        if (largeur < 750)
            largeur = ecran.width - 20;
        int hauteur = (ecran.height * 3) / 4;
        if (hauteur < 550)
            hauteur = ecran.height - 50;
        setSize(new Dimension(largeur, hauteur));
        setLocation((ecran.width - largeur)/2, (ecran.height - hauteur)/2);
        addWindowFocusListener(new WindowAdapter() {
            public void windowGainedFocus(WindowEvent e) {
                textPane.requestFocusInWindow();
            }
        });
    }
    
    protected void setupMenuBar() {
        barreInsertion = doc.cfg.makeMenus(doc);
        
        final int cmdMenu = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
        
        JMenu menuFichier = new JMenu(rb.getString("menus.Fichier"));
        
        if (urlEnregistrement != null) {
            JMenuItem miEnregistrer = menuFichier.add(new ActionEnregistrer());
            miEnregistrer.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_S, cmdMenu));
            
            menuFichier.addSeparator();
        }
        
        JMenuItem miSource = menuFichier.add(new ActionSource());
        JMenuItem miValider = menuFichier.add(new ActionValider());
        
        menuFichier.addSeparator();
        
        JMenuItem miFermer = menuFichier.add(new ActionFermer());
        miFermer.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_W, cmdMenu));
        
        barreInsertion.add(menuFichier, 0);
        
        JMenu menuEdition = new JMenu(rb.getString("menus.Edition"));
        
        monActionAnnuler = new UndoAction();
        JMenuItem miAnnuler = menuEdition.add(monActionAnnuler);
        miAnnuler.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_Z, cmdMenu));
        monActionRetablir = new RedoAction();
        JMenuItem miRetablir = menuEdition.add(monActionRetablir);
        miRetablir.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_R, cmdMenu));
        
        menuEdition.addSeparator();
        
        // on utilise DefaultEditorKit.CopyAction, CutAction et PasteAction au lieu de classes locales pour éviter
        // que le système de contrôle de lecture du presse-papier par une applet ne bloquer un copier-coller
        final TextAction actionCouper = new DefaultEditorKit.CutAction();
        actionCouper.putValue(Action.NAME, rb.getString("menus.Couper"));
        JMenuItem miCouper = menuEdition.add(actionCouper);
        miCouper.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_X, cmdMenu));
        final TextAction actionCopier = new DefaultEditorKit.CopyAction();
        actionCopier.putValue(Action.NAME, rb.getString("menus.Copier"));
        JMenuItem miCopier = menuEdition.add(actionCopier);
        miCopier.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_C, cmdMenu));
        final TextAction actionColler = new DefaultEditorKit.PasteAction();
        actionColler.putValue(Action.NAME, rb.getString("menus.Coller"));
        JMenuItem miColler = menuEdition.add(actionColler);
        miColler.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_V, cmdMenu));
        JMenuItem miToutSelectionner = menuEdition.add(new ActionToutSelectionner());
        miToutSelectionner.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_A, cmdMenu));
        
        menuEdition.addSeparator();
        
        JMenuItem miRechercher = menuEdition.add(new ActionRechercher());
        miRechercher.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F, cmdMenu));
        JMenuItem miSuivant = menuEdition.add(new ActionSuivant());
        miSuivant.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_G, cmdMenu));
        
        if (urlDictionnaire != null) {
            menuEdition.addSeparator();
            menuEdition.add(new ActionOrthographe());
        }
        
        barreInsertion.add(menuEdition, 1);
        if (barreInsertion != null)
            setJMenuBar(barreInsertion);
    }
    
    /*protected void fixBuggySafari() {
        if (barreInsertion == null)
            return;
        // pour Safari, qui gobe tous les raccourcis claviers avec commande, on ajoute contrôle
        final int cmdMenu = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
        if (cmdMenu != Event.CTRL_MASK) {
            final Keymap kmap = textPane.getKeymap();
            KeyStroke[] touches = kmap.getBoundKeyStrokes();
            for (final KeyStroke touche : touches) {
                if ((touche.getModifiers() | cmdMenu) != 0) {
                    final KeyStroke ctrlLettre = KeyStroke.getKeyStroke(touche.getKeyCode(), Event.CTRL_MASK);
                    kmap.addActionForKeyStroke(ctrlLettre, kmap.getAction(touche));
                }
            }
            for (int i=0; i<barreInsertion.getMenuCount(); i++) {
                final JMenu menu = barreInsertion.getMenu(i);
                for (int j=0; j<menu.getItemCount(); j++) {
                    final JMenuItem item = menu.getItem(j);
                    if (item != null) {
                        final KeyStroke touche = item.getAccelerator();
                        if (touche != null) {
                            if ((touche.getModifiers() | cmdMenu) != 0) {
                                final KeyStroke ctrlLettre = KeyStroke.getKeyStroke(touche.getKeyCode(), Event.CTRL_MASK);
                                kmap.addActionForKeyStroke(ctrlLettre, item.getAction());
                            }
                        }
                    }
                }
            }
        }
    }*/
    
    public void miseAJour() {
        monActionAnnuler.updateUndoState();
        monActionRetablir.updateRedoState();
    }
    
    /**
     * Mise à jour des menus (grisé / non grisé) avec la liste des éléments autorisées
     */
    public void majMenus(final int pos) {
        if (doc.cfg == null || barreInsertion == null || textPane.getIgnorerEdition())
            return;
        JaxeElement parent = null;
        if (doc.rootJE != null)
            parent = doc.rootJE.elementA(pos);
        if (parent != null && parent.debut.getOffset() == pos &&
                !(parent instanceof JESwing))
            parent = parent.getParent() ;
        if (parent != null && parent.noeud.getNodeType() == Node.TEXT_NODE)
            parent = parent.getParent();
        ArrayList<Element> autorisees = null;
        Config parentconf = null;
        if (parent == null) {
            parentconf = doc.cfg;
            autorisees = doc.cfg.listeElementsRacines();
        } else if (parent.noeud.getNodeType() == Node.COMMENT_NODE) {
            parentconf = doc.cfg;
            autorisees = new ArrayList<Element>();
        } else {
            final Element parentref = parent.refElement;
            if (parentref == null)
                return;
            parentconf = doc.cfg.getRefConf(parentref);
            final ArrayList<Element> sousElements = parentconf.listeSousElements(parentref);
            if (sousElements != null) {
                autorisees = new ArrayList<Element>();
                final int debutSelection = textPane.getSelectionStart();
                final int finSelection = textPane.getSelectionEnd();
                for (final Element ref : sousElements) {
                    if (parentconf == null || parent == null ||
                            parentconf.insertionPossible(parent, debutSelection, finSelection, ref)) {
                        autorisees.add(ref);
                    }
                }
            }
        }
        for (int i=2; i<barreInsertion.getMenuCount(); i++) { // attention au i=2
            final JMenu menu = barreInsertion.getMenu(i);
            majMenu(menu, parentconf, autorisees);
        }
    }
    
    protected boolean majMenu(final JMenu menu, final Config parentconf, final ArrayList<Element> autorisees) {
        boolean anyenab = false;
        for (int i=0; i<menu.getItemCount(); i++) {
            final JMenuItem item = menu.getItem(i);
            if (item != null) {
                final Action action = item.getAction();
                if (action instanceof ActionInsertionBalise) {
                    final Element refElement = ((ActionInsertionBalise)action).getRefElement();
                    if (refElement != null) {
                        final Config conf = doc.cfg.getRefConf(refElement);
                        final String nomElement = conf.nomElement(refElement);
                        if (conf == parentconf) {
                            boolean enable = false;
                            for (final Element ref : autorisees)
                                if (nomElement.equals(doc.cfg.nomElement(ref))) {
                                    enable = true;
                                    anyenab = true;
                                    if (refElement != ref) // cas de 2 éléments du schéma avec le même nom
                                        ((ActionInsertionBalise)action).setRefElement(ref);
                                    break;
                                }
                            action.setEnabled(enable);
                        } else
                            action.setEnabled(true);
                    }
                } else if (item instanceof JMenu)
                    anyenab = majMenu((JMenu)item, parentconf, autorisees) || anyenab;
            }
        }
        if (!menu.isTopLevelMenu())
            menu.setEnabled(anyenab);
        return(anyenab);
    }
    
    /**
     * Changer l'affichage de la zone de gauche (visible ou non)
     */
    public void setAffichageSide(final boolean visible) {
        if (afficherSide != visible) {
            if (afficherArbre)
                textPane.retirerEcouteurArbre(arbrexml);
            if (afficherAllowed)
                textPane.retirerEcouteurArbre(allowed);
            if (afficherAttributs)
                textPane.retirerEcouteurArbre(attpane);
            if (!afficherSide)
                getContentPane().remove(paneScrollPane);
            else
                getContentPane().remove(split);
            afficherSide = visible;
            modifierSide();
            validate();
            textPane.getCaret().setVisible(true);
        }
    }
    
    public boolean getAffichageSide() {
        return(afficherSide);
    }
    
    /**
     * Changer l'affichage de l'arbre (visible ou non)
     */
    public void setAffichageArbre(final boolean visible) {
        if (afficherArbre != visible) {
            if (!visible)
                textPane.retirerEcouteurArbre(arbrexml);
            if (afficherAllowed)
                textPane.retirerEcouteurArbre(allowed);
            if (afficherAttributs)
                textPane.retirerEcouteurArbre(attpane);
            if (!afficherSide)
                getContentPane().remove(paneScrollPane);
            else
                getContentPane().remove(split);
            afficherArbre = visible;
            afficherSide = (afficherArbre || afficherAllowed || afficherAttributs);
            modifierSide();
            validate();
            textPane.getCaret().setVisible(true);
        }
    }
    
    public boolean getAffichageArbre() {
        return(afficherArbre);
    }
    
    /**
     * Changer l'affichage de la liste d'éléments autorisés à l'endroit du curseur (visible ou non)
     */
    public void setAffichageAllowed(final boolean visible) {
        if (afficherAllowed != visible) {
            if (afficherArbre)
                textPane.retirerEcouteurArbre(arbrexml);
            if (!visible)
                textPane.retirerEcouteurArbre(allowed);
            if (afficherAttributs)
                textPane.retirerEcouteurArbre(attpane);
            if (!afficherSide)
                getContentPane().remove(paneScrollPane);
            else
                getContentPane().remove(split);
            afficherAllowed = visible;
            afficherSide = (afficherArbre || afficherAllowed || afficherAttributs);
            modifierSide();
            validate();
            textPane.getCaret().setVisible(true);
        }
    }
    
    public boolean getAffichageAllowed() {
        return(afficherAllowed);
    }
    
    /**
     * Changer l'affichage de la liste d'attributs (visible ou non)
     */
    public void setAffichageAttributs(final boolean visible) {
        if (afficherAttributs != visible) {
            if (afficherArbre)
                textPane.retirerEcouteurArbre(arbrexml);
            if (afficherAllowed)
                textPane.retirerEcouteurArbre(allowed);
            if (!visible)
                textPane.retirerEcouteurArbre(attpane);
            if (!afficherSide)
                getContentPane().remove(paneScrollPane);
            else
                getContentPane().remove(split);
            afficherAttributs = visible;
            afficherSide = (afficherArbre || afficherAllowed || afficherAttributs);
            modifierSide();
            validate();
            textPane.getCaret().setVisible(true);
        }
    }
    
    public boolean getAffichageAttributs() {
        return(afficherAttributs);
    }
    
    /**
     * Mise à jour de l'affichage
     */
    protected void modifierSide() {
        if (afficherSide) {
            sidepane = new JTabbedPane();
            if (afficherAllowed) {
                allowed = new AllowedElementsPanel((JaxeDocument)textPane.getDocument());
                textPane.addCaretListener(allowed);
                textPane.ajouterEcouteurArbre(allowed);
                sidepane.addTab(rb.getString("tabs.insertion"), allowed);
            } else
                allowed = null;
            
            if (afficherArbre) {
                arbrexml = new ArbreXML(doc) ;
                textPane.ajouterEcouteurArbre(arbrexml);
                sidepane.addTab(rb.getString("tabs.arbre"), arbrexml);
            } else
                arbrexml = null;
            
            if (afficherAttributs) {
                attpane = new AttributePanel((JaxeDocument)textPane.getDocument());
                textPane.addCaretListener(attpane);
                textPane.ajouterEcouteurArbre(attpane);
                sidepane.addTab(rb.getString("tabs.attributs"), attpane);
            } else
                attpane = null;
            
            split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
            split.setLeftComponent(sidepane);
            split.setRightComponent(paneScrollPane);
            split.setDividerLocation(275);
            getContentPane().add(split, BorderLayout.CENTER);
        } else {
            sidepane = null;
            arbrexml = null;
            allowed = null;
            attpane = null;
            getContentPane().add(paneScrollPane, BorderLayout.CENTER);
        }
    }
    
    protected void fermer() {
        if (doc != null && doc.modif && urlEnregistrement != null) {
            // JOptionPane.showConfirmDialog(this... générait des fuites de mémoire avec la JVM d'Apple,
            // peut-être parce que com.apple.laf.AquaRootPaneUI a un WindowListener sur la fermeture de fenêtre,
            // qui ferait du nettoyage...
            // pb: sur Windows, si on met null, un utilisateur peut cliquer sur la fenêtre et se retrouver bloqué...
            final Component parent;
            if (System.getProperty("os.name").startsWith("Mac OS"))
                parent = null;
            else
                parent = this;
            final int r = JOptionPane.showConfirmDialog(parent, rb.getString("fermeture.EnregistrerAvant"),
                rb.getString("fermeture.Fermeture"), JOptionPane.YES_NO_CANCEL_OPTION);
            if (r == JOptionPane.YES_OPTION)
                enregistrer();
            else if (r == JOptionPane.CANCEL_OPTION)
                return;
        }
        if (fenetreSource != null)
            fenetreSource.setVisible(false);
        if (fenetreValidation != null)
            fenetreValidation.setVisible(false);
        setVisible(false);
        if (actionFermeture != null)
            actionFermeture.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "fermer"));
        /*
        // aide pour le garbage collector
        if (doc != null) {
            try {
                doc.remove(0, doc.getLength());
            } catch (BadLocationException ex) {
                System.err.println("JaxeAppletFrame.fermer: " + ex.getClass().getName() + ": " + ex.getMessage());
            }
        }
        */
        ImageKeeper.flushImages();
    }
    
    private static void write(OutputStream out, String s) throws IOException {
        out.write(s.getBytes("US-ASCII"));
    }
    
    /* les standards d'encodage MIME ne sont pas reconnus par PHP -> on utilise UTF-8
    private static void encode1522(StringBuffer sb, int b) throws IOException {
        sb.append('=');
        char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
        char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
        sb.append(hex1);
        sb.append(hex2);
    }
    
    // implémentation limitée de RFC 1522, pour ISO-8859-1
    private static void encode1522(BufferedOutputStream out, String s) throws IOException {
        final String encodage = "ISO-8859-1";
        StringBuffer sb = new StringBuffer();
        sb.append("=?");
        sb.append(encodage);
        sb.append("?Q?");
        byte[] bytes = s.getBytes(encodage);
        for (int i = 0; i < bytes.length; i++) {
            int b = bytes[i];
            if (b < 0)
                b = 256 + b;
            if ((b >= 0x41 && b<= 0x5A) || (b >= 0x61 && b<= 0x7A))
                sb.append((char)b);
            else if (b == 0x20)
                sb.append('_');
            else
                encode1522(sb, b);
        }
        sb.append("?=");
        write(out, sb.toString());
    }
    */
    
    public void enregistrer() {
        try {
            URLConnection urlConn = urlEnregistrement.openConnection();
            urlConn.setDoInput(true);
            urlConn.setDoOutput(true);
            urlConn.setUseCaches(false);
            //urlConn.setRequestProperty("HTTP_REFERER", codebase);
            final String bound = "AaB03x";
            urlConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + bound);
            BufferedOutputStream out = new BufferedOutputStream(urlConn.getOutputStream());
            write(out, "--" + bound + "\r\n");
            /*
            write(out, "Content-Disposition: form-data; name=\"contrib\"\r\n");
            write(out, "Content-type: text/plain; charset=UTF-8\r\n");
            write(out, "Content-transfer-encoding: 8bit\r\n\r\n");
            final String chemin = URLDecoder.decode(urlFichier.getPath(), "UTF-8");
            String contrib = chemin;
            int ind = contrib.lastIndexOf('/');
            if (ind != -1)
                contrib = contrib.substring(ind+1);
            ind = contrib.lastIndexOf('.');
            if (ind != -1)
                contrib = contrib.substring(0, ind);
            out.write(contrib.getBytes("UTF-8"));
            write(out, "\r\n--" + bound + "\r\n");
            */
            final String chemin = URLDecoder.decode(urlFichier.getPath(), "UTF-8");
            write(out, "Content-Disposition: form-data; name=\"chemin\"\r\n");
            write(out, "Content-type: text/plain; charset=UTF-8\r\n");
            write(out, "Content-transfer-encoding: 8bit\r\n\r\n");
            out.write(chemin.getBytes("UTF-8"));
            write(out, "\r\n--" + bound + "\r\n");
            
            write(out, "Content-Disposition: form-data; name=\"contenu\"; filename=\"");
            //encode1522(out, chemin);
            // les standards d'encodage MIME ne sont pas reconnus par PHP -> on utilise UTF-8
            out.write(chemin.getBytes("UTF-8"));
            write(out, "\"\r\n");
            write(out, "Content-Type: application/octet-stream\r\n\r\n");
            OutputStreamWriter nos = new OutputStreamWriter(out, doc.encodage);
            sendToWriter(nos);
            nos.flush();
            write(out, "\r\n--" + bound + "--\r\n\r\n");
            out.flush();
            out.close();
            
            // lecture réponse
            // première ligne : "ok" ou "erreur"
            // deuxième ligne : message d'erreur s'il y a une erreur
            BufferedReader rd = new BufferedReader(new InputStreamReader(urlConn.getInputStream(), "UTF-8"));
            String reponse = rd.readLine();
            if (reponse == null) {
                JOptionPane.showMessageDialog(null,
                    "Erreur à l'enregistrement: aucune réponse du serveur.",
                    "Enregistrement", JOptionPane.ERROR_MESSAGE);
            } else if ("erreur".equals(reponse)) {
                String message = "<html><body><p>Erreur à l'enregistrement:</p>";
                final String ligne2 = rd.readLine();
                if (ligne2 != null) {
                    message += "<p>" + ligne2 + "</p>";
                    final String ligne3 = rd.readLine();
                    if (ligne3 != null)
                        message += "<p>" + ligne3 + "</p>";
                }
                message += "</body></html>";
                JOptionPane.showMessageDialog(null, message, "Enregistrement", JOptionPane.ERROR_MESSAGE);
            } else if ("ok".equals(reponse)) {
                JOptionPane.showMessageDialog(null,
                    "Le fichier a bien été enregistré.",
                    "Enregistrement", JOptionPane.INFORMATION_MESSAGE);
                doc.modif = false;
            } else {
                String message = "<html><body><p>Erreur à l'enregistrement: réponse étrange du serveur:</p>";
                message += "<p>" + reponse + "</p>";
                final String ligne2 = rd.readLine();
                if (ligne2 != null)
                    message += "<p>" + ligne2 + "</p>";
                message += "</body></html>";
                JOptionPane.showMessageDialog(null, message, "Enregistrement", JOptionPane.ERROR_MESSAGE);
            }
            rd.close();
        } catch (IOException ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(null, ex.getMessage(), "Enregistrement", JOptionPane.ERROR_MESSAGE);
        }

        //setEnregistrer(true);
    }
    /*
    public boolean getEnregistrer() {
        return(aEnregistrer);
    }
    
    public void setEnregistrer(boolean enr) {
        aEnregistrer = enr;
    }
    */
    
    protected class ActionEnregistrer extends AbstractAction {
        public ActionEnregistrer() {
            super(rb.getString("menus.Enregistrer"));
        }
        public void actionPerformed(ActionEvent e) {
            enregistrer();
        }
    }
    
    protected class ActionSource extends AbstractAction {
        public ActionSource() {
            super(rb.getString("menus.Source"));
        }
        public void actionPerformed(ActionEvent e) {
            if (fenetreSource == null)
                fenetreSource = new AppletSourceFrame(doc, JaxeAppletFrame.this);
            else
                fenetreSource.miseAJour();
        }
    }
    
    protected class ActionValider extends AbstractAction {
        public ActionValider() {
            super(rb.getString("menus.Validation"));
        }
        public void actionPerformed(ActionEvent e) {
            if (fenetreValidation == null)
                fenetreValidation = new AppletValidationFrame(doc, JaxeAppletFrame.this);
            else
                fenetreValidation.miseAJour();
        }
    }
    
    protected class ActionFermer extends AbstractAction {
        public ActionFermer() {
            super(rb.getString("menus.Fermer"));
        }
        public void actionPerformed(ActionEvent e) {
            fermer();
        }
    }
    
    protected class UndoAction extends AbstractAction {
        public UndoAction() {
            super(rb.getString("menus.Annuler"));
            setEnabled(false);
        }
          
        public void actionPerformed(ActionEvent e) {
            textPane.undo();
        }
        
        protected void updateUndoState() {
            UndoManager undo = textPane.getUndo();
            if (undo.canUndo()) {
                setEnabled(true);
                putValue(Action.NAME, undo.getUndoPresentationName());
            } else {
                setEnabled(false);
                putValue(Action.NAME, rb.getString("menus.Annuler"));
            }
        }      
    }
    
    protected class RedoAction extends AbstractAction {
        public RedoAction() {
            super(rb.getString("menus.Retablir"));
            setEnabled(false);
        }

        public void actionPerformed(ActionEvent e) {
            UndoManager undo = textPane.getUndo();
            try {
                undo.redo();
            } catch (CannotRedoException ex) {
                System.out.println("Impossible de rétablir: " + ex);
                ex.printStackTrace();
            }
            updateRedoState();
            monActionAnnuler.updateUndoState();
        }

        protected void updateRedoState() {
            UndoManager undo = textPane.getUndo();
            if (undo.canRedo()) {
                setEnabled(true);
                putValue(Action.NAME, undo.getRedoPresentationName());
            } else {
                setEnabled(false);
                putValue(Action.NAME, rb.getString("menus.Retablir"));
            }
        }
    }
    
    protected class CaretListenerLabel extends JLabel implements CaretListener {
        
        private JaxeDocument doc;
        
        public CaretListenerLabel (final String label, final JaxeDocument doc) {
            super(label);
            this.doc = doc;
        }

        public void caretUpdate(final CaretEvent e) {
            final int dot = e.getDot();
            final int mark = e.getMark();
            if (dot == mark) {  // no selection
                setText(dot + ": " + doc.getPathAsString(dot));
            }
            majMenus(dot);
        }
    }
    
    protected class ActionRechercher extends TextAction {

        public ActionRechercher() {
            super(rb.getString("menus.Rechercher"));
        }

        public void actionPerformed(ActionEvent e) {
            textPane.rechercher();
        }
    }

    protected class ActionSuivant extends TextAction {

        public ActionSuivant() {
            super(rb.getString("menus.RechercherSuivant"));
        }

        public void actionPerformed(ActionEvent e) {
            textPane.suivant();
        }
    }

    protected class ActionToutSelectionner extends TextAction {

        public ActionToutSelectionner() {
            super(rb.getString("menus.ToutSelectionner"));
        }

        public void actionPerformed(final ActionEvent e) {
            final JTextComponent target = getTextComponent(e);
            if (target instanceof JaxeTextPane)
                ((JaxeTextPane)target).toutSelectionner();
            else if (target != null)
                target.selectAll();
        }
    }
    
    class ActionOrthographe extends AbstractAction {

        public ActionOrthographe() {
            super(rb.getString("menus.Orthographe"));
        }
        public void actionPerformed(final ActionEvent e) {
            if (urlDictionnaire == null) {
                JOptionPane.showMessageDialog(JaxeAppletFrame.this, JaxeResourceBundle.getRB().getString("erreur.Dictionnaire"));
                return;
            }
            SpellDictionary dictionary;
            final String encoding = "ISO-8859-1"; // for dictionary and phonetic file
            // user dictionary is using the default text encoding
            try {
                // build main dictionary
                if (urlPhonetique != null)
                    dictionary = new SpellDictionaryDichoMem(urlDictionnaire, urlPhonetique, encoding);
                else
                    dictionary = new SpellDictionaryDichoMem(urlDictionnaire, encoding);
                final JTextComponentSpellChecker sc = new JTextComponentSpellChecker(dictionary);
                
                // no user dictionnary
                
                // start checking
                final int status = sc.spellCheck(textPane);
                if (status == SpellChecker.SPELLCHECK_OK)
                    JOptionPane.showMessageDialog(JaxeAppletFrame.this, JaxeResourceBundle.getRB().getString("orthographe.aucuneErreur"));
            } catch (final Exception ex) {
                System.err.println("ActionOrthographe: " + ex.getClass().getName() + ": " + ex.getMessage());
            }
        }
    }
}
